@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
@@ -49,8 +49,7 @@ const FIELD_TO_STEP: Record<string, number> = {
49
49
  };
50
50
 
51
51
  const dtClass = "text-[var(--color-text-secondary)]";
52
- const ddClass =
53
- "font-[number:var(--font-weight-medium)] text-[var(--color-text-primary)]";
52
+ const ddClass = "font-[number:var(--font-weight-medium)] text-[var(--color-text-primary)]";
54
53
 
55
54
  function SummaryRow({ label, value }: { label: string; value: string }) {
56
55
  return (
@@ -83,8 +82,7 @@ export const ConnectionForm = ({
83
82
  }>();
84
83
  const navigation = useNavigation();
85
84
 
86
- const serverErrors =
87
- actionData?.status === "error" ? actionData.errors : undefined;
85
+ const serverErrors = actionData?.status === "error" ? actionData.errors : undefined;
88
86
  const isSubmitting = navigation.state === "submitting";
89
87
 
90
88
  // Compute the initial step from server errors so we navigate to the
@@ -151,9 +149,7 @@ export const ConnectionForm = ({
151
149
 
152
150
  const fieldsPerStep: Record<number, (keyof ConnectBucketFormData)[]> = {
153
151
  0: ["providerType", "s3Uri", "name"],
154
- 1: isAWS
155
- ? ["ownerScope", "roleArn", "bucketRegion"]
156
- : ["ownerScope", "bucketEndpoint"],
152
+ 1: isAWS ? ["ownerScope", "roleArn", "bucketRegion"] : ["ownerScope", "bucketEndpoint"],
157
153
  };
158
154
 
159
155
  const onSubmit = (data: ConnectBucketFormData) => {
@@ -300,9 +296,7 @@ export const ConnectionForm = ({
300
296
  ]}
301
297
  selectedKey={field.value}
302
298
  onSelectionChange={(key) => field.onChange(key)}
303
- renderItem={(item) => (
304
- <ScopePill scope={item.id} />
305
- )}
299
+ renderItem={(item) => <ScopePill scope={item.id} />}
306
300
  />
307
301
  )}
308
302
  />
@@ -389,10 +383,7 @@ export const ConnectionForm = ({
389
383
  ].join(" ")}
390
384
  >
391
385
  <dl className="flex flex-col gap-[var(--spacing-2)] text-[length:var(--font-size-sm)]">
392
- <SummaryRow
393
- label="Provider"
394
- value={isAWS ? "AWS S3" : "MinIO"}
395
- />
386
+ <SummaryRow label="Provider" value={isAWS ? "AWS S3" : "MinIO"} />
396
387
  <SummaryRow label="Name" value={nameValue} />
397
388
  <SummaryRow label="Bucket" value={bucketName} />
398
389
  {prefix && <SummaryRow label="Prefix" value={prefix} />}
@@ -10,14 +10,8 @@ export const connectionNameSchema = z
10
10
  /^[a-zA-Z0-9][a-zA-Z0-9 -]*[a-zA-Z0-9]$/,
11
11
  "Name must be alphanumeric with hyphens or spaces, no leading/trailing hyphens or spaces",
12
12
  )
13
- .refine(
14
- (val) => !val.includes("--"),
15
- "Name must not contain consecutive hyphens",
16
- )
17
- .refine(
18
- (val) => !val.includes(" "),
19
- "Name must not contain consecutive spaces",
20
- );
13
+ .refine((val) => !val.includes("--"), "Name must not contain consecutive hyphens")
14
+ .refine((val) => !val.includes(" "), "Name must not contain consecutive spaces");
21
15
 
22
16
  // AWS ARN pattern validation
23
17
  const arnPattern = /^arn:aws:iam::\d{12}:role\/[\w+=,.@-]+$/;
@@ -39,11 +33,7 @@ const s3UriSchema = z
39
33
  /** Auto-suggest a connection name from an S3 URI (e.g. "s3://my-bucket/path" → "my-bucket"). */
40
34
  export function suggestName(s3Uri: string): string {
41
35
  const { bucketName, prefix } = parseS3Uri(s3Uri);
42
- const lastSegment = prefix
43
- .replace(/\/$/, "")
44
- .split("/")
45
- .filter(Boolean)
46
- .pop();
36
+ const lastSegment = prefix.replace(/\/$/, "").split("/").filter(Boolean).pop();
47
37
  const base = lastSegment ? `${bucketName} ${lastSegment}` : bucketName;
48
38
  return base
49
39
  .replace(/[^a-zA-Z0-9 -]/g, " ")
@@ -54,9 +44,7 @@ export function suggestName(s3Uri: string): string {
54
44
  }
55
45
 
56
46
  // Helper to parse S3 URI into bucket name and prefix
57
- export const parseS3Uri = (
58
- uri: string,
59
- ): { bucketName: string; prefix: string } => {
47
+ export const parseS3Uri = (uri: string): { bucketName: string; prefix: string } => {
60
48
  const cleaned = uri.replace(/^s3:\/\//, "");
61
49
  const [bucketName, ...prefixParts] = cleaned.split("/");
62
50
  const prefix = prefixParts.join("/");
@@ -21,13 +21,7 @@ const fetchPreviewObject = async (
21
21
  const creds = credentials[config.name];
22
22
  if (!creds) return undefined;
23
23
  const s3 = await getS3Client(config, creds, userId);
24
- const objects = await getObjects(
25
- config,
26
- s3,
27
- null,
28
- config.prefix || undefined,
29
- 100,
30
- );
24
+ const objects = await getObjects(config, s3, null, config.prefix || undefined, 100);
31
25
  return objects.find((obj) => isImageFile(obj.Key ?? ""));
32
26
  };
33
27
 
@@ -57,17 +51,13 @@ export interface LoaderData {
57
51
  pinnedPaths: SerializedPinnedPath[];
58
52
  }
59
53
 
60
- export async function loadConnections({
61
- context,
62
- }: LoaderFunctionArgs) {
54
+ export async function loadConnections({ context }: LoaderFunctionArgs) {
63
55
  const { connectionConfigs, credentials, user } = context.get(authContext);
64
56
  const userId = user.sub;
65
57
 
66
58
  const [previews, recentlyViewedRaw, pinnedPathsRaw] = await Promise.all([
67
59
  Promise.allSettled(
68
- connectionConfigs.map((config) =>
69
- fetchPreviewObject(config, credentials, userId),
70
- ),
60
+ connectionConfigs.map((config) => fetchPreviewObject(config, credentials, userId)),
71
61
  ),
72
62
  getRecentlyViewed(userId, 20),
73
63
  getPinnedPaths(userId),
@@ -88,16 +78,14 @@ export async function loadConnections({
88
78
  };
89
79
  });
90
80
 
91
- const recentlyViewed: SerializedRecentlyViewed[] = recentlyViewedRaw.map(
92
- (item) => ({
93
- id: item.id,
94
- connectionName: item.connectionName,
95
- pathName: item.pathName,
96
- name: item.name,
97
- type: item.type,
98
- viewedAt: item.viewedAt.toISOString(),
99
- }),
100
- );
81
+ const recentlyViewed: SerializedRecentlyViewed[] = recentlyViewedRaw.map((item) => ({
82
+ id: item.id,
83
+ connectionName: item.connectionName,
84
+ pathName: item.pathName,
85
+ name: item.name,
86
+ type: item.type,
87
+ viewedAt: item.viewedAt.toISOString(),
88
+ }));
101
89
 
102
90
  const pinnedPaths: SerializedPinnedPath[] = pinnedPathsRaw.map((pin) => ({
103
91
  id: pin.id,
@@ -63,11 +63,7 @@ export default function ConnectionsListRoute() {
63
63
  title="No storage connections"
64
64
  description="Add a storage connection to view your cloud storage."
65
65
  action={
66
- <Button
67
- size="lg"
68
- variant="neutral"
69
- onPress={() => openModal("add-connection")}
70
- >
66
+ <Button size="lg" variant="neutral" onPress={() => openModal("add-connection")}>
71
67
  Connect Storage
72
68
  </Button>
73
69
  }
@@ -4,9 +4,7 @@ import { prisma } from "~/.server/db/prisma";
4
4
  import { canSee, filterVisible } from "~/utils/authorization";
5
5
 
6
6
  /** List all connection configs visible to the user. */
7
- export async function listConnections(
8
- user: UserProfile,
9
- ): Promise<ConnectionConfig[]> {
7
+ export async function listConnections(user: UserProfile): Promise<ConnectionConfig[]> {
10
8
  const allConfigs = await prisma.connectionConfig.findMany();
11
9
  return filterVisible(user, allConfigs);
12
10
  }
@@ -42,10 +42,7 @@ export async function createConnection(
42
42
  });
43
43
  }
44
44
 
45
- export const createAction = async ({
46
- request,
47
- context,
48
- }: ActionFunctionArgs) => {
45
+ export const createAction = async ({ request, context }: ActionFunctionArgs) => {
49
46
  const { user } = context.get(authContext);
50
47
 
51
48
  const formData = await request.formData();
@@ -108,10 +105,7 @@ export const createAction = async ({
108
105
  headers: { "Set-Cookie": await sessionStorage.commitSession(session) },
109
106
  });
110
107
  } catch (error) {
111
- if (
112
- error instanceof Prisma.PrismaClientKnownRequestError &&
113
- error.code === "P2002"
114
- ) {
108
+ if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
115
109
  return {
116
110
  errors: {
117
111
  name: ["This name is already taken. Please choose another."],
@@ -21,24 +21,14 @@ export function resolveDefaultScope(
21
21
  );
22
22
  }
23
23
 
24
- export default function CreateConnectionModal({
25
- onClose,
26
- }: {
27
- onClose: () => void;
28
- }) {
29
- const rootData = useRouteLoaderData("root") as
30
- | { user?: UserProfile }
31
- | undefined;
24
+ export default function CreateConnectionModal({ onClose }: { onClose: () => void }) {
25
+ const rootData = useRouteLoaderData("root") as { user?: UserProfile } | undefined;
32
26
  const user = rootData?.user;
33
27
  const [searchParams] = useSearchParams();
34
28
 
35
29
  if (!user) return null;
36
30
 
37
- const defaultScope = resolveDefaultScope(
38
- searchParams.get("scope"),
39
- user.adminScopes,
40
- user.sub,
41
- );
31
+ const defaultScope = resolveDefaultScope(searchParams.get("scope"), user.adminScopes, user.sub);
42
32
 
43
33
  return (
44
34
  <RouteModal title="Connect Storage" onClose={onClose}>
@@ -24,10 +24,7 @@ export async function deleteConnection(user: UserProfile, name: string) {
24
24
  await prisma.connectionConfig.delete({ where: { id: config.id } });
25
25
  }
26
26
 
27
- export const deleteAction = async ({
28
- request,
29
- context,
30
- }: ActionFunctionArgs) => {
27
+ export const deleteAction = async ({ request, context }: ActionFunctionArgs) => {
31
28
  const { user } = context.get(authContext);
32
29
  const session = context.get(sessionContext);
33
30
  const formData = await request.formData();
@@ -65,10 +65,7 @@ export async function updateConnection(
65
65
  return { ...updated, previousName, previousBucketName };
66
66
  }
67
67
 
68
- export const updateAction = async ({
69
- request,
70
- context,
71
- }: ActionFunctionArgs) => {
68
+ export const updateAction = async ({ request, context }: ActionFunctionArgs) => {
72
69
  const { user } = context.get(authContext);
73
70
  const session = context.get(sessionContext);
74
71
  const formData = await request.formData();
@@ -141,10 +138,7 @@ export const updateAction = async ({
141
138
  headers: { "Set-Cookie": await sessionStorage.commitSession(session) },
142
139
  });
143
140
  } catch (error) {
144
- if (
145
- error instanceof Prisma.PrismaClientKnownRequestError &&
146
- error.code === "P2002"
147
- ) {
141
+ if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
148
142
  return {
149
143
  errors: {
150
144
  name: [
@@ -17,15 +17,12 @@ export default function UpdateConnectionModal({
17
17
  nodeName ? state.connections[nodeName]?.connectionConfig : undefined,
18
18
  );
19
19
 
20
- const rootData = useRouteLoaderData("root") as
21
- | { user?: UserProfile }
22
- | undefined;
20
+ const rootData = useRouteLoaderData("root") as { user?: UserProfile } | undefined;
23
21
  const user = rootData?.user;
24
22
 
25
23
  if (!user || !nodeName || !connectionConfig) return null;
26
24
 
27
- const { bucketName, prefix, provider, ownerScope, roleArn, region, endpoint } =
28
- connectionConfig;
25
+ const { bucketName, prefix, provider, ownerScope, roleArn, region, endpoint } = connectionConfig;
29
26
 
30
27
  const s3Uri = prefix ? `${bucketName}/${prefix}` : bucketName;
31
28
 
@@ -41,15 +38,8 @@ export default function UpdateConnectionModal({
41
38
  };
42
39
 
43
40
  return (
44
- <RouteModal
45
- title="Edit Connection"
46
- onClose={() => onClose(["nodeName"])}
47
- >
48
- <ConnectionForm
49
- adminScopes={user.adminScopes}
50
- userId={user.sub}
51
- initialData={initialData}
52
- />
41
+ <RouteModal title="Edit Connection" onClose={() => onClose(["nodeName"])}>
42
+ <ConnectionForm adminScopes={user.adminScopes} userId={user.sub} initialData={initialData} />
53
43
  </RouteModal>
54
44
  );
55
45
  }
@@ -1,11 +1,7 @@
1
1
  import { Button, EmptyState } from "@cytario/design";
2
2
  import { FileSearch } from "lucide-react";
3
3
  import { useMemo } from "react";
4
- import {
5
- type MetaFunction,
6
- type ShouldRevalidateFunction,
7
- useLoaderData,
8
- } from "react-router";
4
+ import { type MetaFunction, type ShouldRevalidateFunction, useLoaderData } from "react-router";
9
5
 
10
6
  import { Section } from "~/components/Container";
11
7
  import { DashboardSection } from "~/components/DashboardSection";
@@ -18,7 +14,6 @@ import {
18
14
  } from "~/routes/connections/connections.loader";
19
15
  import { isImageFile } from "~/utils/fileType";
20
16
 
21
-
22
17
  const title = "Storage Connections";
23
18
  const MAX_RECENT_IMAGES = 4;
24
19
  const MAX_PINNED = 10;
@@ -27,10 +22,7 @@ const MAX_RECENT_FILES = 6;
27
22
  const MAX_CONNECTIONS = 100;
28
23
 
29
24
  export const meta: MetaFunction = () => {
30
- return [
31
- { title },
32
- { name: "description", content: "Manage your storage connections" },
33
- ];
25
+ return [{ title }, { name: "description", content: "Manage your storage connections" }];
34
26
  };
35
27
 
36
28
  export const shouldRevalidate: ShouldRevalidateFunction = ({
@@ -48,8 +40,7 @@ export const shouldRevalidate: ShouldRevalidateFunction = ({
48
40
  export { loadConnections as loader } from "~/routes/connections/connections.loader";
49
41
 
50
42
  export default function HomeRoute() {
51
- const { nodes, connectionConfigs, recentlyViewed, pinnedPaths } =
52
- useLoaderData<LoaderData>();
43
+ const { nodes, connectionConfigs, recentlyViewed, pinnedPaths } = useLoaderData<LoaderData>();
53
44
 
54
45
  const { openModal } = useModal();
55
46
 
@@ -62,9 +53,7 @@ export default function HomeRoute() {
62
53
  const allRecentItems: TreeNode[] = useMemo(
63
54
  () =>
64
55
  recentlyViewed
65
- .filter((item: SerializedRecentlyViewed) =>
66
- configByName.has(item.connectionName),
67
- )
56
+ .filter((item: SerializedRecentlyViewed) => configByName.has(item.connectionName))
68
57
  .map((item: SerializedRecentlyViewed) => {
69
58
  return {
70
59
  id: `${item.connectionName}/${item.pathName}`,
@@ -93,9 +82,7 @@ export default function HomeRoute() {
93
82
  const pinnedNodes: TreeNode[] = useMemo(
94
83
  () =>
95
84
  pinnedPaths
96
- .filter((pin: SerializedPinnedPath) =>
97
- configByName.has(pin.connectionName),
98
- )
85
+ .filter((pin: SerializedPinnedPath) => configByName.has(pin.connectionName))
99
86
  .map((pin: SerializedPinnedPath) => {
100
87
  return {
101
88
  id: `${pin.connectionName}/${pin.pathName}`,
@@ -108,9 +95,7 @@ export default function HomeRoute() {
108
95
  pin.totalSize != null || pin.lastModified != null
109
96
  ? ({
110
97
  Size: pin.totalSize ?? undefined,
111
- LastModified: pin.lastModified
112
- ? new Date(pin.lastModified)
113
- : undefined,
98
+ LastModified: pin.lastModified ? new Date(pin.lastModified) : undefined,
114
99
  } as TreeNode["_Object"])
115
100
  : undefined,
116
101
  };
@@ -128,12 +113,7 @@ export default function HomeRoute() {
128
113
  showAllHref="/recent"
129
114
  />
130
115
 
131
- <DashboardSection
132
- title="Pinned"
133
- nodes={pinnedNodes}
134
- viewMode="list"
135
- maxItems={MAX_PINNED}
136
- />
116
+ <DashboardSection title="Pinned" nodes={pinnedNodes} viewMode="list" maxItems={MAX_PINNED} />
137
117
 
138
118
  <DashboardSection
139
119
  title="Recently Browsed"
@@ -166,18 +146,13 @@ export default function HomeRoute() {
166
146
  title="Start exploring your data"
167
147
  description="Add a storage connection to view your cloud storage."
168
148
  action={
169
- <Button
170
- size="lg"
171
- variant="neutral"
172
- onPress={() => openModal("add-connection")}
173
- >
149
+ <Button size="lg" variant="neutral" onPress={() => openModal("add-connection")}>
174
150
  Connect Storage
175
151
  </Button>
176
152
  }
177
153
  />
178
154
  </Section>
179
155
  )}
180
-
181
156
  </div>
182
157
  );
183
158
  }
@@ -11,24 +11,12 @@ type ModalComponent = ComponentType<{ onClose: (extraKeys?: string[]) => void }>
11
11
  * Add new entries here to register search-param-driven modals.
12
12
  */
13
13
  const MODAL_REGISTRY = {
14
- "add-connection": lazy(
15
- () => import("~/routes/connections/createConnection.modal"),
16
- ),
17
- "convert-overlay": lazy(
18
- () => import("~/components/DataGrid/ConvertOverlay.modal"),
19
- ),
20
- "edit-connection": lazy(
21
- () => import("~/routes/connections/updateConnection.modal"),
22
- ),
23
- "directory-info": lazy(
24
- () => import("~/components/DirectoryView/modals/DirectoryInfo.modal"),
25
- ),
26
- "file-info": lazy(
27
- () => import("~/components/DirectoryView/modals/FileInfo.modal"),
28
- ),
29
- cyberduck: lazy(
30
- () => import("~/components/DirectoryView/modals/Cyberduck.modal"),
31
- ),
14
+ "add-connection": lazy(() => import("~/routes/connections/createConnection.modal")),
15
+ "convert-overlay": lazy(() => import("~/components/DataGrid/ConvertOverlay.modal")),
16
+ "edit-connection": lazy(() => import("~/routes/connections/updateConnection.modal")),
17
+ "directory-info": lazy(() => import("~/components/DirectoryView/modals/DirectoryInfo.modal")),
18
+ "file-info": lazy(() => import("~/components/DirectoryView/modals/FileInfo.modal")),
19
+ cyberduck: lazy(() => import("~/components/DirectoryView/modals/Cyberduck.modal")),
32
20
  } satisfies Record<string, ModalComponent>;
33
21
 
34
22
  /**
@@ -5,10 +5,7 @@ import { type LoaderFunctionArgs } from "react-router";
5
5
  import { ConnectionConfig } from "~/.generated/client";
6
6
  import { authContext } from "~/.server/auth/authMiddleware";
7
7
  import { getS3Client } from "~/.server/auth/getS3Client";
8
- import {
9
- buildDirectoryTree,
10
- TreeNode,
11
- } from "~/components/DirectoryView/buildDirectoryTree";
8
+ import { buildDirectoryTree, TreeNode } from "~/components/DirectoryView/buildDirectoryTree";
12
9
  import { type NotificationInput } from "~/components/Notification/Notification.store";
13
10
  import { getConnection } from "~/routes/connections/connections.server";
14
11
  import { getObjects } from "~/utils/getObjects";
@@ -37,8 +34,7 @@ export const loader = async ({
37
34
  params,
38
35
  context,
39
36
  }: LoaderFunctionArgs): Promise<BucketRouteLoaderResponse> => {
40
- const { user, credentials: connectionsCredentials } =
41
- context.get(authContext);
37
+ const { user, credentials: connectionsCredentials } = context.get(authContext);
42
38
  const { name: connectionName } = params;
43
39
 
44
40
  if (!connectionName) throw new Error("Connection name is required");
@@ -51,16 +47,11 @@ export const loader = async ({
51
47
  const { bucketName } = connectionConfig;
52
48
 
53
49
  const credentials = connectionsCredentials[connectionName];
54
- if (!credentials)
55
- throw new Error(`No credentials for connection: ${connectionName}`);
50
+ if (!credentials) throw new Error(`No credentials for connection: ${connectionName}`);
56
51
 
57
52
  const urlPath = params["*"] ?? "";
58
53
  const connPrefix = connectionConfig.prefix?.replace(/\/$/, "") ?? "";
59
- const pathName = connPrefix
60
- ? urlPath
61
- ? `${connPrefix}/${urlPath}`
62
- : connPrefix
63
- : urlPath;
54
+ const pathName = connPrefix ? (urlPath ? `${connPrefix}/${urlPath}` : connPrefix) : urlPath;
64
55
  const prefix = getPrefix(pathName);
65
56
  const name = getName(pathName, bucketName);
66
57
 
@@ -94,12 +85,7 @@ export const loader = async ({
94
85
  );
95
86
 
96
87
  if (objects.length > 0) {
97
- const nodes = buildDirectoryTree(
98
- objects,
99
- connectionName,
100
- prefix,
101
- urlPath,
102
- );
88
+ const nodes = buildDirectoryTree(objects, connectionName, prefix, urlPath);
103
89
 
104
90
  return {
105
91
  connectionName,
@@ -9,10 +9,7 @@ import {
9
9
  useNavigate,
10
10
  } from "react-router";
11
11
 
12
- import {
13
- type BucketRouteLoaderResponse,
14
- loader,
15
- } from "./objects.loader";
12
+ import { type BucketRouteLoaderResponse, loader } from "./objects.loader";
16
13
  import { requestDurationMiddleware } from "~/.server/requestDurationMiddleware";
17
14
  import { getCrumbs } from "~/components/Breadcrumbs/getCrumbs";
18
15
  import { ClientOnly } from "~/components/ClientOnly";
@@ -35,9 +32,9 @@ import { createSignedFetch } from "~/utils/signedFetch";
35
32
 
36
33
  // Lazy load Viewer to prevent SSR issues with client-only code
37
34
  const Viewer = lazy(() =>
38
- import("~/components/.client/ImageViewer/components/ImageViewer").then(
39
- (module) => ({ default: module.Viewer }),
40
- ),
35
+ import("~/components/.client/ImageViewer/components/ImageViewer").then((module) => ({
36
+ default: module.Viewer,
37
+ })),
41
38
  );
42
39
 
43
40
  export { loader };
@@ -82,10 +79,7 @@ export const handle = {
82
79
  * This route has no mutating forms of its own, so skipping the
83
80
  * `formAction` branch is safe.
84
81
  */
85
- export const shouldRevalidate: ShouldRevalidateFunction = ({
86
- currentUrl,
87
- nextUrl,
88
- }) => {
82
+ export const shouldRevalidate: ShouldRevalidateFunction = ({ currentUrl, nextUrl }) => {
89
83
  if (currentUrl.pathname !== nextUrl.pathname) return true;
90
84
  if (currentUrl.search !== nextUrl.search) return true;
91
85
  return false;
@@ -157,10 +151,7 @@ export default function ObjectsRoute() {
157
151
  { method: "delete", action: "/api/pinned" },
158
152
  );
159
153
  } else {
160
- const totalSize = nodes.reduce(
161
- (sum, node) => sum + computeDirectorySize(node),
162
- 0,
163
- );
154
+ const totalSize = nodes.reduce((sum, node) => sum + computeDirectorySize(node), 0);
164
155
  const lastModified = nodes.reduce(
165
156
  (max, node) => Math.max(max, computeDirectoryLastModified(node)),
166
157
  0,
@@ -169,9 +160,7 @@ export default function ObjectsRoute() {
169
160
  {
170
161
  connectionName,
171
162
  pathName: urlPath,
172
- displayName: urlPath
173
- ? getName(urlPath, connectionName)
174
- : connectionName,
163
+ displayName: urlPath ? getName(urlPath, connectionName) : connectionName,
175
164
  totalSize: String(totalSize),
176
165
  lastModified: lastModified ? String(lastModified) : "",
177
166
  },
@@ -203,10 +192,7 @@ export default function ObjectsRoute() {
203
192
  {isPinned ? <BookmarkCheck size={16} /> : <Bookmark size={16} />}
204
193
  {isPinned ? "Pinned" : "Pin"}
205
194
  </Button>
206
- <Button
207
- onPress={() => openModal("cyberduck", { connectionName })}
208
- variant="secondary"
209
- >
195
+ <Button onPress={() => openModal("cyberduck", { connectionName })} variant="secondary">
210
196
  <Download size={16} />
211
197
  Access with Cyberduck
212
198
  </Button>
@@ -226,13 +212,10 @@ export default function ObjectsRoute() {
226
212
  <header className="flex items-center justify-between p-4 bg-amber-100 border-b border-amber-300 text-amber-900">
227
213
  <div className="flex items-center gap-2">
228
214
  <span className="text-sm">
229
- CSV files are slow to query. Convert to Parquet for better
230
- performance.
215
+ CSV files are slow to query. Convert to Parquet for better performance.
231
216
  </span>
232
217
  </div>
233
- <Button onPress={() => openModal("convert-overlay")}>
234
- Convert to Parquet
235
- </Button>
218
+ <Button onPress={() => openModal("convert-overlay")}>Convert to Parquet</Button>
236
219
  </header>
237
220
  )}
238
221
  <DataGrid resourceId={resourceId} />
@@ -251,9 +234,7 @@ export default function ObjectsRoute() {
251
234
  // Feed it directly to `constructS3Url`, which expects a full key.
252
235
  const s3Url = constructS3Url(connectionConfig, pathName);
253
236
  const signedFetch = createSignedFetch(
254
- () =>
255
- useConnectionsStore.getState().connections[connectionName]
256
- ?.credentials,
237
+ () => useConnectionsStore.getState().connections[connectionName]?.credentials,
257
238
  connectionConfig,
258
239
  );
259
240
  return (
@@ -11,10 +11,7 @@ export const middleware = [requestDurationMiddleware, authMiddleware];
11
11
 
12
12
  const label = createLabel("presign", "gray");
13
13
 
14
- export const loader = async ({
15
- params,
16
- context,
17
- }: ActionFunctionArgs): Promise<Response> => {
14
+ export const loader = async ({ params, context }: ActionFunctionArgs): Promise<Response> => {
18
15
  const { user, credentials: connectionsCredentials } = context.get(authContext);
19
16
  const { name: connectionName } = params;
20
17
  const pathName = params["*"] ?? "";
@@ -27,29 +24,19 @@ export const loader = async ({
27
24
  }
28
25
 
29
26
  const { provider, bucketName, prefix: connPrefix } = connectionConfig;
30
- const s3Key = connPrefix
31
- ? `${connPrefix.replace(/\/$/, "")}/${pathName}`
32
- : pathName;
27
+ const s3Key = connPrefix ? `${connPrefix.replace(/\/$/, "")}/${pathName}` : pathName;
33
28
  console.info(`${label} Presign route: ${provider}/${bucketName}/${s3Key}`);
34
29
 
35
30
  const credentials = connectionsCredentials[connectionName];
36
- if (!credentials)
37
- throw new Error(`No credentials for connection: ${connectionName}`);
31
+ if (!credentials) throw new Error(`No credentials for connection: ${connectionName}`);
38
32
 
39
33
  try {
40
34
  const s3Client = await getS3Client(connectionConfig, credentials, user.sub);
41
- const presignedUrl = await getPresignedUrl(
42
- connectionConfig,
43
- s3Client,
44
- s3Key,
45
- );
35
+ const presignedUrl = await getPresignedUrl(connectionConfig, s3Client, s3Key);
46
36
 
47
37
  return Response.json({ url: presignedUrl });
48
38
  } catch (error) {
49
39
  console.error("Error presigning url", error);
50
- return Response.json(
51
- { error: "Failed to generate presigned URL" },
52
- { status: 500 },
53
- );
40
+ return Response.json({ error: "Failed to generate presigned URL" }, { status: 500 });
54
41
  }
55
42
  };
@@ -105,10 +105,7 @@ export default function RecentRoute() {
105
105
  </>
106
106
  }
107
107
  >
108
- <Button
109
- variant="secondary"
110
- onPress={() => clearFetcher.submit({}, { method: "delete" })}
111
- >
108
+ <Button variant="secondary" onPress={() => clearFetcher.submit({}, { method: "delete" })}>
112
109
  <Trash2 size={16} />
113
110
  Clear history
114
111
  </Button>