@workos-inc/widgets 0.0.0-pre.1 → 0.0.0-pre.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/dist/cjs/lib/api/role.d.ts.map +1 -1
  2. package/dist/cjs/lib/api/role.js +35 -14
  3. package/dist/cjs/lib/api/role.js.map +1 -1
  4. package/dist/cjs/lib/api/user.d.ts.map +1 -1
  5. package/dist/cjs/lib/api/user.js +105 -67
  6. package/dist/cjs/lib/api/user.js.map +1 -1
  7. package/dist/cjs/lib/constants.d.ts +2 -1
  8. package/dist/cjs/lib/constants.d.ts.map +1 -1
  9. package/dist/cjs/lib/constants.js +3 -2
  10. package/dist/cjs/lib/constants.js.map +1 -1
  11. package/dist/cjs/lib/delete-user-dialog.d.ts.map +1 -1
  12. package/dist/cjs/lib/delete-user-dialog.js +1 -1
  13. package/dist/cjs/lib/delete-user-dialog.js.map +1 -1
  14. package/dist/cjs/lib/edit-user-details-dialog.d.ts.map +1 -1
  15. package/dist/cjs/lib/edit-user-details-dialog.js +3 -5
  16. package/dist/cjs/lib/edit-user-details-dialog.js.map +1 -1
  17. package/dist/cjs/lib/elements.d.ts +21 -12
  18. package/dist/cjs/lib/elements.d.ts.map +1 -1
  19. package/dist/cjs/lib/elements.js +118 -30
  20. package/dist/cjs/lib/elements.js.map +1 -1
  21. package/dist/cjs/lib/error-boundary.d.ts +58 -0
  22. package/dist/cjs/lib/error-boundary.d.ts.map +1 -0
  23. package/dist/cjs/lib/error-boundary.js +113 -0
  24. package/dist/cjs/lib/error-boundary.js.map +1 -0
  25. package/dist/cjs/lib/errors.d.ts +34 -0
  26. package/dist/cjs/lib/errors.d.ts.map +1 -0
  27. package/dist/cjs/lib/errors.js +40 -0
  28. package/dist/cjs/lib/errors.js.map +1 -0
  29. package/dist/cjs/lib/invite-user-dialog.d.ts.map +1 -1
  30. package/dist/cjs/lib/invite-user-dialog.js +2 -2
  31. package/dist/cjs/lib/invite-user-dialog.js.map +1 -1
  32. package/dist/cjs/lib/resend-invite-dialog.d.ts.map +1 -1
  33. package/dist/cjs/lib/resend-invite-dialog.js +2 -2
  34. package/dist/cjs/lib/resend-invite-dialog.js.map +1 -1
  35. package/dist/cjs/lib/revoke-invite-dialog.d.ts.map +1 -1
  36. package/dist/cjs/lib/revoke-invite-dialog.js +1 -1
  37. package/dist/cjs/lib/revoke-invite-dialog.js.map +1 -1
  38. package/dist/cjs/lib/use-layout-effect.d.ts +4 -0
  39. package/dist/cjs/lib/use-layout-effect.d.ts.map +1 -0
  40. package/dist/cjs/lib/use-layout-effect.js +8 -0
  41. package/dist/cjs/lib/use-layout-effect.js.map +1 -0
  42. package/dist/cjs/lib/user-actions-dropdown.d.ts.map +1 -1
  43. package/dist/cjs/lib/user-actions-dropdown.js +2 -9
  44. package/dist/cjs/lib/user-actions-dropdown.js.map +1 -1
  45. package/dist/cjs/lib/users-filter.d.ts +1 -0
  46. package/dist/cjs/lib/users-filter.d.ts.map +1 -1
  47. package/dist/cjs/lib/users-filter.js +2 -9
  48. package/dist/cjs/lib/users-filter.js.map +1 -1
  49. package/dist/cjs/lib/users-management-context.d.ts +2 -2
  50. package/dist/cjs/lib/users-management-context.d.ts.map +1 -1
  51. package/dist/cjs/lib/users-management.d.ts +9 -6
  52. package/dist/cjs/lib/users-management.d.ts.map +1 -1
  53. package/dist/cjs/lib/users-management.js +77 -20
  54. package/dist/cjs/lib/users-management.js.map +1 -1
  55. package/dist/cjs/lib/users-search.d.ts.map +1 -1
  56. package/dist/cjs/lib/users-search.js +3 -9
  57. package/dist/cjs/lib/users-search.js.map +1 -1
  58. package/dist/cjs/lib/utils.d.ts +2 -0
  59. package/dist/cjs/lib/utils.d.ts.map +1 -1
  60. package/dist/cjs/lib/utils.js +18 -0
  61. package/dist/cjs/lib/utils.js.map +1 -1
  62. package/dist/cjs/users-management.client.d.ts +1 -1
  63. package/dist/cjs/users-management.client.d.ts.map +1 -1
  64. package/dist/cjs/users-management.client.js +15 -5
  65. package/dist/cjs/users-management.client.js.map +1 -1
  66. package/dist/cjs/workos-widgets.client.d.ts.map +1 -1
  67. package/dist/cjs/workos-widgets.client.js +2 -1
  68. package/dist/cjs/workos-widgets.client.js.map +1 -1
  69. package/dist/esm/lib/api/role.d.ts.map +1 -1
  70. package/dist/esm/lib/api/role.js +35 -14
  71. package/dist/esm/lib/api/role.js.map +1 -1
  72. package/dist/esm/lib/api/user.d.ts.map +1 -1
  73. package/dist/esm/lib/api/user.js +105 -67
  74. package/dist/esm/lib/api/user.js.map +1 -1
  75. package/dist/esm/lib/constants.d.ts +2 -1
  76. package/dist/esm/lib/constants.d.ts.map +1 -1
  77. package/dist/esm/lib/constants.js +2 -1
  78. package/dist/esm/lib/constants.js.map +1 -1
  79. package/dist/esm/lib/delete-user-dialog.d.ts.map +1 -1
  80. package/dist/esm/lib/delete-user-dialog.js +2 -2
  81. package/dist/esm/lib/delete-user-dialog.js.map +1 -1
  82. package/dist/esm/lib/edit-user-details-dialog.d.ts.map +1 -1
  83. package/dist/esm/lib/edit-user-details-dialog.js +4 -6
  84. package/dist/esm/lib/edit-user-details-dialog.js.map +1 -1
  85. package/dist/esm/lib/elements.d.ts +21 -12
  86. package/dist/esm/lib/elements.d.ts.map +1 -1
  87. package/dist/esm/lib/elements.js +95 -30
  88. package/dist/esm/lib/elements.js.map +1 -1
  89. package/dist/esm/lib/error-boundary.d.ts +58 -0
  90. package/dist/esm/lib/error-boundary.d.ts.map +1 -0
  91. package/dist/esm/lib/error-boundary.js +86 -0
  92. package/dist/esm/lib/error-boundary.js.map +1 -0
  93. package/dist/esm/lib/errors.d.ts +34 -0
  94. package/dist/esm/lib/errors.d.ts.map +1 -0
  95. package/dist/esm/lib/errors.js +34 -0
  96. package/dist/esm/lib/errors.js.map +1 -0
  97. package/dist/esm/lib/invite-user-dialog.d.ts.map +1 -1
  98. package/dist/esm/lib/invite-user-dialog.js +3 -3
  99. package/dist/esm/lib/invite-user-dialog.js.map +1 -1
  100. package/dist/esm/lib/resend-invite-dialog.d.ts.map +1 -1
  101. package/dist/esm/lib/resend-invite-dialog.js +3 -3
  102. package/dist/esm/lib/resend-invite-dialog.js.map +1 -1
  103. package/dist/esm/lib/revoke-invite-dialog.d.ts.map +1 -1
  104. package/dist/esm/lib/revoke-invite-dialog.js +2 -2
  105. package/dist/esm/lib/revoke-invite-dialog.js.map +1 -1
  106. package/dist/esm/lib/use-layout-effect.d.ts +4 -0
  107. package/dist/esm/lib/use-layout-effect.d.ts.map +1 -0
  108. package/dist/esm/lib/use-layout-effect.js +5 -0
  109. package/dist/esm/lib/use-layout-effect.js.map +1 -0
  110. package/dist/esm/lib/user-actions-dropdown.d.ts.map +1 -1
  111. package/dist/esm/lib/user-actions-dropdown.js +3 -10
  112. package/dist/esm/lib/user-actions-dropdown.js.map +1 -1
  113. package/dist/esm/lib/users-filter.d.ts +1 -0
  114. package/dist/esm/lib/users-filter.d.ts.map +1 -1
  115. package/dist/esm/lib/users-filter.js +4 -11
  116. package/dist/esm/lib/users-filter.js.map +1 -1
  117. package/dist/esm/lib/users-management-context.d.ts +2 -2
  118. package/dist/esm/lib/users-management-context.d.ts.map +1 -1
  119. package/dist/esm/lib/users-management.d.ts +9 -6
  120. package/dist/esm/lib/users-management.d.ts.map +1 -1
  121. package/dist/esm/lib/users-management.js +74 -22
  122. package/dist/esm/lib/users-management.js.map +1 -1
  123. package/dist/esm/lib/users-search.d.ts.map +1 -1
  124. package/dist/esm/lib/users-search.js +5 -11
  125. package/dist/esm/lib/users-search.js.map +1 -1
  126. package/dist/esm/lib/utils.d.ts +2 -0
  127. package/dist/esm/lib/utils.d.ts.map +1 -1
  128. package/dist/esm/lib/utils.js +13 -0
  129. package/dist/esm/lib/utils.js.map +1 -1
  130. package/dist/esm/users-management.client.d.ts +1 -1
  131. package/dist/esm/users-management.client.d.ts.map +1 -1
  132. package/dist/esm/users-management.client.js +16 -6
  133. package/dist/esm/users-management.client.js.map +1 -1
  134. package/dist/esm/workos-widgets.client.d.ts.map +1 -1
  135. package/dist/esm/workos-widgets.client.js +2 -1
  136. package/dist/esm/workos-widgets.client.js.map +1 -1
  137. package/dist/tsconfig.cjs.tsbuildinfo +1 -1
  138. package/dist/tsconfig.esm.tsbuildinfo +1 -1
  139. package/package.json +8 -3
  140. package/src/base.css +111 -0
  141. package/src/lib/api/role.ts +39 -16
  142. package/src/lib/api/user.ts +119 -75
  143. package/src/lib/constants.ts +2 -1
  144. package/src/lib/delete-user-dialog.tsx +7 -3
  145. package/src/lib/edit-user-details-dialog.tsx +15 -10
  146. package/src/lib/elements.tsx +242 -61
  147. package/src/lib/error-boundary.tsx +166 -0
  148. package/src/lib/errors.ts +49 -0
  149. package/src/lib/invite-user-dialog.tsx +21 -12
  150. package/src/lib/resend-invite-dialog.tsx +11 -5
  151. package/src/lib/revoke-invite-dialog.tsx +7 -3
  152. package/src/lib/use-layout-effect.ts +6 -0
  153. package/src/lib/user-actions-dropdown.tsx +8 -16
  154. package/src/lib/users-filter.tsx +13 -73
  155. package/src/lib/users-management-context.tsx +1 -1
  156. package/src/lib/users-management.tsx +345 -184
  157. package/src/lib/users-search.tsx +5 -63
  158. package/src/lib/utils.ts +21 -0
  159. package/src/users-management.client.tsx +39 -16
  160. package/src/users-management.css +4 -0
  161. package/src/workos-widgets.client.tsx +2 -1
  162. package/dist/cjs/lib/pagination.d.ts +0 -8
  163. package/dist/cjs/lib/pagination.d.ts.map +0 -1
  164. package/dist/cjs/lib/pagination.js +0 -67
  165. package/dist/cjs/lib/pagination.js.map +0 -1
  166. package/dist/esm/lib/pagination.d.ts +0 -8
  167. package/dist/esm/lib/pagination.d.ts.map +0 -1
  168. package/dist/esm/lib/pagination.js +0 -40
  169. package/dist/esm/lib/pagination.js.map +0 -1
  170. package/src/lib/pagination.tsx +0 -69
@@ -5,6 +5,8 @@ import {
5
5
  Box,
6
6
  Flex,
7
7
  Grid,
8
+ Heading,
9
+ Select,
8
10
  Skeleton,
9
11
  Table,
10
12
  Text,
@@ -19,9 +21,12 @@ import {
19
21
  IconButton,
20
22
  PrimaryButton,
21
23
  SecondaryButton,
24
+ SelectContent,
25
+ SelectItem,
26
+ SelectTrigger,
27
+ TextField,
22
28
  } from "./elements";
23
29
  import { InviteUserDialog } from "./invite-user-dialog";
24
- import { Pagination } from "./pagination";
25
30
  import { SearchProvider, useSearchContext } from "./search-provider";
26
31
  import { useIsHydrated } from "./use-is-hydrated";
27
32
  import { UserActionsDropdown } from "./user-actions-dropdown";
@@ -30,65 +35,81 @@ import { UsersSearch } from "./users-search";
30
35
  import { getBestName, getComparativeReadableDate } from "./utils";
31
36
  import { USER_ROW_LIMIT } from "./constants";
32
37
  import { useUsersManagementContext } from "./users-management-context";
38
+ import clsx from "clsx";
39
+ import { ApiError, FetchError, NoAuthTokenError } from "./errors";
33
40
 
34
41
  interface UsersManagementProps {
35
- rolesData: Role[] | undefined;
36
- userData: Paginated<User[]> | undefined;
37
-
38
- // Render the rows with an alternate background color
39
- alternate?: boolean;
42
+ rolesData: Role[];
43
+ userData: Paginated<User[]>;
44
+ disableRolesFilter?: boolean;
40
45
  // When the users list is loading new users
41
- isPending?: boolean;
42
- // Show the skeleton UI
43
- isInitialLoading?: boolean;
46
+ isPending: boolean | undefined;
44
47
  }
45
48
 
46
49
  export const UsersManagement = ({
47
50
  userData,
48
51
  rolesData,
49
- alternate,
50
52
  isPending,
51
- isInitialLoading,
53
+ disableRolesFilter,
52
54
  }: UsersManagementProps) => {
53
55
  const users = userData?.data;
54
56
  const usersCount = users?.length ?? 0;
55
57
  const isHydrated = useIsHydrated();
58
+ const pagination = userData?.pagination;
59
+ const { dispatch } = useUsersManagementContext();
60
+
61
+ // we only want to show the loading indicator for some buttons if the request
62
+ // is still pending after 500ms. If the request is fast enough the indicator
63
+ // is a bit jarring.
64
+ const [deferredLoading, setDeferredLoading] = React.useState(false);
65
+ React.useEffect(() => {
66
+ if (isPending) {
67
+ const timeoutId = window.setTimeout(() => {
68
+ setDeferredLoading(true);
69
+ }, 500);
70
+ return () => {
71
+ window.clearTimeout(timeoutId);
72
+ };
73
+ } else {
74
+ setDeferredLoading(false);
75
+ }
76
+ }, [isPending]);
77
+
56
78
  return (
57
79
  <SearchProvider>
58
- <Flex direction="column" gap="3">
80
+ <UsersManagementRoot>
59
81
  <Grid columns="1fr auto" gap="2">
60
82
  <Flex gap="2" align="center">
61
- <Skeleton loading={isInitialLoading}>
62
- <Box flexBasis="380px" flexGrow="0" flexShrink="1">
63
- <UsersSearch />
64
- </Box>
65
- </Skeleton>
66
- <Skeleton loading={isInitialLoading}>
67
- <Box flexGrow="0" flexShrink="0">
68
- <UsersFilter roles={rolesData} />
69
- </Box>
70
- </Skeleton>
71
- </Flex>
72
-
73
- <Skeleton loading={isInitialLoading}>
74
- <Box flexGrow="0" flexShrink="0" style={{ placeSelf: "flex-end" }}>
75
- <InviteUserDialog>
76
- <PrimaryButton>Invite user</PrimaryButton>
77
- </InviteUserDialog>
83
+ <Box flexBasis="380px" flexGrow="0" flexShrink="1">
84
+ <UsersSearch />
78
85
  </Box>
79
- </Skeleton>
86
+ <Box flexGrow="0" flexShrink="0">
87
+ <Select.Root value="all" onValueChange={() => void 0}>
88
+ <SelectTrigger>All</SelectTrigger>
89
+ <SelectContent>
90
+ <SelectItem value="all">All</SelectItem>
91
+ </SelectContent>
92
+ </Select.Root>
93
+ <UsersFilter roles={rolesData} disabled={disableRolesFilter} />
94
+ </Box>
95
+ </Flex>
96
+ <Box flexGrow="0" flexShrink="0" style={{ placeSelf: "flex-end" }}>
97
+ <InviteUserDialog>
98
+ <PrimaryButton>Invite user</PrimaryButton>
99
+ </InviteUserDialog>
100
+ </Box>
80
101
  </Grid>
81
102
  <Table.Root variant="ghost" size="1">
82
103
  <Table.Header>
83
104
  <Table.Row>
84
105
  <Table.ColumnHeaderCell width="260px">
85
- <Skeleton loading={isInitialLoading}>User</Skeleton>
106
+ User
86
107
  </Table.ColumnHeaderCell>
87
108
  <Table.ColumnHeaderCell width="100px">
88
- <Skeleton loading={isInitialLoading}>Role</Skeleton>
109
+ Role
89
110
  </Table.ColumnHeaderCell>
90
111
  <Table.ColumnHeaderCell width="140px">
91
- <Skeleton loading={isInitialLoading}>Last active</Skeleton>
112
+ Last active
92
113
  </Table.ColumnHeaderCell>
93
114
  <Table.ColumnHeaderCell width="28px" />
94
115
  </Table.Row>
@@ -100,118 +121,108 @@ export const UsersManagement = ({
100
121
  opacity: isPending && usersCount > 0 ? 0.5 : 1,
101
122
  }}
102
123
  >
103
- {isInitialLoading && (
104
- <SkeletonRows length={USER_ROW_LIMIT} alternate={alternate} />
105
- )}
106
- {users?.map((user, i) => {
107
- // TODO only support one role for now
108
- const userRole = user.roles[0]?.name;
109
- const userDisplayName = getBestName(user);
110
- const dimText =
111
- user.status === "InviteRevoked" ||
112
- user.status === "InviteExpired";
113
- return (
114
- <Table.Row
115
- key={user.id}
116
- align="center"
117
- style={{
118
- background:
119
- alternate && i % 2 === 1 ? "var(--gray-a1)" : undefined,
120
- }}
121
- >
122
- <Table.RowHeaderCell>
123
- <Flex
124
- align="center"
125
- gap="3"
126
- overflow="hidden"
127
- height="var(--space-7)"
128
- >
129
- <Avatar
130
- size="2"
131
- fallback={<FallbackUserIcon />}
132
- src={user.profilePictureUrl ?? undefined}
133
- dim={dimText}
134
- />
124
+ {users.length > 0 ? (
125
+ users.map((user) => {
126
+ // TODO only support one role for now
127
+ const userRole = user.roles[0]?.name;
128
+ const userDisplayName = getBestName(user);
129
+ const dimText =
130
+ user.status === "InviteRevoked" ||
131
+ user.status === "InviteExpired";
132
+ return (
133
+ <Table.Row key={user.id} align="center">
134
+ <Table.RowHeaderCell>
135
+ <Flex
136
+ align="center"
137
+ gap="3"
138
+ overflow="hidden"
139
+ height="var(--space-7)"
140
+ >
141
+ <Avatar
142
+ size="2"
143
+ fallback={<FallbackUserIcon />}
144
+ src={user.profilePictureUrl ?? undefined}
145
+ dim={dimText}
146
+ />
135
147
 
136
- {userDisplayName ? (
137
- <Flex
138
- direction="column"
139
- align="start"
140
- height="var(--space-7)"
141
- justify="center"
142
- overflow="hidden"
143
- >
148
+ {userDisplayName ? (
149
+ <Flex
150
+ direction="column"
151
+ align="start"
152
+ height="var(--space-7)"
153
+ justify="center"
154
+ overflow="hidden"
155
+ >
156
+ <Flex gap="2" align="center" minWidth="0">
157
+ <TableCellText dim={dimText}>
158
+ {userDisplayName}
159
+ </TableCellText>
160
+ <UserBadge user={user} />
161
+ </Flex>
162
+ <TableCellText
163
+ level="secondary"
164
+ title={user.email}
165
+ dim={dimText}
166
+ >
167
+ {user.email}
168
+ </TableCellText>
169
+ </Flex>
170
+ ) : (
144
171
  <Flex gap="2" align="center" minWidth="0">
145
- <TableCellText dim={dimText}>
146
- {userDisplayName}
172
+ <TableCellText dim={dimText} title={user.email}>
173
+ {user.email}
147
174
  </TableCellText>
148
175
  <UserBadge user={user} />
149
176
  </Flex>
150
- <TableCellText
151
- level="secondary"
152
- title={user.email}
153
- dim={dimText}
177
+ )}
178
+ </Flex>
179
+ </Table.RowHeaderCell>
180
+ <Table.Cell>
181
+ <TableCellText dim={dimText}>
182
+ {userRole || (
183
+ <>
184
+ <VisuallyHidden>No roles assigned</VisuallyHidden>
185
+ <span aria-hidden style={{ userSelect: "none" }}>
186
+
187
+ </span>
188
+ </>
189
+ )}
190
+ </TableCellText>
191
+ </Table.Cell>
192
+ <Table.Cell>
193
+ <LastActive
194
+ user={user}
195
+ isHydrated={isHydrated}
196
+ dim={dimText}
197
+ />
198
+ </Table.Cell>
199
+ <Table.Cell justify="end">
200
+ <UserActionsDropdown user={user}>
201
+ <IconButton title="User actions">
202
+ <VisuallyHidden>User actions</VisuallyHidden>
203
+ <svg
204
+ xmlns="http://www.w3.org/2000/svg"
205
+ fill="none"
206
+ viewBox="0 0 24 24"
207
+ width="16"
208
+ height="16"
209
+ strokeWidth={1.5}
210
+ stroke="currentColor"
211
+ aria-hidden
154
212
  >
155
- {user.email}
156
- </TableCellText>
157
- </Flex>
158
- ) : (
159
- <Flex gap="2" align="center" minWidth="0">
160
- <TableCellText dim={dimText} title={user.email}>
161
- {user.email}
162
- </TableCellText>
163
- <UserBadge user={user} />
164
- </Flex>
165
- )}
166
- </Flex>
167
- </Table.RowHeaderCell>
168
- <Table.Cell>
169
- <TableCellText dim={dimText}>
170
- {userRole || (
171
- <>
172
- <VisuallyHidden>No roles assigned</VisuallyHidden>
173
- <span aria-hidden style={{ userSelect: "none" }}>
174
-
175
- </span>
176
- </>
177
- )}
178
- </TableCellText>
179
- </Table.Cell>
180
- <Table.Cell>
181
- <LastActive
182
- user={user}
183
- isHydrated={isHydrated}
184
- dim={dimText}
185
- />
186
- </Table.Cell>
187
- <Table.Cell justify="end">
188
- <UserActionsDropdown user={user}>
189
- <IconButton title="User actions">
190
- <VisuallyHidden>User actions</VisuallyHidden>
191
- <svg
192
- xmlns="http://www.w3.org/2000/svg"
193
- fill="none"
194
- viewBox="0 0 24 24"
195
- width="16"
196
- height="16"
197
- strokeWidth={1.5}
198
- stroke="currentColor"
199
- aria-hidden
200
- >
201
- <path
202
- strokeLinecap="round"
203
- strokeLinejoin="round"
204
- d="M6.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM12.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM18.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z"
205
- />
206
- </svg>
207
- </IconButton>
208
- </UserActionsDropdown>
209
- </Table.Cell>
210
- </Table.Row>
211
- );
212
- })}
213
-
214
- {users?.length === 0 && (
213
+ <path
214
+ strokeLinecap="round"
215
+ strokeLinejoin="round"
216
+ d="M6.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM12.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM18.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z"
217
+ />
218
+ </svg>
219
+ </IconButton>
220
+ </UserActionsDropdown>
221
+ </Table.Cell>
222
+ </Table.Row>
223
+ );
224
+ })
225
+ ) : (
215
226
  <Table.Row align="center">
216
227
  <Table.Cell colSpan={4}>
217
228
  <UsersManagementEmptyState />
@@ -221,12 +232,205 @@ export const UsersManagement = ({
221
232
  </Table.Body>
222
233
  </Table.Root>
223
234
 
224
- <Pagination isPending={isPending} pagination={userData?.pagination} />
225
- </Flex>
235
+ <Flex gap="2" justify="end">
236
+ <SecondaryButton
237
+ size="1"
238
+ disabled={!pagination?.after || isPending || undefined}
239
+ loading={deferredLoading}
240
+ onClick={() => {
241
+ if (pagination?.after) {
242
+ dispatch({
243
+ type: "SET_PAGINATION",
244
+ pagination: { after: pagination.after },
245
+ });
246
+ }
247
+ }}
248
+ >
249
+ Previous
250
+ </SecondaryButton>
251
+ <SecondaryButton
252
+ size="1"
253
+ disabled={!pagination?.before || isPending || undefined}
254
+ loading={deferredLoading}
255
+ onClick={() => {
256
+ if (pagination?.before) {
257
+ dispatch({
258
+ type: "SET_PAGINATION",
259
+ pagination: { before: pagination.before },
260
+ });
261
+ }
262
+ }}
263
+ >
264
+ Next
265
+ </SecondaryButton>
266
+ </Flex>
267
+ </UsersManagementRoot>
226
268
  </SearchProvider>
227
269
  );
228
270
  };
229
271
 
272
+ export function UsersManagementLoading() {
273
+ return (
274
+ <UsersManagementRoot>
275
+ <Grid columns="1fr auto" gap="2">
276
+ <Flex gap="2" align="center">
277
+ <Skeleton loading>
278
+ <Box flexBasis="380px" flexGrow="0" flexShrink="1">
279
+ <TextField />
280
+ </Box>
281
+ </Skeleton>
282
+ <Skeleton loading>
283
+ <Box flexGrow="0" flexShrink="0">
284
+ <Select.Root value="all" onValueChange={() => void 0}>
285
+ <SelectTrigger>All</SelectTrigger>
286
+ <SelectContent>
287
+ <SelectItem value="all">All</SelectItem>
288
+ </SelectContent>
289
+ </Select.Root>
290
+ </Box>
291
+ </Skeleton>
292
+ </Flex>
293
+ <Skeleton loading>
294
+ <Box flexGrow="0" flexShrink="0" style={{ placeSelf: "flex-end" }}>
295
+ <PrimaryButton>Invite user</PrimaryButton>
296
+ </Box>
297
+ </Skeleton>
298
+ </Grid>
299
+ <Table.Root variant="ghost" size="1">
300
+ <Table.Header>
301
+ <Table.Row>
302
+ <Table.ColumnHeaderCell width="260px">
303
+ <Skeleton loading>User</Skeleton>
304
+ </Table.ColumnHeaderCell>
305
+ <Table.ColumnHeaderCell width="100px">
306
+ <Skeleton>Role</Skeleton>
307
+ </Table.ColumnHeaderCell>
308
+ <Table.ColumnHeaderCell width="140px">
309
+ <Skeleton>Last active</Skeleton>
310
+ </Table.ColumnHeaderCell>
311
+ <Table.ColumnHeaderCell width="28px" />
312
+ </Table.Row>
313
+ </Table.Header>
314
+
315
+ <Table.Body>
316
+ {Array.from({ length: USER_ROW_LIMIT }, (_, index) => (
317
+ <Table.Row key={index} align="center">
318
+ <Table.RowHeaderCell>
319
+ <Flex align="center" gap="3">
320
+ <Skeleton>
321
+ <Avatar size="2" fallback="F" />
322
+ </Skeleton>
323
+
324
+ <Flex
325
+ direction="column"
326
+ height="var(--space-7)"
327
+ justify="center"
328
+ >
329
+ <Skeleton width="180px" height="var(--space-4)" />
330
+ <Skeleton width="90px" height="var(--space-3)" mt="1" />
331
+ </Flex>
332
+ </Flex>
333
+ </Table.RowHeaderCell>
334
+ <Table.Cell>
335
+ <Flex wrap="wrap" gap="1">
336
+ <Skeleton width="75px" height="var(--space-4)" />
337
+ </Flex>
338
+ </Table.Cell>
339
+ <Table.Cell>
340
+ <Skeleton width="120px" height="var(--space-4)" />
341
+ </Table.Cell>
342
+ <Table.Cell justify="end" />
343
+ </Table.Row>
344
+ ))}
345
+ </Table.Body>
346
+ </Table.Root>
347
+
348
+ <Flex gap="2" justify="end">
349
+ <Skeleton loading>
350
+ <SecondaryButton size="1">Previous</SecondaryButton>
351
+ </Skeleton>
352
+ <Skeleton loading>
353
+ <SecondaryButton size="1">Next</SecondaryButton>
354
+ </Skeleton>
355
+ </Flex>
356
+ </UsersManagementRoot>
357
+ );
358
+ }
359
+
360
+ export function UsersManagementError({ error }: { error: unknown }) {
361
+ React.useEffect(() => {
362
+ console.error(error);
363
+ }, [error]);
364
+
365
+ const render = (heading: string, message: React.ReactNode) => (
366
+ <UsersManagementRoot justify="center" align="center" minHeight="676px">
367
+ <Flex
368
+ p="8"
369
+ justify="center"
370
+ align="center"
371
+ direction="column"
372
+ maxWidth="520px"
373
+ >
374
+ <Heading size="5" align="center" mb="1" wrap="balance" asChild>
375
+ <h3>{heading}</h3>
376
+ </Heading>
377
+ <Text align="center" wrap="balance">
378
+ {message}
379
+ </Text>
380
+ </Flex>
381
+ </UsersManagementRoot>
382
+ );
383
+
384
+ if (error instanceof FetchError) {
385
+ return render(
386
+ "Error fetching data",
387
+ "An error occurred. You may need to configure CORS in the WorkOS Dashboard. " +
388
+ "Contact your organization admin for support.",
389
+ );
390
+ }
391
+
392
+ if (error instanceof NoAuthTokenError) {
393
+ return render(
394
+ "Error fetching data",
395
+ "Authorization error. You likely forgot to provide an authorization " +
396
+ "token to the Users Management Widget.",
397
+ );
398
+ }
399
+
400
+ if (error instanceof ApiError && error.status === 404) {
401
+ // The widgets API treats all authorization errors as 404s. If there is a
402
+ // legitimate 404, it's a bug on our end but there's currently no way to
403
+ // distinguish between the two.
404
+ return render(
405
+ "Error fetching data",
406
+ "Authorization error. Contact your organization admin for support.",
407
+ );
408
+ }
409
+
410
+ return render(
411
+ "Error fetching data",
412
+ "Unknown error. Contact your organization admin for support.",
413
+ );
414
+ }
415
+
416
+ function UsersManagementRoot({
417
+ className,
418
+ children,
419
+ ...props
420
+ }: React.ComponentProps<typeof Flex>) {
421
+ return (
422
+ <Flex
423
+ className={clsx("woswidgets-widget", className)}
424
+ data-woswidgets-widget-id="users-management"
425
+ direction="column"
426
+ gap="3"
427
+ {...props}
428
+ >
429
+ {children}
430
+ </Flex>
431
+ );
432
+ }
433
+
230
434
  function UserBadge({ user }: { user: User }) {
231
435
  // TODO: This is not yet available in the data. Update here after API is updated.
232
436
  if (user.isLoggedInUser) {
@@ -343,49 +547,6 @@ function LastActiveImpl({
343
547
  );
344
548
  }
345
549
 
346
- const SkeletonRows = ({
347
- length,
348
- alternate = false,
349
- }: {
350
- length: number;
351
- alternate?: boolean;
352
- }) => {
353
- return Array.from({ length }, (_, index) => {
354
- return (
355
- <Table.Row
356
- key={index}
357
- align="center"
358
- style={{
359
- background:
360
- alternate && index % 2 === 1 ? "var(--gray-a1)" : undefined,
361
- }}
362
- >
363
- <Table.RowHeaderCell>
364
- <Flex align="center" gap="3">
365
- <Skeleton>
366
- <Avatar size="2" fallback="F" />
367
- </Skeleton>
368
-
369
- <Flex direction="column" height="var(--space-7)" justify="center">
370
- <Skeleton width="180px" height="var(--space-4)" />
371
- <Skeleton width="90px" height="var(--space-3)" mt="1" />
372
- </Flex>
373
- </Flex>
374
- </Table.RowHeaderCell>
375
- <Table.Cell>
376
- <Flex wrap="wrap" gap="1">
377
- <Skeleton width="75px" height="var(--space-4)" />
378
- </Flex>
379
- </Table.Cell>
380
- <Table.Cell>
381
- <Skeleton width="120px" height="var(--space-4)" />
382
- </Table.Cell>
383
- <Table.Cell justify="end" />
384
- </Table.Row>
385
- );
386
- });
387
- };
388
-
389
550
  const TableCellText = React.forwardRef<HTMLSpanElement, TableCellTextProps>(
390
551
  function TableCellText(
391
552
  { children, dim, level = "primary", ...props },