@cytario/web 2.1.3 → 2.1.5

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 (202) hide show
  1. package/README.md +20 -20
  2. package/app/.server/auth/README.md +8 -8
  3. package/app/.server/auth/authMiddleware.ts +9 -25
  4. package/app/.server/auth/exchangeAuthCode.ts +2 -6
  5. package/app/.server/auth/getS3Client.ts +3 -13
  6. package/app/.server/auth/getSessionCredentials.ts +6 -20
  7. package/app/.server/auth/getUserInfo.ts +2 -6
  8. package/app/.server/auth/keycloakAdmin/client.ts +2 -9
  9. package/app/.server/auth/keycloakAdmin/groups.ts +9 -26
  10. package/app/.server/auth/keycloakAdmin/users.ts +7 -23
  11. package/app/.server/auth/oauthState.ts +4 -13
  12. package/app/.server/auth/redirectIfAuthenticated.ts +1 -3
  13. package/app/.server/auth/refreshAuthTokens.ts +5 -19
  14. package/app/.server/auth/sessionMiddleware.ts +1 -4
  15. package/app/.server/auth/sessionStorage.ts +1 -4
  16. package/app/.server/auth/verifyIdToken.ts +1 -3
  17. package/app/.server/auth/wellKnownEndpoints.ts +1 -4
  18. package/app/.server/db/redis.ts +5 -1
  19. package/app/.server/logging.ts +1 -4
  20. package/app/.server/requestDurationMiddleware.ts +1 -4
  21. package/app/components/.client/ImageViewer/README.md +5 -5
  22. package/app/components/.client/ImageViewer/components/ChannelsController/ChannelsController.tsx +7 -9
  23. package/app/components/.client/ImageViewer/components/ChannelsController/ChannelsControllerBrightfieldItem.tsx +1 -2
  24. package/app/components/.client/ImageViewer/components/ChannelsController/ChannelsControllerItem.tsx +2 -5
  25. package/app/components/.client/ImageViewer/components/ChannelsController/ChannelsControllerItemList.tsx +7 -15
  26. package/app/components/.client/ImageViewer/components/ChannelsController/ColorPicker/ColorPicker.tsx +2 -9
  27. package/app/components/.client/ImageViewer/components/ChannelsController/ColorPicker/ColorSwatch.tsx +1 -4
  28. package/app/components/.client/ImageViewer/components/ChannelsController/DomainSlider.tsx +1 -3
  29. package/app/components/.client/ImageViewer/components/ChannelsController/Histogram.tsx +16 -18
  30. package/app/components/.client/ImageViewer/components/ChannelsController/HistogramChannel.tsx +2 -8
  31. package/app/components/.client/ImageViewer/components/ChannelsController/MinMaxSettings.tsx +6 -15
  32. package/app/components/.client/ImageViewer/components/FeatureBar/FeatureBarDragHandle.tsx +1 -5
  33. package/app/components/.client/ImageViewer/components/FeatureBar/FeatureBarToggle.tsx +1 -5
  34. package/app/components/.client/ImageViewer/components/FeatureBar/FeatureItem.tsx +1 -5
  35. package/app/components/.client/ImageViewer/components/FeatureBar/Presets.tsx +3 -11
  36. package/app/components/.client/ImageViewer/components/FeatureBar/useFeatureBar.tsx +16 -25
  37. package/app/components/.client/ImageViewer/components/Image/Channels/useChannelsLayer.ts +7 -18
  38. package/app/components/.client/ImageViewer/components/Image/ImageContainer.tsx +1 -1
  39. package/app/components/.client/ImageViewer/components/Image/ImagePanel.tsx +6 -26
  40. package/app/components/.client/ImageViewer/components/Image/ImagePreview.tsx +2 -9
  41. package/app/components/.client/ImageViewer/components/Image/Overlays/AdditivePolygonLayer.tsx +1 -5
  42. package/app/components/.client/ImageViewer/components/Image/Overlays/AdditiveScatterplotLayer.tsx +1 -5
  43. package/app/components/.client/ImageViewer/components/Image/Overlays/OverlaysLayer.tsx +6 -24
  44. package/app/components/.client/ImageViewer/components/Image/Overlays/markerUniforms.ts +2 -5
  45. package/app/components/.client/ImageViewer/components/Image/Overlays/useOverlaysLayer.tsx +7 -21
  46. package/app/components/.client/ImageViewer/components/Image/useInitializeChannels.ts +1 -7
  47. package/app/components/.client/ImageViewer/components/Image/useResizeObserver.ts +1 -1
  48. package/app/components/.client/ImageViewer/components/Magnifier.tsx +5 -13
  49. package/app/components/.client/ImageViewer/components/Measurements/ActiveViewStatePreview.tsx +3 -8
  50. package/app/components/.client/ImageViewer/components/Measurements/CursorTick.tsx +2 -7
  51. package/app/components/.client/ImageViewer/components/Measurements/Ruler.tsx +1 -8
  52. package/app/components/.client/ImageViewer/components/Measurements/SlideCarrier.tsx +3 -13
  53. package/app/components/.client/ImageViewer/components/Measurements/Tick.tsx +1 -1
  54. package/app/components/.client/ImageViewer/components/Measurements/calculateViewStateToFit.ts +1 -1
  55. package/app/components/.client/ImageViewer/components/Measurements/useMeasurements.ts +9 -28
  56. package/app/components/.client/ImageViewer/components/OverlaysController/AddOverlay.tsx +4 -13
  57. package/app/components/.client/ImageViewer/components/OverlaysController/OverlayPicker.modal.tsx +1 -6
  58. package/app/components/.client/ImageViewer/components/OverlaysController/OverlaysController.Item.tsx +24 -54
  59. package/app/components/.client/ImageViewer/components/OverlaysController/OverlaysController.tsx +1 -3
  60. package/app/components/.client/ImageViewer/components/SplitViewToggle.tsx +1 -3
  61. package/app/components/.client/ImageViewer/components/ViewerHeader.tsx +1 -3
  62. package/app/components/.client/ImageViewer/state/decoders/decodeJPEG2000.d.ts +9 -11
  63. package/app/components/.client/ImageViewer/state/decoders/decodeJPEG2000.js +11 -11
  64. package/app/components/.client/ImageViewer/state/decoders/decoder.worker.js +49 -49
  65. package/app/components/.client/ImageViewer/state/decoders/genericDecoder.ts +76 -81
  66. package/app/components/.client/ImageViewer/state/decoders/jp2k-decoder.ts +9 -9
  67. package/app/components/.client/ImageViewer/state/decoders/lzwDecoder.ts +9 -9
  68. package/app/components/.client/ImageViewer/state/loaders/loadBioformatsZarrWithCredentials.ts +10 -22
  69. package/app/components/.client/ImageViewer/state/store/ViewerStoreContext.tsx +4 -18
  70. package/app/components/.client/ImageViewer/state/store/createViewerStore.ts +110 -194
  71. package/app/components/.client/ImageViewer/state/store/getInitialChannelsState.ts +2 -6
  72. package/app/components/.client/ImageViewer/state/store/selectors.ts +9 -9
  73. package/app/components/.client/ImageViewer/state/store/types.ts +3 -12
  74. package/app/components/.client/ImageViewer/state/transport/CredentialedHTTPStore.ts +1 -5
  75. package/app/components/.client/ImageViewer/state/transport/SigV4TiffClient.ts +2 -9
  76. package/app/components/.client/ImageViewer/utils/getSelectionStats.ts +1 -4
  77. package/app/components/.client/ImageViewer/utils/handleImageViewerHover.ts +1 -1
  78. package/app/components/.client/ImageViewer/utils/mapChannelConfigsToState.ts +2 -4
  79. package/app/components/.client/ImageViewer/utils/useTilesLoading.ts +3 -3
  80. package/app/components/AppHeader.tsx +1 -4
  81. package/app/components/Breadcrumbs/Breadcrumbs.tsx +4 -13
  82. package/app/components/Breadcrumbs/getCrumbs.tsx +1 -1
  83. package/app/components/ClientOnly.tsx +1 -1
  84. package/app/components/Container.tsx +3 -15
  85. package/app/components/DataGrid/ConvertOverlay.modal.tsx +1 -6
  86. package/app/components/DataGrid/DataGrid.tsx +7 -27
  87. package/app/components/DataGrid/WktSvg.tsx +2 -4
  88. package/app/components/DataGrid/getParquetSchema.ts +1 -4
  89. package/app/components/DescriptionList.tsx +1 -3
  90. package/app/components/DirectoryView/ConnectionMenu.tsx +8 -22
  91. package/app/components/DirectoryView/DirectoryView.tsx +10 -46
  92. package/app/components/DirectoryView/DirectoryViewGrid.tsx +16 -49
  93. package/app/components/DirectoryView/DirectoryViewTableConnection.tsx +1 -4
  94. package/app/components/DirectoryView/DirectoryViewTableDirectory.tsx +2 -7
  95. package/app/components/DirectoryView/DirectoryViewTree.tsx +5 -21
  96. package/app/components/DirectoryView/FilterBar.tsx +9 -48
  97. package/app/components/DirectoryView/buildDirectoryTree.ts +6 -25
  98. package/app/components/DirectoryView/filterNodes.ts +4 -11
  99. package/app/components/DirectoryView/modals/Cyberduck.modal.tsx +6 -15
  100. package/app/components/DirectoryView/modals/FileInfo.modal.tsx +1 -5
  101. package/app/components/DirectoryView/useLayoutStore.ts +5 -25
  102. package/app/components/GlobalSearch/GlobalSearch.tsx +1 -4
  103. package/app/components/GlobalSearch/SearchBar.tsx +1 -7
  104. package/app/components/GlobalSearch/Suggestions.tsx +0 -1
  105. package/app/components/ImageViewer/state/formatRegistry.ts +5 -18
  106. package/app/components/LavaLoader.tsx +4 -11
  107. package/app/components/Layout/Footer.tsx +1 -4
  108. package/app/components/Pills/ScopePill.tsx +2 -8
  109. package/app/components/Table/ColumnFilterInput.tsx +5 -20
  110. package/app/components/Table/ColumnResizeHandle.tsx +1 -5
  111. package/app/components/Table/ColumnSortButton.tsx +1 -5
  112. package/app/components/Table/SelectionFooter.tsx +3 -5
  113. package/app/components/Table/Table.tsx +5 -21
  114. package/app/components/Table/TableBodyRow.tsx +19 -31
  115. package/app/components/Table/TableHeaderRow.tsx +7 -28
  116. package/app/components/Table/TableMenu.tsx +4 -20
  117. package/app/components/Table/state/createTableStore.ts +3 -9
  118. package/app/components/Table/state/useTableStore.ts +1 -3
  119. package/app/components/Table/types.ts +4 -10
  120. package/app/components/Table/useColumnFilters.ts +1 -4
  121. package/app/components/Table/useColumnVisibility.ts +4 -11
  122. package/app/components/Table/useColumnWidths.ts +1 -3
  123. package/app/components/Table/useTableSorting.ts +4 -12
  124. package/app/components/Tooltip/Tooltip.tsx +4 -17
  125. package/app/components/Tooltip/TooltipSpan.tsx +5 -28
  126. package/app/components/Tooltip/useCopyToClipboard.ts +1 -3
  127. package/app/components/Tooltip/useMiddleEllipsis.ts +2 -7
  128. package/app/components/Tooltip/useOverflowDetection.ts +2 -5
  129. package/app/components/UserMenu.tsx +2 -9
  130. package/app/entry.server.tsx +9 -19
  131. package/app/hooks/useSearchParam.ts +2 -4
  132. package/app/lib/bootstrapPluginsCore.ts +4 -9
  133. package/app/root.tsx +4 -15
  134. package/app/routes/admin/assertAdminScope.ts +1 -3
  135. package/app/routes/admin/assertGroupPathsInScope.ts +3 -11
  136. package/app/routes/admin/assertGroupsInScope.ts +3 -11
  137. package/app/routes/admin/assertUsersInScope.ts +2 -8
  138. package/app/routes/admin/bulkInvite/bulkInvite.action.ts +2 -13
  139. package/app/routes/admin/bulkInvite/bulkInvite.form.tsx +18 -35
  140. package/app/routes/admin/createGroup/createGroup.action.ts +3 -10
  141. package/app/routes/admin/createGroup/createGroup.form.tsx +2 -9
  142. package/app/routes/admin/createGroup/createGroup.modal.tsx +1 -5
  143. package/app/routes/admin/inviteUser/inviteUser.action.ts +1 -4
  144. package/app/routes/admin/inviteUser/inviteUser.form.tsx +2 -8
  145. package/app/routes/admin/inviteUser/inviteUser.loader.ts +1 -4
  146. package/app/routes/admin/inviteUser/inviteUser.modal.tsx +3 -16
  147. package/app/routes/admin/updateUser/updateUser.form.tsx +4 -15
  148. package/app/routes/admin/updateUser/updateUser.modal.tsx +5 -23
  149. package/app/routes/admin/updateUser/userDetail.action.ts +2 -10
  150. package/app/routes/admin/users/BulkActions.tsx +15 -38
  151. package/app/routes/admin/users/bulkUsers.action.ts +2 -9
  152. package/app/routes/admin/users/bulkUsers.schema.ts +1 -6
  153. package/app/routes/admin/users/users.route.tsx +14 -63
  154. package/app/routes/api/cyberduck-profile.$name.ts +6 -2
  155. package/app/routes/auth/callback.route.tsx +8 -33
  156. package/app/routes/auth/login.route.tsx +8 -11
  157. package/app/routes/auth/logout.route.tsx +4 -14
  158. package/app/routes/config.route.tsx +1 -5
  159. package/app/routes/connections/connection.form.tsx +5 -14
  160. package/app/routes/connections/connection.schema.ts +4 -16
  161. package/app/routes/connections/connections.loader.ts +11 -23
  162. package/app/routes/connections/connections.route.tsx +1 -5
  163. package/app/routes/connections/connections.server.ts +1 -3
  164. package/app/routes/connections/createConnection.action.ts +2 -8
  165. package/app/routes/connections/createConnection.modal.tsx +3 -13
  166. package/app/routes/connections/deleteConnection.action.ts +1 -4
  167. package/app/routes/connections/updateConnection.action.ts +2 -8
  168. package/app/routes/connections/updateConnection.modal.tsx +4 -14
  169. package/app/routes/home/home.route.tsx +8 -33
  170. package/app/routes/layouts/ModalOutlet.tsx +6 -18
  171. package/app/routes/objects/objects.loader.ts +5 -19
  172. package/app/routes/objects/objects.route.tsx +11 -30
  173. package/app/routes/presign.route.tsx +5 -18
  174. package/app/routes/recent.route.tsx +1 -4
  175. package/app/routes/search.route.tsx +4 -17
  176. package/app/routes.ts +1 -4
  177. package/app/tailwind.css +17 -12
  178. package/app/types/cornerstone-codecs.d.ts +25 -29
  179. package/app/utils/connectionsStore/selectors.ts +2 -6
  180. package/app/utils/connectionsStore/useConnectionsStore.ts +2 -6
  181. package/app/utils/db/convertCsvToParquet.ts +1 -3
  182. package/app/utils/db/createDatabase.ts +1 -3
  183. package/app/utils/db/createSingleton.ts +1 -1
  184. package/app/utils/db/getBlobFromObjectNode.ts +3 -9
  185. package/app/utils/db/getGeomQuery.ts +1 -1
  186. package/app/utils/db/getMarkerInfoWasm.ts +1 -3
  187. package/app/utils/db/getTileBoundingBox.ts +1 -4
  188. package/app/utils/db/sqlQueries.ts +1 -4
  189. package/app/utils/fileType.ts +80 -10
  190. package/app/utils/filterObjects.ts +2 -5
  191. package/app/utils/localFilesStore/useFileStore.ts +7 -7
  192. package/app/utils/recentlyViewed.server.ts +1 -4
  193. package/app/utils/resourceId.ts +3 -11
  194. package/app/utils/s3Provider.ts +3 -7
  195. package/app/utils/signedFetch.ts +4 -13
  196. package/bin-src/codegen.ts +4 -1
  197. package/package.json +5 -1
  198. package/prisma/seed.ts +1 -2
  199. package/public/favicon/site.webmanifest +1 -1
  200. package/server.js +1 -4
  201. package/server.js.map +1 -1
  202. package/vite-plugins/cytario-plugins.ts +2 -8
@@ -1,11 +1,6 @@
1
1
  import { Button, Checkbox } from "@cytario/design";
2
2
  import { useEffect, useState } from "react";
3
- import {
4
- useActionData,
5
- useNavigate,
6
- useNavigation,
7
- useOutletContext,
8
- } from "react-router";
3
+ import { useActionData, useNavigate, useNavigation, useOutletContext } from "react-router";
9
4
 
10
5
  import { InviteUserForm } from "./inviteUser.form";
11
6
  import { type GroupInfo } from "~/.server/auth/keycloakAdmin";
@@ -50,21 +45,13 @@ export default function InviteModal() {
50
45
  actionData={actionData}
51
46
  />
52
47
  <footer className="flex items-center gap-3 mt-6">
53
- <Checkbox
54
- isSelected={inviteAnother}
55
- onChange={setInviteAnother}
56
- className="mr-auto"
57
- >
48
+ <Checkbox isSelected={inviteAnother} onChange={setInviteAnother} className="mr-auto">
58
49
  <span className="text-sm text-slate-600">Invite another</span>
59
50
  </Checkbox>
60
51
  <Button onPress={() => navigate(-1)} variant="secondary">
61
52
  Cancel
62
53
  </Button>
63
- <Button
64
- type="submit"
65
- form="invite-form"
66
- isDisabled={isSubmitting}
67
- >
54
+ <Button type="submit" form="invite-form" isDisabled={isSubmitting}>
68
55
  {isSubmitting ? "Inviting..." : "Send Invite"}
69
56
  </Button>
70
57
  </footer>
@@ -19,11 +19,7 @@ interface UpdateUserFormProps {
19
19
  groupPaths: Set<string>;
20
20
  }
21
21
 
22
- export const UpdateUserForm = ({
23
- user,
24
- groups,
25
- groupPaths,
26
- }: UpdateUserFormProps) => {
22
+ export const UpdateUserForm = ({ user, groups, groupPaths }: UpdateUserFormProps) => {
27
23
  const submit = useSubmit();
28
24
 
29
25
  const [memberGroupIds, setMemberGroupIds] = useState<Set<string>>(() => {
@@ -61,10 +57,7 @@ export const UpdateUserForm = ({
61
57
  }
62
58
  });
63
59
  for (const group of groups) {
64
- formData.append(
65
- `group-${group.id}`,
66
- String(memberGroupIds.has(group.id)),
67
- );
60
+ formData.append(`group-${group.id}`, String(memberGroupIds.has(group.id)));
68
61
  }
69
62
  return formData;
70
63
  };
@@ -73,9 +66,7 @@ export const UpdateUserForm = ({
73
66
  const changes: string[] = [];
74
67
 
75
68
  if (user.enabled && !data.enabled) {
76
- changes.push(
77
- `Disable account for ${user.firstName} ${user.lastName}`,
78
- );
69
+ changes.push(`Disable account for ${user.firstName} ${user.lastName}`);
79
70
  }
80
71
 
81
72
  const addedAdminGroups = groups.filter(
@@ -249,9 +240,7 @@ export const UpdateUserForm = ({
249
240
  confirmLabel="Save Changes"
250
241
  confirmVariant="primary"
251
242
  >
252
- <p className="text-sm text-slate-600">
253
- You are about to make the following changes:
254
- </p>
243
+ <p className="text-sm text-slate-600">You are about to make the following changes:</p>
255
244
  <ul className="list-disc list-inside text-sm text-slate-900 space-y-1">
256
245
  {warnings.map((w) => (
257
246
  <li key={w}>{w}</li>
@@ -1,16 +1,8 @@
1
1
  import { Button } from "@cytario/design";
2
- import {
3
- useNavigate,
4
- useNavigation,
5
- useOutletContext,
6
- useParams,
7
- } from "react-router";
2
+ import { useNavigate, useNavigation, useOutletContext, useParams } from "react-router";
8
3
 
9
4
  import { UpdateUserForm } from "./updateUser.form";
10
- import {
11
- type UserWithGroups,
12
- type GroupInfo,
13
- } from "~/.server/auth/keycloakAdmin";
5
+ import { type UserWithGroups, type GroupInfo } from "~/.server/auth/keycloakAdmin";
14
6
  import { RouteModal } from "~/components/RouteModal";
15
7
 
16
8
  export { userDetailAction as action } from "./userDetail.action";
@@ -33,23 +25,13 @@ export default function UserModal() {
33
25
  }
34
26
 
35
27
  return (
36
- <RouteModal
37
- title={`Edit User \u2014 ${match.user.firstName} ${match.user.lastName}`}
38
- >
39
- <UpdateUserForm
40
- user={match.user}
41
- groups={groups}
42
- groupPaths={match.groupPaths}
43
- />
28
+ <RouteModal title={`Edit User \u2014 ${match.user.firstName} ${match.user.lastName}`}>
29
+ <UpdateUserForm user={match.user} groups={groups} groupPaths={match.groupPaths} />
44
30
  <footer className="flex gap-3 justify-end mt-6">
45
31
  <Button onPress={() => navigate(-1)} variant="secondary">
46
32
  Cancel
47
33
  </Button>
48
- <Button
49
- type="submit"
50
- form="update-form"
51
- isDisabled={isSubmitting}
52
- >
34
+ <Button type="submit" form="update-form" isDisabled={isSubmitting}>
53
35
  {isSubmitting ? "Saving..." : "Save Changes"}
54
36
  </Button>
55
37
  </footer>
@@ -5,19 +5,11 @@ import { assertGroupsInScope } from "../assertGroupsInScope";
5
5
  import { assertUsersInScope } from "../assertUsersInScope";
6
6
  import { authContext } from "~/.server/auth/authMiddleware";
7
7
  import { getSession } from "~/.server/auth/getSession";
8
- import {
9
- addUserToGroup,
10
- removeUserFromGroup,
11
- updateUser,
12
- } from "~/.server/auth/keycloakAdmin";
8
+ import { addUserToGroup, removeUserFromGroup, updateUser } from "~/.server/auth/keycloakAdmin";
13
9
  import { sessionStorage } from "~/.server/auth/sessionStorage";
14
10
  import { updateUserSchema } from "~/routes/admin/updateUser/updateUser.schema";
15
11
 
16
- export const userDetailAction: ActionFunction = async ({
17
- request,
18
- context,
19
- params,
20
- }) => {
12
+ export const userDetailAction: ActionFunction = async ({ request, context, params }) => {
21
13
  const { user } = context.get(authContext);
22
14
  const { adminUrl, scope } = assertAdminScope(request.url, user.adminScopes);
23
15
 
@@ -3,17 +3,10 @@ import { Ban, Check, UserMinus, UserPlus } from "lucide-react";
3
3
  import { useMemo, useState } from "react";
4
4
  import { useNavigation, useSubmit } from "react-router";
5
5
 
6
- import {
7
- type GroupInfo,
8
- type UserWithGroups,
9
- } from "~/.server/auth/keycloakAdmin";
6
+ import { type GroupInfo, type UserWithGroups } from "~/.server/auth/keycloakAdmin";
10
7
  import { ConfirmDialog } from "~/components/ConfirmDialog";
11
8
 
12
- type BulkIntent =
13
- | "addToGroup"
14
- | "removeFromGroup"
15
- | "enableAccounts"
16
- | "disableAccounts";
9
+ type BulkIntent = "addToGroup" | "removeFromGroup" | "enableAccounts" | "disableAccounts";
17
10
 
18
11
  interface BulkActionsProps {
19
12
  selectedUserIds: string[];
@@ -81,12 +74,7 @@ function GroupSelector({
81
74
  );
82
75
  }
83
76
 
84
- export function BulkActions({
85
- selectedUserIds,
86
- users,
87
- groups,
88
- onSuccess,
89
- }: BulkActionsProps) {
77
+ export function BulkActions({ selectedUserIds, users, groups, onSuccess }: BulkActionsProps) {
90
78
  const submit = useSubmit();
91
79
  const { state } = useNavigation();
92
80
  const isSubmitting = state === "submitting";
@@ -96,10 +84,7 @@ export function BulkActions({
96
84
  const [selectedGroupId, setSelectedGroupId] = useState("");
97
85
 
98
86
  const allGroupOptions = useMemo(
99
- () =>
100
- groups
101
- .filter((g) => !g.isAdmin)
102
- .map((g) => ({ id: g.id, name: g.path })),
87
+ () => groups.filter((g) => !g.isAdmin).map((g) => ({ id: g.id, name: g.path })),
103
88
  [groups],
104
89
  );
105
90
 
@@ -110,19 +95,13 @@ export function BulkActions({
110
95
 
111
96
  // Remove: only groups at least one selected user is in
112
97
  const removeGroupOptions = useMemo(
113
- () =>
114
- allGroupOptions.filter((o) =>
115
- selectedUsers.some((u) => u.groupPaths.has(o.name)),
116
- ),
98
+ () => allGroupOptions.filter((o) => selectedUsers.some((u) => u.groupPaths.has(o.name))),
117
99
  [allGroupOptions, selectedUsers],
118
100
  );
119
101
 
120
102
  // Add: only groups where at least one selected user is NOT yet a member
121
103
  const addGroupOptions = useMemo(
122
- () =>
123
- allGroupOptions.filter((o) =>
124
- selectedUsers.some((u) => !u.groupPaths.has(o.name)),
125
- ),
104
+ () => allGroupOptions.filter((o) => selectedUsers.some((u) => !u.groupPaths.has(o.name))),
126
105
  [allGroupOptions, selectedUsers],
127
106
  );
128
107
 
@@ -145,10 +124,7 @@ export function BulkActions({
145
124
  const formData = new FormData();
146
125
  formData.set("intent", intent);
147
126
  formData.set("userIds", selectedUserIds.join(","));
148
- if (
149
- selectedGroupId &&
150
- (intent === "addToGroup" || intent === "removeFromGroup")
151
- ) {
127
+ if (selectedGroupId && (intent === "addToGroup" || intent === "removeFromGroup")) {
152
128
  formData.set("groupId", selectedGroupId);
153
129
  }
154
130
 
@@ -215,15 +191,16 @@ export function BulkActions({
215
191
  confirmVariant={config.confirmVariant}
216
192
  >
217
193
  <p className="text-sm text-slate-600">
218
- This will affect{" "}
219
- <span className="font-medium text-slate-900">{count}</span> user
194
+ This will affect <span className="font-medium text-slate-900">{count}</span> user
220
195
  {count !== 1 ? "s" : ""}.
221
196
  </p>
222
- {config.needsGroup && <GroupSelector
223
- options={getGroupOptions(intent)}
224
- value={selectedGroupId}
225
- onChange={setSelectedGroupId}
226
- />}
197
+ {config.needsGroup && (
198
+ <GroupSelector
199
+ options={getGroupOptions(intent)}
200
+ value={selectedGroupId}
201
+ onChange={setSelectedGroupId}
202
+ />
203
+ )}
227
204
  </ConfirmDialog>
228
205
  )}
229
206
  </>
@@ -6,11 +6,7 @@ import { assertGroupsInScope } from "../assertGroupsInScope";
6
6
  import { assertUsersInScope } from "../assertUsersInScope";
7
7
  import { authContext } from "~/.server/auth/authMiddleware";
8
8
  import { getSession } from "~/.server/auth/getSession";
9
- import {
10
- addUserToGroup,
11
- removeUserFromGroup,
12
- setUserEnabled,
13
- } from "~/.server/auth/keycloakAdmin";
9
+ import { addUserToGroup, removeUserFromGroup, setUserEnabled } from "~/.server/auth/keycloakAdmin";
14
10
  import { sessionStorage } from "~/.server/auth/sessionStorage";
15
11
 
16
12
  const actionLabels = {
@@ -20,10 +16,7 @@ const actionLabels = {
20
16
  disableAccounts: "disabled",
21
17
  } as const;
22
18
 
23
- export const bulkUsersAction: ActionFunction = async ({
24
- request,
25
- context,
26
- }) => {
19
+ export const bulkUsersAction: ActionFunction = async ({ request, context }) => {
27
20
  const { user } = context.get(authContext);
28
21
  const { adminUrl, scope } = assertAdminScope(request.url, user.adminScopes);
29
22
 
@@ -2,12 +2,7 @@ import { z } from "zod";
2
2
 
3
3
  export const bulkActionSchema = z
4
4
  .object({
5
- intent: z.enum([
6
- "addToGroup",
7
- "removeFromGroup",
8
- "enableAccounts",
9
- "disableAccounts",
10
- ]),
5
+ intent: z.enum(["addToGroup", "removeFromGroup", "enableAccounts", "disableAccounts"]),
11
6
  userIds: z
12
7
  .string()
13
8
  .min(1, "At least one user is required")
@@ -1,11 +1,4 @@
1
- import {
2
- Badge,
3
- Banner,
4
- Button,
5
- ButtonLink,
6
- EmptyState,
7
- Pill,
8
- } from "@cytario/design";
1
+ import { Badge, Banner, Button, ButtonLink, EmptyState, Pill } from "@cytario/design";
9
2
  import { type RowSelectionState } from "@tanstack/react-table";
10
3
  import { FolderPlus, Plug, UserPlus, Users, UsersRound } from "lucide-react";
11
4
  import { useMemo, useState } from "react";
@@ -19,19 +12,12 @@ import {
19
12
 
20
13
  import { BulkActions } from "./BulkActions";
21
14
  import type { ConnectionConfig } from "~/.generated/client";
22
- import {
23
- type UserWithGroups,
24
- type GroupInfo,
25
- } from "~/.server/auth/keycloakAdmin";
15
+ import { type UserWithGroups, type GroupInfo } from "~/.server/auth/keycloakAdmin";
26
16
  import { Container, Section, SectionHeader } from "~/components/Container";
27
17
  import { ProviderPill } from "~/components/Pills/ProviderPill";
28
18
  import { ScopePill } from "~/components/Pills/ScopePill";
29
19
  import { SelectionFooter } from "~/components/Table/SelectionFooter";
30
- import {
31
- type CellRenderers,
32
- type ColumnConfig,
33
- Table,
34
- } from "~/components/Table/Table";
20
+ import { type CellRenderers, type ColumnConfig, Table } from "~/components/Table/Table";
35
21
  import { useModal } from "~/hooks/useModal";
36
22
 
37
23
  export const meta: MetaFunction = () => [{ title: "Admin — Users" }];
@@ -46,9 +32,7 @@ export const shouldRevalidate: ShouldRevalidateFunction = ({
46
32
  defaultShouldRevalidate,
47
33
  }) => {
48
34
  if (formAction) return defaultShouldRevalidate;
49
- return (
50
- currentUrl.searchParams.get("scope") !== nextUrl.searchParams.get("scope")
51
- );
35
+ return currentUrl.searchParams.get("scope") !== nextUrl.searchParams.get("scope");
52
36
  };
53
37
 
54
38
  interface UserRow {
@@ -65,10 +49,7 @@ function buildGroupColumn(
65
49
  allGroups: GroupInfo[],
66
50
  counts: Map<string, number>,
67
51
  totalCount: number,
68
- {
69
- pillVisibleCount,
70
- ...extra
71
- }: Partial<ColumnConfig> & { pillVisibleCount?: number } = {},
52
+ { pillVisibleCount, ...extra }: Partial<ColumnConfig> & { pillVisibleCount?: number } = {},
72
53
  ): ColumnConfig {
73
54
  const options = [
74
55
  { label: "All", value: "" },
@@ -159,25 +140,11 @@ function buildColumns(
159
140
  ],
160
141
  filterRender: (option) => {
161
142
  const label =
162
- option.value === "true"
163
- ? "Active"
164
- : option.value === "false"
165
- ? "Disabled"
166
- : option.label;
167
- return (
168
- <Pill color={option.value === "true" ? "green" : "slate"}>
169
- {label}
170
- </Pill>
171
- );
143
+ option.value === "true" ? "Active" : option.value === "false" ? "Disabled" : option.label;
144
+ return <Pill color={option.value === "true" ? "green" : "slate"}>{label}</Pill>;
172
145
  },
173
146
  },
174
- buildGroupColumn(
175
- "groups",
176
- "Groups",
177
- groups,
178
- groupCounts,
179
- totalCount,
180
- ),
147
+ buildGroupColumn("groups", "Groups", groups, groupCounts, totalCount),
181
148
  ];
182
149
  }
183
150
 
@@ -193,9 +160,7 @@ function buildCellRenderers(scope: string): CellRenderers<UserRow> {
193
160
  ),
194
161
  enabled: (row) => {
195
162
  const label = row.enabled === "true" ? "Active" : "Disabled";
196
- return (
197
- <Pill color={row.enabled === "true" ? "green" : "slate"}>{label}</Pill>
198
- );
163
+ return <Pill color={row.enabled === "true" ? "green" : "slate"}>{label}</Pill>;
199
164
  },
200
165
  groups: (row) => (
201
166
  <div className="flex flex-wrap gap-1">
@@ -279,21 +244,14 @@ export default function AdminUsersRoute() {
279
244
  >
280
245
  Bulk Invite
281
246
  </ButtonLink>
282
- <Button
283
- variant="secondary"
284
- iconLeft={Plug}
285
- onPress={() => openModal("add-connection")}
286
- >
247
+ <Button variant="secondary" iconLeft={Plug} onPress={() => openModal("add-connection")}>
287
248
  Connect Storage
288
249
  </Button>
289
250
  </SectionHeader>
290
251
 
291
252
  <Container>
292
253
  <section aria-labelledby="connections-heading" className="mb-6">
293
- <h3
294
- id="connections-heading"
295
- className="text-sm font-medium text-slate-500 mb-2"
296
- >
254
+ <h3 id="connections-heading" className="text-sm font-medium text-slate-500 mb-2">
297
255
  Connections
298
256
  </h3>
299
257
  {connections.length > 0 ? (
@@ -314,12 +272,8 @@ export default function AdminUsersRoute() {
314
272
  ))}
315
273
  </ul>
316
274
  ) : (
317
- <Banner
318
- variant="warning"
319
- title="No connections linked to this group"
320
- >
321
- Members won&apos;t be able to access any data until you connect
322
- storage.
275
+ <Banner variant="warning" title="No connections linked to this group">
276
+ Members won&apos;t be able to access any data until you connect storage.
323
277
  </Banner>
324
278
  )}
325
279
  </section>
@@ -342,10 +296,7 @@ export default function AdminUsersRoute() {
342
296
  title="No users yet"
343
297
  description="Invite team members to get started."
344
298
  action={
345
- <ButtonLink
346
- href={`/admin/users/invite?scope=${encodeURIComponent(scope)}`}
347
- size="lg"
348
- >
299
+ <ButtonLink href={`/admin/users/invite?scope=${encodeURIComponent(scope)}`} size="lg">
349
300
  Invite User
350
301
  </ButtonLink>
351
302
  }
@@ -161,9 +161,13 @@ ${scopesXml}
161
161
  <key>STS Endpoint</key>
162
162
  <string>${escapeXml(stsEndpoint)}</string>
163
163
  <key>Properties</key>
164
- <dict>${roleArn ? `
164
+ <dict>${
165
+ roleArn
166
+ ? `
165
167
  <key>s3.assumerole.rolearn</key>
166
- <string>${escapeXml(roleArn)}</string>` : ""}
168
+ <string>${escapeXml(roleArn)}</string>`
169
+ : ""
170
+ }
167
171
  ${s3PropertiesXml} </dict>
168
172
  </dict>
169
173
  </plist>`;
@@ -48,10 +48,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
48
48
  // Validate required parameters
49
49
  if (!code || !state) {
50
50
  console.error(`${label} Missing code or state parameter`);
51
- return failWithNotification(
52
- request,
53
- "Authentication failed. Missing required parameters.",
54
- );
51
+ return failWithNotification(request, "Authentication failed. Missing required parameters.");
55
52
  }
56
53
 
57
54
  try {
@@ -59,19 +56,13 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
59
56
  const stateData = await validateOAuthState(state);
60
57
  if (!stateData) {
61
58
  console.error(`${label} Invalid or expired state parameter`);
62
- return failWithNotification(
63
- request,
64
- "Authentication session expired. Please try again.",
65
- );
59
+ return failWithNotification(request, "Authentication session expired. Please try again.");
66
60
  }
67
61
 
68
62
  // Guard for in-flight states from before PKCE deployment
69
63
  if (!stateData.codeVerifier || !stateData.nonce) {
70
64
  console.error(`${label} State missing codeVerifier or nonce`);
71
- return failWithNotification(
72
- request,
73
- "Authentication session invalid. Please try again.",
74
- );
65
+ return failWithNotification(request, "Authentication session invalid. Please try again.");
75
66
  }
76
67
 
77
68
  console.info(`${label} State validated successfully`);
@@ -81,28 +72,18 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
81
72
 
82
73
  // Exchange authorization code for tokens with PKCE verifier
83
74
  console.info(`${label} Exchanging authorization code for tokens`);
84
- const tokens = await exchangeAuthCode(
85
- code,
86
- redirectUri,
87
- stateData.codeVerifier,
88
- );
75
+ const tokens = await exchangeAuthCode(code, redirectUri, stateData.codeVerifier);
89
76
 
90
77
  // Verify ID token signature via JWKS and validate nonce from verified payload
91
78
  const idTokenPayload = await verifyIdToken(tokens.id_token);
92
79
  if (!idTokenPayload) {
93
80
  console.error(`${label} ID token signature verification failed`);
94
- return failWithNotification(
95
- request,
96
- "Authentication failed. Please try again.",
97
- );
81
+ return failWithNotification(request, "Authentication failed. Please try again.");
98
82
  }
99
83
 
100
84
  if (idTokenPayload.nonce !== stateData.nonce) {
101
85
  console.error(`${label} Nonce mismatch in ID token`);
102
- return failWithNotification(
103
- request,
104
- "Authentication failed. Please try again.",
105
- );
86
+ return failWithNotification(request, "Authentication failed. Please try again.");
106
87
  }
107
88
 
108
89
  // Get user info using the access token
@@ -126,20 +107,14 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
126
107
 
127
108
  // Redirect to the originally requested page or default to home
128
109
  const redirectTo = validateRedirectTo(stateData.redirectTo);
129
- console.info(
130
- `${label} Authentication successful, redirecting to:`,
131
- redirectTo,
132
- );
110
+ console.info(`${label} Authentication successful, redirecting to:`, redirectTo);
133
111
 
134
112
  return redirect(redirectTo, {
135
113
  headers: { "Set-Cookie": await sessionStorage.commitSession(session) },
136
114
  });
137
115
  } catch (error) {
138
116
  console.error(`${label} Authentication failed:`, error);
139
- return failWithNotification(
140
- request,
141
- "Authentication failed. Please try again.",
142
- );
117
+ return failWithNotification(request, "Authentication failed. Please try again.");
143
118
  }
144
119
  };
145
120
 
@@ -1,10 +1,7 @@
1
1
  import { LoaderFunctionArgs, redirect, MetaFunction } from "react-router";
2
2
 
3
3
  import { getSession } from "~/.server/auth/getSession";
4
- import {
5
- generateOAuthState,
6
- validateRedirectTo,
7
- } from "~/.server/auth/oauthState";
4
+ import { generateOAuthState, validateRedirectTo } from "~/.server/auth/oauthState";
8
5
  import { getWellKnownEndpoints } from "~/.server/auth/wellKnownEndpoints";
9
6
  import { createLabel } from "~/.server/logging";
10
7
  import { cytarioConfig } from "~/config";
@@ -14,8 +11,7 @@ export const meta: MetaFunction = () => {
14
11
  { title: "Cytario | Login" },
15
12
  {
16
13
  name: "description",
17
- content:
18
- "Log in to Cytario to access your secure workspace and manage your data.",
14
+ content: "Log in to Cytario to access your secure workspace and manage your data.",
19
15
  },
20
16
  ];
21
17
  };
@@ -75,10 +71,9 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
75
71
  return redirect(authUrl.toString());
76
72
  } catch (error) {
77
73
  console.error(`${label} Failed to initiate login:`, error);
78
- throw new Response(
79
- "Unable to connect to authentication service. Please try again later.",
80
- { status: 502 },
81
- );
74
+ throw new Response("Unable to connect to authentication service. Please try again later.", {
75
+ status: 502,
76
+ });
82
77
  }
83
78
  };
84
79
 
@@ -86,7 +81,9 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
86
81
  export default function LoginRoute() {
87
82
  return (
88
83
  <div className="flex items-center justify-center h-screen">
89
- <p role="status" className="text-slate-500">Redirecting to login...</p>
84
+ <p role="status" className="text-slate-500">
85
+ Redirecting to login...
86
+ </p>
90
87
  </div>
91
88
  );
92
89
  }
@@ -31,18 +31,14 @@ const revokeRefreshToken = async (
31
31
  });
32
32
 
33
33
  if (!response.ok) {
34
- console.warn(
35
- `${label} Token revocation returned ${response.status}`,
36
- );
34
+ console.warn(`${label} Token revocation returned ${response.status}`);
37
35
  }
38
36
  } catch (error) {
39
37
  console.warn(`${label} Token revocation failed:`, error);
40
38
  }
41
39
  };
42
40
 
43
- export const loader = async ({
44
- request,
45
- }: LoaderFunctionArgs): Promise<Response> => {
41
+ export const loader = async ({ request }: LoaderFunctionArgs): Promise<Response> => {
46
42
  const session = await getSession(request);
47
43
  const { authTokens } = await getSessionData(session);
48
44
 
@@ -52,18 +48,12 @@ export const loader = async ({
52
48
  // Revoke refresh token before ending session (best-effort)
53
49
  if (authTokens?.refreshToken) {
54
50
  console.info(`${label} Revoking refresh token`);
55
- await revokeRefreshToken(
56
- authTokens.refreshToken,
57
- wellKnownEndpoints.revocation_endpoint,
58
- );
51
+ await revokeRefreshToken(authTokens.refreshToken, wellKnownEndpoints.revocation_endpoint);
59
52
  }
60
53
 
61
54
  // Build the logout URL with post_logout_redirect_uri
62
55
  const logoutUrl = new URL(wellKnownEndpoints.end_session_endpoint);
63
- logoutUrl.searchParams.set(
64
- "post_logout_redirect_uri",
65
- `${cytarioConfig.endpoints.webapp}/login`,
66
- );
56
+ logoutUrl.searchParams.set("post_logout_redirect_uri", `${cytarioConfig.endpoints.webapp}/login`);
67
57
 
68
58
  // Include id_token_hint for better logout behavior
69
59
  if (authTokens?.idToken) {
@@ -23,11 +23,7 @@ function PreferencesSection() {
23
23
  return (
24
24
  <div className="flex flex-col gap-3">
25
25
  <H2>Preferences</H2>
26
- <Switch
27
- isSelected={showHiddenFiles}
28
- onChange={toggleShowHiddenFiles}
29
- className="text-sm"
30
- >
26
+ <Switch isSelected={showHiddenFiles} onChange={toggleShowHiddenFiles} className="text-sm">
31
27
  Show hidden files
32
28
  </Switch>
33
29
  <p className="text-xs text-(--color-text-secondary)">