@cytario/web 2.1.4 → 2.1.6
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.
- package/README.md +20 -20
- package/app/.server/auth/README.md +8 -8
- package/app/.server/auth/authMiddleware.ts +9 -25
- package/app/.server/auth/exchangeAuthCode.ts +2 -6
- package/app/.server/auth/getS3Client.ts +3 -13
- package/app/.server/auth/getSessionCredentials.ts +6 -20
- package/app/.server/auth/getUserInfo.ts +2 -6
- package/app/.server/auth/keycloakAdmin/client.ts +2 -9
- package/app/.server/auth/keycloakAdmin/groups.ts +9 -26
- package/app/.server/auth/keycloakAdmin/users.ts +7 -23
- package/app/.server/auth/oauthState.ts +4 -13
- package/app/.server/auth/redirectIfAuthenticated.ts +1 -3
- package/app/.server/auth/refreshAuthTokens.ts +5 -19
- package/app/.server/auth/sessionMiddleware.ts +1 -4
- package/app/.server/auth/sessionStorage.ts +1 -4
- package/app/.server/auth/verifyIdToken.ts +1 -3
- package/app/.server/auth/wellKnownEndpoints.ts +1 -4
- package/app/.server/db/redis.ts +5 -1
- package/app/.server/logging.ts +1 -4
- package/app/.server/requestDurationMiddleware.ts +1 -4
- package/app/components/.client/ImageViewer/README.md +5 -5
- package/app/components/.client/ImageViewer/components/ChannelsController/ChannelsController.tsx +7 -9
- package/app/components/.client/ImageViewer/components/ChannelsController/ChannelsControllerBrightfieldItem.tsx +1 -2
- package/app/components/.client/ImageViewer/components/ChannelsController/ChannelsControllerItem.tsx +2 -5
- package/app/components/.client/ImageViewer/components/ChannelsController/ChannelsControllerItemList.tsx +7 -15
- package/app/components/.client/ImageViewer/components/ChannelsController/ColorPicker/ColorPicker.tsx +2 -9
- package/app/components/.client/ImageViewer/components/ChannelsController/ColorPicker/ColorSwatch.tsx +1 -4
- package/app/components/.client/ImageViewer/components/ChannelsController/DomainSlider.tsx +1 -3
- package/app/components/.client/ImageViewer/components/ChannelsController/Histogram.tsx +16 -18
- package/app/components/.client/ImageViewer/components/ChannelsController/HistogramChannel.tsx +2 -8
- package/app/components/.client/ImageViewer/components/ChannelsController/MinMaxSettings.tsx +6 -15
- package/app/components/.client/ImageViewer/components/FeatureBar/FeatureBarDragHandle.tsx +1 -5
- package/app/components/.client/ImageViewer/components/FeatureBar/FeatureBarToggle.tsx +1 -5
- package/app/components/.client/ImageViewer/components/FeatureBar/FeatureItem.tsx +1 -5
- package/app/components/.client/ImageViewer/components/FeatureBar/Presets.tsx +3 -11
- package/app/components/.client/ImageViewer/components/FeatureBar/useFeatureBar.tsx +16 -25
- package/app/components/.client/ImageViewer/components/Image/Channels/useChannelsLayer.ts +7 -18
- package/app/components/.client/ImageViewer/components/Image/ImageContainer.tsx +1 -1
- package/app/components/.client/ImageViewer/components/Image/ImagePanel.tsx +6 -26
- package/app/components/.client/ImageViewer/components/Image/ImagePreview.tsx +2 -9
- package/app/components/.client/ImageViewer/components/Image/Overlays/AdditivePolygonLayer.tsx +1 -5
- package/app/components/.client/ImageViewer/components/Image/Overlays/AdditiveScatterplotLayer.tsx +1 -5
- package/app/components/.client/ImageViewer/components/Image/Overlays/OverlaysLayer.tsx +6 -24
- package/app/components/.client/ImageViewer/components/Image/Overlays/markerUniforms.ts +2 -5
- package/app/components/.client/ImageViewer/components/Image/Overlays/useOverlaysLayer.tsx +7 -21
- package/app/components/.client/ImageViewer/components/Image/useInitializeChannels.ts +1 -7
- package/app/components/.client/ImageViewer/components/Image/useResizeObserver.ts +1 -1
- package/app/components/.client/ImageViewer/components/Magnifier.tsx +5 -13
- package/app/components/.client/ImageViewer/components/Measurements/ActiveViewStatePreview.tsx +3 -8
- package/app/components/.client/ImageViewer/components/Measurements/CursorTick.tsx +2 -7
- package/app/components/.client/ImageViewer/components/Measurements/Ruler.tsx +1 -8
- package/app/components/.client/ImageViewer/components/Measurements/SlideCarrier.tsx +3 -13
- package/app/components/.client/ImageViewer/components/Measurements/Tick.tsx +1 -1
- package/app/components/.client/ImageViewer/components/Measurements/calculateViewStateToFit.ts +1 -1
- package/app/components/.client/ImageViewer/components/Measurements/useMeasurements.ts +9 -28
- package/app/components/.client/ImageViewer/components/OverlaysController/AddOverlay.tsx +4 -13
- package/app/components/.client/ImageViewer/components/OverlaysController/OverlayPicker.modal.tsx +1 -6
- package/app/components/.client/ImageViewer/components/OverlaysController/OverlaysController.Item.tsx +24 -54
- package/app/components/.client/ImageViewer/components/OverlaysController/OverlaysController.tsx +1 -3
- package/app/components/.client/ImageViewer/components/SplitViewToggle.tsx +1 -3
- package/app/components/.client/ImageViewer/components/ViewerHeader.tsx +1 -3
- package/app/components/.client/ImageViewer/state/decoders/decodeJPEG2000.d.ts +9 -11
- package/app/components/.client/ImageViewer/state/decoders/decodeJPEG2000.js +11 -11
- package/app/components/.client/ImageViewer/state/decoders/decoder.worker.js +49 -49
- package/app/components/.client/ImageViewer/state/decoders/genericDecoder.ts +76 -81
- package/app/components/.client/ImageViewer/state/decoders/jp2k-decoder.ts +9 -9
- package/app/components/.client/ImageViewer/state/decoders/lzwDecoder.ts +9 -9
- package/app/components/.client/ImageViewer/state/loaders/loadBioformatsZarrWithCredentials.ts +10 -22
- package/app/components/.client/ImageViewer/state/store/ViewerStoreContext.tsx +4 -18
- package/app/components/.client/ImageViewer/state/store/createViewerStore.ts +110 -194
- package/app/components/.client/ImageViewer/state/store/getInitialChannelsState.ts +2 -6
- package/app/components/.client/ImageViewer/state/store/selectors.ts +9 -9
- package/app/components/.client/ImageViewer/state/store/types.ts +3 -12
- package/app/components/.client/ImageViewer/state/transport/CredentialedHTTPStore.ts +1 -5
- package/app/components/.client/ImageViewer/state/transport/SigV4TiffClient.ts +2 -9
- package/app/components/.client/ImageViewer/utils/getSelectionStats.ts +1 -4
- package/app/components/.client/ImageViewer/utils/handleImageViewerHover.ts +1 -1
- package/app/components/.client/ImageViewer/utils/mapChannelConfigsToState.ts +2 -4
- package/app/components/.client/ImageViewer/utils/useTilesLoading.ts +3 -3
- package/app/components/AppHeader.tsx +1 -4
- package/app/components/Breadcrumbs/Breadcrumbs.tsx +4 -13
- package/app/components/Breadcrumbs/getCrumbs.tsx +1 -1
- package/app/components/ClientOnly.tsx +1 -1
- package/app/components/Container.tsx +3 -15
- package/app/components/DataGrid/ConvertOverlay.modal.tsx +1 -6
- package/app/components/DataGrid/DataGrid.tsx +7 -27
- package/app/components/DataGrid/WktSvg.tsx +2 -4
- package/app/components/DataGrid/getParquetSchema.ts +1 -4
- package/app/components/DescriptionList.tsx +1 -3
- package/app/components/DirectoryView/ConnectionMenu.tsx +8 -22
- package/app/components/DirectoryView/DirectoryView.tsx +10 -46
- package/app/components/DirectoryView/DirectoryViewGrid.tsx +16 -49
- package/app/components/DirectoryView/DirectoryViewTableConnection.tsx +1 -4
- package/app/components/DirectoryView/DirectoryViewTableDirectory.tsx +2 -7
- package/app/components/DirectoryView/DirectoryViewTree.tsx +5 -21
- package/app/components/DirectoryView/FilterBar.tsx +9 -48
- package/app/components/DirectoryView/buildDirectoryTree.ts +6 -25
- package/app/components/DirectoryView/filterNodes.ts +4 -11
- package/app/components/DirectoryView/modals/Cyberduck.modal.tsx +6 -15
- package/app/components/DirectoryView/modals/FileInfo.modal.tsx +1 -5
- package/app/components/DirectoryView/useLayoutStore.ts +5 -25
- package/app/components/GlobalSearch/GlobalSearch.tsx +1 -4
- package/app/components/GlobalSearch/SearchBar.tsx +1 -7
- package/app/components/GlobalSearch/Suggestions.tsx +0 -1
- package/app/components/ImageViewer/state/formatRegistry.ts +5 -18
- package/app/components/LavaLoader.tsx +4 -11
- package/app/components/Layout/Footer.tsx +1 -4
- package/app/components/Pills/ScopePill.tsx +2 -8
- package/app/components/Table/ColumnFilterInput.tsx +5 -20
- package/app/components/Table/ColumnResizeHandle.tsx +1 -5
- package/app/components/Table/ColumnSortButton.tsx +1 -5
- package/app/components/Table/SelectionFooter.tsx +3 -5
- package/app/components/Table/Table.tsx +5 -21
- package/app/components/Table/TableBodyRow.tsx +19 -31
- package/app/components/Table/TableHeaderRow.tsx +7 -28
- package/app/components/Table/TableMenu.tsx +4 -20
- package/app/components/Table/state/createTableStore.ts +3 -9
- package/app/components/Table/state/useTableStore.ts +1 -3
- package/app/components/Table/types.ts +4 -10
- package/app/components/Table/useColumnFilters.ts +1 -4
- package/app/components/Table/useColumnVisibility.ts +4 -11
- package/app/components/Table/useColumnWidths.ts +1 -3
- package/app/components/Table/useTableSorting.ts +4 -12
- package/app/components/Tooltip/Tooltip.tsx +4 -17
- package/app/components/Tooltip/TooltipSpan.tsx +5 -28
- package/app/components/Tooltip/useCopyToClipboard.ts +1 -3
- package/app/components/Tooltip/useMiddleEllipsis.ts +2 -7
- package/app/components/Tooltip/useOverflowDetection.ts +2 -5
- package/app/components/UserMenu.tsx +2 -9
- package/app/entry.server.tsx +9 -19
- package/app/hooks/useSearchParam.ts +2 -4
- package/app/lib/bootstrapPluginsCore.ts +4 -9
- package/app/root.tsx +4 -15
- package/app/routes/admin/assertAdminScope.ts +1 -3
- package/app/routes/admin/assertGroupPathsInScope.ts +3 -11
- package/app/routes/admin/assertGroupsInScope.ts +3 -11
- package/app/routes/admin/assertUsersInScope.ts +2 -8
- package/app/routes/admin/bulkInvite/bulkInvite.action.ts +2 -13
- package/app/routes/admin/bulkInvite/bulkInvite.form.tsx +18 -35
- package/app/routes/admin/createGroup/createGroup.action.ts +3 -10
- package/app/routes/admin/createGroup/createGroup.form.tsx +2 -9
- package/app/routes/admin/createGroup/createGroup.modal.tsx +1 -5
- package/app/routes/admin/inviteUser/inviteUser.action.ts +1 -4
- package/app/routes/admin/inviteUser/inviteUser.form.tsx +2 -8
- package/app/routes/admin/inviteUser/inviteUser.loader.ts +1 -4
- package/app/routes/admin/inviteUser/inviteUser.modal.tsx +3 -16
- package/app/routes/admin/updateUser/updateUser.form.tsx +4 -15
- package/app/routes/admin/updateUser/updateUser.modal.tsx +5 -23
- package/app/routes/admin/updateUser/userDetail.action.ts +2 -10
- package/app/routes/admin/users/BulkActions.tsx +15 -38
- package/app/routes/admin/users/bulkUsers.action.ts +2 -9
- package/app/routes/admin/users/bulkUsers.schema.ts +1 -6
- package/app/routes/admin/users/users.route.tsx +14 -63
- package/app/routes/api/cyberduck-profile.$name.ts +6 -2
- package/app/routes/auth/callback.route.tsx +8 -33
- package/app/routes/auth/login.route.tsx +8 -11
- package/app/routes/auth/logout.route.tsx +4 -14
- package/app/routes/config.route.tsx +1 -5
- package/app/routes/connections/connection.form.tsx +5 -14
- package/app/routes/connections/connection.schema.ts +4 -16
- package/app/routes/connections/connections.loader.ts +11 -23
- package/app/routes/connections/connections.route.tsx +1 -5
- package/app/routes/connections/connections.server.ts +1 -3
- package/app/routes/connections/createConnection.action.ts +2 -8
- package/app/routes/connections/createConnection.modal.tsx +3 -13
- package/app/routes/connections/deleteConnection.action.ts +1 -4
- package/app/routes/connections/updateConnection.action.ts +2 -8
- package/app/routes/connections/updateConnection.modal.tsx +4 -14
- package/app/routes/home/home.route.tsx +8 -33
- package/app/routes/layouts/ModalOutlet.tsx +6 -18
- package/app/routes/objects/objects.loader.ts +5 -19
- package/app/routes/objects/objects.route.tsx +11 -30
- package/app/routes/presign.route.tsx +5 -18
- package/app/routes/recent.route.tsx +1 -4
- package/app/routes/search.route.tsx +4 -17
- package/app/routes.ts +1 -4
- package/app/tailwind.css +17 -12
- package/app/types/cornerstone-codecs.d.ts +25 -29
- package/app/utils/connectionsStore/selectors.ts +2 -6
- package/app/utils/connectionsStore/useConnectionsStore.ts +2 -6
- package/app/utils/db/convertCsvToParquet.ts +1 -3
- package/app/utils/db/createDatabase.ts +1 -3
- package/app/utils/db/createSingleton.ts +1 -1
- package/app/utils/db/getBlobFromObjectNode.ts +3 -9
- package/app/utils/db/getGeomQuery.ts +1 -1
- package/app/utils/db/getMarkerInfoWasm.ts +1 -3
- package/app/utils/db/getTileBoundingBox.ts +1 -4
- package/app/utils/db/sqlQueries.ts +1 -4
- package/app/utils/fileType.ts +80 -10
- package/app/utils/filterObjects.ts +2 -5
- package/app/utils/localFilesStore/useFileStore.ts +7 -7
- package/app/utils/recentlyViewed.server.ts +1 -4
- package/app/utils/resourceId.ts +3 -11
- package/app/utils/s3Provider.ts +3 -7
- package/app/utils/signedFetch.ts +4 -13
- package/bin-src/codegen.ts +4 -1
- package/package.json +5 -1
- package/prisma/seed.ts +1 -2
- package/public/favicon/site.webmanifest +1 -1
- package/server.js +1 -4
- package/server.js.map +1 -1
- package/vite-plugins/cytario-plugins.ts +2 -8
|
@@ -8,16 +8,12 @@ import { getSelectionStats } from "../../utils/getSelectionStats";
|
|
|
8
8
|
|
|
9
9
|
/** Returns the RGB color for a channel, falling back to OVERLAY_COLORS if metadata lacks a Color. */
|
|
10
10
|
const getInitialColor = (channels: Channel[], index: number): RGB => {
|
|
11
|
-
const colorRaw =
|
|
12
|
-
channels[index]?.Color ?? OVERLAY_COLORS[index % OVERLAY_COLORS.length];
|
|
11
|
+
const colorRaw = channels[index]?.Color ?? OVERLAY_COLORS[index % OVERLAY_COLORS.length];
|
|
13
12
|
return colorRaw.slice(0, 3) as RGB;
|
|
14
13
|
};
|
|
15
14
|
|
|
16
15
|
/** Builds the initial channel configs (color, domain, contrast limits) from OME-TIFF metadata. */
|
|
17
|
-
export const getInitialChannelsState = async (
|
|
18
|
-
metadata: Image,
|
|
19
|
-
loader: Loader,
|
|
20
|
-
) => {
|
|
16
|
+
export const getInitialChannelsState = async (metadata: Image, loader: Loader) => {
|
|
21
17
|
const channels = metadata.Pixels.Channels as Channel[];
|
|
22
18
|
|
|
23
19
|
const selection = { c: 0, x: 0, y: 0, z: 0, t: 0 };
|
|
@@ -14,7 +14,12 @@ const EMPTY_ARRAY: readonly string[] = Object.freeze([]);
|
|
|
14
14
|
// Zustand uses Object.is for equality — returning a new object on every call
|
|
15
15
|
// causes infinite re-render loops.
|
|
16
16
|
let _bfGroupCache: { ids: readonly string[]; result: BrightfieldGroup | null } | null = null;
|
|
17
|
-
let _bfSelectedCache: {
|
|
17
|
+
let _bfSelectedCache: {
|
|
18
|
+
r: ChannelConfig;
|
|
19
|
+
g: ChannelConfig;
|
|
20
|
+
b: ChannelConfig;
|
|
21
|
+
result: ChannelConfig;
|
|
22
|
+
} | null = null;
|
|
18
23
|
export const select = {
|
|
19
24
|
id: (state: ViewerStore) => state.id,
|
|
20
25
|
error: (state: ViewerStore) => state.error,
|
|
@@ -54,10 +59,8 @@ export const select = {
|
|
|
54
59
|
setCursorPosition: (state: ViewerStore) => state.setCursorPosition,
|
|
55
60
|
|
|
56
61
|
/* Channels State Management */
|
|
57
|
-
setActiveChannelsStateIndex: (state: ViewerStore) =>
|
|
58
|
-
|
|
59
|
-
activeChannelsStateIndex: (state: ViewerStore) =>
|
|
60
|
-
state.imagePanels[state.imagePanelIndex],
|
|
62
|
+
setActiveChannelsStateIndex: (state: ViewerStore) => state.setActiveChannelsStateIndex,
|
|
63
|
+
activeChannelsStateIndex: (state: ViewerStore) => state.imagePanels[state.imagePanelIndex],
|
|
61
64
|
|
|
62
65
|
/* Layers */
|
|
63
66
|
layersState: (state: ViewerStore) => {
|
|
@@ -112,10 +115,7 @@ export const select = {
|
|
|
112
115
|
|
|
113
116
|
/* Channels > Selected */
|
|
114
117
|
selectedChannelId: (state: ViewerStore) =>
|
|
115
|
-
state.selectedChannelId as
|
|
116
|
-
| keyof ChannelsStateColumns
|
|
117
|
-
| typeof BRIGHTFIELD_GROUP_ID
|
|
118
|
-
| null,
|
|
118
|
+
state.selectedChannelId as keyof ChannelsStateColumns | typeof BRIGHTFIELD_GROUP_ID | null,
|
|
119
119
|
setSelectedChannelId: (state: ViewerStore) => state.setSelectedChannelId,
|
|
120
120
|
selectedChannel: (state: ViewerStore): ChannelConfig | null => {
|
|
121
121
|
const selectedChannelId = select.selectedChannelId(state);
|
|
@@ -55,9 +55,7 @@ export interface BrightfieldGroup {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/** Detects brightfield R/G/B channels by name from UltiStacker output. */
|
|
58
|
-
export const detectBrightfieldGroup = (
|
|
59
|
-
channelIds: readonly string[],
|
|
60
|
-
): BrightfieldGroup | null => {
|
|
58
|
+
export const detectBrightfieldGroup = (channelIds: readonly string[]): BrightfieldGroup | null => {
|
|
61
59
|
const red = channelIds.find((id) => id.toLowerCase() === "red");
|
|
62
60
|
const green = channelIds.find((id) => id.toLowerCase() === "green");
|
|
63
61
|
const blue = channelIds.find((id) => id.toLowerCase() === "blue");
|
|
@@ -140,16 +138,9 @@ interface ViewerStoreActions {
|
|
|
140
138
|
setContrastLimits: (contrastLimits: ByteDomain) => void;
|
|
141
139
|
resetContrastLimits: () => void;
|
|
142
140
|
|
|
143
|
-
setChannelVisibility: (
|
|
144
|
-
key: keyof ChannelsStateColumns,
|
|
145
|
-
isVisible: boolean
|
|
146
|
-
) => void;
|
|
141
|
+
setChannelVisibility: (key: keyof ChannelsStateColumns, isVisible: boolean) => void;
|
|
147
142
|
|
|
148
|
-
setMarkerVisibility: (
|
|
149
|
-
fileName: string,
|
|
150
|
-
markerName: string,
|
|
151
|
-
isVisible: boolean
|
|
152
|
-
) => void;
|
|
143
|
+
setMarkerVisibility: (fileName: string, markerName: string, isVisible: boolean) => void;
|
|
153
144
|
|
|
154
145
|
setChannelColor: (key: keyof ChannelsState, color: RGBA) => void;
|
|
155
146
|
setMarkerColor: (fileName: string, markerName: string, color: RGBA) => void;
|
|
@@ -13,11 +13,7 @@ export class CredentialedHTTPStore {
|
|
|
13
13
|
private signedFetch: SignedFetch;
|
|
14
14
|
private extraHeaders: Record<string, string>;
|
|
15
15
|
|
|
16
|
-
constructor(
|
|
17
|
-
url: string,
|
|
18
|
-
signedFetch: SignedFetch,
|
|
19
|
-
extraHeaders?: Record<string, string>,
|
|
20
|
-
) {
|
|
16
|
+
constructor(url: string, signedFetch: SignedFetch, extraHeaders?: Record<string, string>) {
|
|
21
17
|
this.baseUrl = new URL(url.endsWith("/") ? url : url + "/");
|
|
22
18
|
this.signedFetch = signedFetch;
|
|
23
19
|
this.extraHeaders = extraHeaders ?? {};
|
|
@@ -15,20 +15,13 @@ export class SigV4TiffClient {
|
|
|
15
15
|
private signedFetch: SignedFetch;
|
|
16
16
|
private extraHeaders: Record<string, string>;
|
|
17
17
|
|
|
18
|
-
constructor(
|
|
19
|
-
url: string,
|
|
20
|
-
signedFetch: SignedFetch,
|
|
21
|
-
extraHeaders?: Record<string, string>,
|
|
22
|
-
) {
|
|
18
|
+
constructor(url: string, signedFetch: SignedFetch, extraHeaders?: Record<string, string>) {
|
|
23
19
|
this.url = url;
|
|
24
20
|
this.signedFetch = signedFetch;
|
|
25
21
|
this.extraHeaders = extraHeaders ?? {};
|
|
26
22
|
}
|
|
27
23
|
|
|
28
|
-
async request({
|
|
29
|
-
headers,
|
|
30
|
-
signal,
|
|
31
|
-
}: { headers?: HeadersInit; signal?: AbortSignal } = {}) {
|
|
24
|
+
async request({ headers, signal }: { headers?: HeadersInit; signal?: AbortSignal } = {}) {
|
|
32
25
|
const response = await this.signedFetch(this.url, {
|
|
33
26
|
headers: {
|
|
34
27
|
...this.extraHeaders,
|
|
@@ -32,10 +32,7 @@ export async function getSelectionStats({
|
|
|
32
32
|
const sortedPixels = [...pixels].sort((a, b) => a - b);
|
|
33
33
|
// dtype is structurally `string` in @cytario/plugin-api; one of the
|
|
34
34
|
// canonical PixelType values is guaranteed at runtime.
|
|
35
|
-
const histogram = getHistogram(
|
|
36
|
-
sortedPixels,
|
|
37
|
-
getDtypeBitDepth(data.dtype as SupportedDtype),
|
|
38
|
-
);
|
|
35
|
+
const histogram = getHistogram(sortedPixels, getDtypeBitDepth(data.dtype as SupportedDtype));
|
|
39
36
|
const domain = getDomain(sortedPixels);
|
|
40
37
|
const contrastLimits = getContrastLimits(sortedPixels);
|
|
41
38
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// import { PickingInfo } from "@deck.gl/core";
|
|
4
4
|
|
|
5
5
|
export const handleImageViewerHover = (
|
|
6
|
-
{ tile, coordinate, sourceLayer: layer }: any // PickingInfo
|
|
6
|
+
{ tile, coordinate, sourceLayer: layer }: any, // PickingInfo
|
|
7
7
|
) => {
|
|
8
8
|
let hoverData;
|
|
9
9
|
// Tiled layer needs a custom layerZoomScale.
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { ChannelsStateColumns, ChannelsState } from "../state/store/types";
|
|
2
2
|
|
|
3
|
-
export const mapChannelConfigsToState = (
|
|
4
|
-
state: ChannelsState
|
|
5
|
-
): ChannelsStateColumns => {
|
|
3
|
+
export const mapChannelConfigsToState = (state: ChannelsState): ChannelsStateColumns => {
|
|
6
4
|
return Object.entries(state).reduce<ChannelsStateColumns>(
|
|
7
5
|
(acc, [id, config]) => {
|
|
8
6
|
if (!config.isVisible) return acc;
|
|
@@ -25,6 +23,6 @@ export const mapChannelConfigsToState = (
|
|
|
25
23
|
domains: [],
|
|
26
24
|
selections: [],
|
|
27
25
|
histograms: [],
|
|
28
|
-
}
|
|
26
|
+
},
|
|
29
27
|
);
|
|
30
28
|
};
|
|
@@ -2,7 +2,7 @@ import { useRef, useCallback } from "react";
|
|
|
2
2
|
|
|
3
3
|
export const useTilesLoading = (
|
|
4
4
|
imagePanelId: number,
|
|
5
|
-
setIsTilesLoading: (imagePanelId: number, count: number) => void
|
|
5
|
+
setIsTilesLoading: (imagePanelId: number, count: number) => void,
|
|
6
6
|
) => {
|
|
7
7
|
const loadingSet = useRef(new Set<string>());
|
|
8
8
|
|
|
@@ -12,7 +12,7 @@ export const useTilesLoading = (
|
|
|
12
12
|
loadingSet.current.add(id);
|
|
13
13
|
setIsTilesLoading(imagePanelId, loadingSet.current.size);
|
|
14
14
|
},
|
|
15
|
-
[imagePanelId, setIsTilesLoading]
|
|
15
|
+
[imagePanelId, setIsTilesLoading],
|
|
16
16
|
);
|
|
17
17
|
|
|
18
18
|
const finishTile = useCallback(
|
|
@@ -20,7 +20,7 @@ export const useTilesLoading = (
|
|
|
20
20
|
loadingSet.current.delete(id);
|
|
21
21
|
setIsTilesLoading(imagePanelId, loadingSet.current.size);
|
|
22
22
|
},
|
|
23
|
-
[imagePanelId, setIsTilesLoading]
|
|
23
|
+
[imagePanelId, setIsTilesLoading],
|
|
24
24
|
);
|
|
25
25
|
|
|
26
26
|
return { loadTile, finishTile };
|
|
@@ -53,10 +53,7 @@ export function AppHeader() {
|
|
|
53
53
|
<div className="h-full flex-none flex gap-2 p-2 items-center">
|
|
54
54
|
<GlobalSearch />
|
|
55
55
|
{data?.accountSettingsUrl && data.user && (
|
|
56
|
-
<UserMenu
|
|
57
|
-
user={data.user}
|
|
58
|
-
accountSettingsUrl={data.accountSettingsUrl}
|
|
59
|
-
/>
|
|
56
|
+
<UserMenu user={data.user} accountSettingsUrl={data.accountSettingsUrl} />
|
|
60
57
|
)}
|
|
61
58
|
</div>
|
|
62
59
|
</header>
|
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
type BreadcrumbItem,
|
|
4
|
-
} from "@cytario/design";
|
|
5
|
-
import { Link , UIMatch, useMatches } from "react-router";
|
|
6
|
-
|
|
1
|
+
import { Breadcrumbs as DesignBreadcrumbs, type BreadcrumbItem } from "@cytario/design";
|
|
2
|
+
import { Link, UIMatch, useMatches } from "react-router";
|
|
7
3
|
|
|
8
4
|
import { Logo } from "../Logo";
|
|
9
5
|
|
|
@@ -21,9 +17,7 @@ type BreadcrumbMatch = UIMatch<
|
|
|
21
17
|
|
|
22
18
|
export function Breadcrumbs() {
|
|
23
19
|
const matches = useMatches() as BreadcrumbMatch[];
|
|
24
|
-
const filteredMatches = matches.filter(
|
|
25
|
-
(match) => match.handle && match.handle.breadcrumb
|
|
26
|
-
);
|
|
20
|
+
const filteredMatches = matches.filter((match) => match.handle && match.handle.breadcrumb);
|
|
27
21
|
|
|
28
22
|
const crumbs = filteredMatches.flatMap((match) => {
|
|
29
23
|
const result = match.handle.breadcrumb(match);
|
|
@@ -47,10 +41,7 @@ export function Breadcrumbs() {
|
|
|
47
41
|
</Link>
|
|
48
42
|
)}
|
|
49
43
|
{items.length > 0 && (
|
|
50
|
-
<DesignBreadcrumbs
|
|
51
|
-
items={items}
|
|
52
|
-
className="flex items-center overflow-hidden"
|
|
53
|
-
/>
|
|
44
|
+
<DesignBreadcrumbs items={items} className="flex items-center overflow-hidden" />
|
|
54
45
|
)}
|
|
55
46
|
</div>
|
|
56
47
|
);
|
|
@@ -10,7 +10,7 @@ export interface CrumbsOptions {
|
|
|
10
10
|
export const getCrumbs = (
|
|
11
11
|
basePath: string,
|
|
12
12
|
segments: string[],
|
|
13
|
-
options?: CrumbsOptions
|
|
13
|
+
options?: CrumbsOptions,
|
|
14
14
|
): BreadcrumbData[] => {
|
|
15
15
|
const { dataConnectionName, dataConnectionPath } = options ?? {};
|
|
16
16
|
|
|
@@ -18,18 +18,8 @@ export function Section({ children, className, flush }: SectionProps) {
|
|
|
18
18
|
);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
export const Container = ({
|
|
22
|
-
children
|
|
23
|
-
wide,
|
|
24
|
-
}: {
|
|
25
|
-
children: ReactNode;
|
|
26
|
-
wide?: boolean;
|
|
27
|
-
}) => {
|
|
28
|
-
return (
|
|
29
|
-
<div className={wide ? "mx-auto px-4" : "container mx-auto px-4"}>
|
|
30
|
-
{children}
|
|
31
|
-
</div>
|
|
32
|
-
);
|
|
21
|
+
export const Container = ({ children, wide }: { children: ReactNode; wide?: boolean }) => {
|
|
22
|
+
return <div className={wide ? "mx-auto px-4" : "container mx-auto px-4"}>{children}</div>;
|
|
33
23
|
};
|
|
34
24
|
|
|
35
25
|
export function SectionHeader({
|
|
@@ -48,9 +38,7 @@ export function SectionHeader({
|
|
|
48
38
|
{name && <H2 className="grow">{name}</H2>}
|
|
49
39
|
{children}
|
|
50
40
|
</div>
|
|
51
|
-
{secondaryActions &&
|
|
52
|
-
<div className="flex items-center gap-2">{secondaryActions}</div>
|
|
53
|
-
)}
|
|
41
|
+
{secondaryActions && <div className="flex items-center gap-2">{secondaryActions}</div>}
|
|
54
42
|
</header>
|
|
55
43
|
</Container>
|
|
56
44
|
);
|
|
@@ -4,12 +4,7 @@ import { RouteModal } from "~/components/RouteModal";
|
|
|
4
4
|
/** CSV to Parquet conversion modal. */
|
|
5
5
|
export default function ConvertOverlayModal({ onClose }: { onClose: () => void }) {
|
|
6
6
|
return (
|
|
7
|
-
<RouteModal
|
|
8
|
-
title="Convert CSV to Parquet"
|
|
9
|
-
onClose={onClose}
|
|
10
|
-
size="lg"
|
|
11
|
-
isDismissable={false}
|
|
12
|
-
>
|
|
7
|
+
<RouteModal title="Convert CSV to Parquet" onClose={onClose} size="lg" isDismissable={false}>
|
|
13
8
|
<AddOverlay callback={onClose} query="csv" />
|
|
14
9
|
</RouteModal>
|
|
15
10
|
);
|
|
@@ -36,9 +36,7 @@ export const DataGrid = ({ resourceId }: { resourceId: string }) => {
|
|
|
36
36
|
|
|
37
37
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
38
38
|
const { connectionName } = parseResourceId(resourceId);
|
|
39
|
-
const connectionConfig = useConnectionsStore(
|
|
40
|
-
select.connectionConfig(connectionName),
|
|
41
|
-
);
|
|
39
|
+
const connectionConfig = useConnectionsStore(select.connectionConfig(connectionName));
|
|
42
40
|
|
|
43
41
|
// Initial data fetch
|
|
44
42
|
useEffect(() => {
|
|
@@ -77,12 +75,7 @@ export const DataGrid = ({ resourceId }: { resourceId: string }) => {
|
|
|
77
75
|
} finally {
|
|
78
76
|
setIsFetchingMore(false);
|
|
79
77
|
}
|
|
80
|
-
}, [
|
|
81
|
-
resourceId,
|
|
82
|
-
rows.length,
|
|
83
|
-
isFetchingMore,
|
|
84
|
-
hasMore,
|
|
85
|
-
]);
|
|
78
|
+
}, [resourceId, rows.length, isFetchingMore, hasMore]);
|
|
86
79
|
|
|
87
80
|
const columnHelper = createColumnHelper<Record<string, unknown>>();
|
|
88
81
|
|
|
@@ -161,26 +154,17 @@ export const DataGrid = ({ resourceId }: { resourceId: string }) => {
|
|
|
161
154
|
<table className="min-w-full border-collapse text-sm">
|
|
162
155
|
<thead className="bg-gray-100 dark:bg-slate-800 sticky top-0 z-10">
|
|
163
156
|
{table.getHeaderGroups().map((headerGroup) => (
|
|
164
|
-
<tr
|
|
165
|
-
key={headerGroup.id}
|
|
166
|
-
className="grid"
|
|
167
|
-
style={{ gridTemplateColumns }}
|
|
168
|
-
>
|
|
157
|
+
<tr key={headerGroup.id} className="grid" style={{ gridTemplateColumns }}>
|
|
169
158
|
{headerGroup.headers.map((header) => (
|
|
170
159
|
<th
|
|
171
160
|
key={header.id}
|
|
172
161
|
className={`border-b border-gray-200 dark:border-slate-700 px-4 py-2 font-semibold ${
|
|
173
|
-
RIGHT_ALIGNED_COLUMNS.has(header.id)
|
|
174
|
-
? "text-right"
|
|
175
|
-
: "text-left"
|
|
162
|
+
RIGHT_ALIGNED_COLUMNS.has(header.id) ? "text-right" : "text-left"
|
|
176
163
|
}`}
|
|
177
164
|
>
|
|
178
165
|
{header.isPlaceholder
|
|
179
166
|
? null
|
|
180
|
-
: flexRender(
|
|
181
|
-
header.column.columnDef.header,
|
|
182
|
-
header.getContext(),
|
|
183
|
-
)}
|
|
167
|
+
: flexRender(header.column.columnDef.header, header.getContext())}
|
|
184
168
|
</th>
|
|
185
169
|
))}
|
|
186
170
|
</tr>
|
|
@@ -208,9 +192,7 @@ export const DataGrid = ({ resourceId }: { resourceId: string }) => {
|
|
|
208
192
|
<td
|
|
209
193
|
key={cell.id}
|
|
210
194
|
className={`border-b border-gray-100 dark:border-slate-700 tabular-nums px-4 flex items-center ${
|
|
211
|
-
RIGHT_ALIGNED_COLUMNS.has(cell.column.id)
|
|
212
|
-
? "justify-end"
|
|
213
|
-
: ""
|
|
195
|
+
RIGHT_ALIGNED_COLUMNS.has(cell.column.id) ? "justify-end" : ""
|
|
214
196
|
}`}
|
|
215
197
|
>
|
|
216
198
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
@@ -221,9 +203,7 @@ export const DataGrid = ({ resourceId }: { resourceId: string }) => {
|
|
|
221
203
|
})}
|
|
222
204
|
</tbody>
|
|
223
205
|
</table>
|
|
224
|
-
{isFetchingMore &&
|
|
225
|
-
<div className="p-2 text-center text-gray-500">Loading more...</div>
|
|
226
|
-
)}
|
|
206
|
+
{isFetchingMore && <div className="p-2 text-center text-gray-500">Loading more...</div>}
|
|
227
207
|
</div>
|
|
228
208
|
);
|
|
229
209
|
};
|
|
@@ -87,15 +87,13 @@ function parseWkt(wkt: string): Point[][] {
|
|
|
87
87
|
|
|
88
88
|
if (geometry.type === "Polygon") {
|
|
89
89
|
// Polygon: [[[x, y], [x, y], ...]]
|
|
90
|
-
return (coords as number[][][]).map((ring) =>
|
|
91
|
-
ring.map(([x, y]) => ({ x, y }))
|
|
92
|
-
);
|
|
90
|
+
return (coords as number[][][]).map((ring) => ring.map(([x, y]) => ({ x, y })));
|
|
93
91
|
}
|
|
94
92
|
|
|
95
93
|
if (geometry.type === "MultiPolygon") {
|
|
96
94
|
// MultiPolygon: [[[[x, y], ...]], [[[x, y], ...]]]
|
|
97
95
|
return (coords as number[][][][]).flatMap((polygon) =>
|
|
98
|
-
polygon.map((ring) => ring.map(([x, y]) => ({ x, y })))
|
|
96
|
+
polygon.map((ring) => ring.map(([x, y]) => ({ x, y }))),
|
|
99
97
|
);
|
|
100
98
|
}
|
|
101
99
|
|
|
@@ -2,7 +2,6 @@ import { getFileType, getReadFunction } from "./fileReader";
|
|
|
2
2
|
import { createDatabase } from "../../utils/db/createDatabase";
|
|
3
3
|
import { resolveResourceId } from "~/utils/connectionsStore/selectors";
|
|
4
4
|
|
|
5
|
-
|
|
6
5
|
export interface ParquetColumn {
|
|
7
6
|
name: string;
|
|
8
7
|
type: string;
|
|
@@ -12,9 +11,7 @@ export interface ParquetColumn {
|
|
|
12
11
|
* Fetch the schema (column names and types) from a data file on S3.
|
|
13
12
|
* Supports: parquet, csv, json
|
|
14
13
|
*/
|
|
15
|
-
export async function getParquetSchema(
|
|
16
|
-
resourceId: string,
|
|
17
|
-
): Promise<ParquetColumn[]> {
|
|
14
|
+
export async function getParquetSchema(resourceId: string): Promise<ParquetColumn[]> {
|
|
18
15
|
const { credentials, connectionConfig, s3Uri } = resolveResourceId(resourceId);
|
|
19
16
|
const connection = await createDatabase(resourceId, credentials, connectionConfig);
|
|
20
17
|
const fileType = getFileType(resourceId);
|
|
@@ -16,9 +16,7 @@ const DescriptionList = ({ data, className }: DescriptionListProps<any>) => {
|
|
|
16
16
|
<React.Fragment key={key}>
|
|
17
17
|
<dt className="font-bold text-sm w-32 truncate">{key}</dt>
|
|
18
18
|
<dd>
|
|
19
|
-
<code className="text-slate-700 px-2 py-1 bg-slate-50">
|
|
20
|
-
{String(value)}
|
|
21
|
-
</code>
|
|
19
|
+
<code className="text-slate-700 px-2 py-1 bg-slate-50">{String(value)}</code>
|
|
22
20
|
</dd>
|
|
23
21
|
</React.Fragment>
|
|
24
22
|
))}
|
|
@@ -20,18 +20,12 @@ export function ConnectionMenu({ connectionName }: ConnectionMenuProps) {
|
|
|
20
20
|
const focusReturnRef = useRef<HTMLElement | null>(null);
|
|
21
21
|
const { openModal } = useModal();
|
|
22
22
|
|
|
23
|
-
const connectionConfig = useConnectionsStore(
|
|
24
|
-
select.connectionConfig(connectionName),
|
|
25
|
-
);
|
|
23
|
+
const connectionConfig = useConnectionsStore(select.connectionConfig(connectionName));
|
|
26
24
|
|
|
27
|
-
const rootData = useRouteLoaderData("root") as
|
|
28
|
-
| { user?: UserProfile }
|
|
29
|
-
| undefined;
|
|
25
|
+
const rootData = useRouteLoaderData("root") as { user?: UserProfile } | undefined;
|
|
30
26
|
const user = rootData?.user;
|
|
31
27
|
const userCanModify =
|
|
32
|
-
user && connectionConfig
|
|
33
|
-
? canModify(user, connectionConfig.ownerScope)
|
|
34
|
-
: false;
|
|
28
|
+
user && connectionConfig ? canModify(user, connectionConfig.ownerScope) : false;
|
|
35
29
|
|
|
36
30
|
return (
|
|
37
31
|
<>
|
|
@@ -50,9 +44,7 @@ export function ConnectionMenu({ connectionName }: ConnectionMenuProps) {
|
|
|
50
44
|
<MenuItem
|
|
51
45
|
id="edit"
|
|
52
46
|
icon={Pencil}
|
|
53
|
-
onAction={() =>
|
|
54
|
-
openModal("edit-connection", { nodeName: connectionName })
|
|
55
|
-
}
|
|
47
|
+
onAction={() => openModal("edit-connection", { nodeName: connectionName })}
|
|
56
48
|
>
|
|
57
49
|
Edit
|
|
58
50
|
</MenuItem>
|
|
@@ -63,8 +55,7 @@ export function ConnectionMenu({ connectionName }: ConnectionMenuProps) {
|
|
|
63
55
|
isDanger
|
|
64
56
|
textValue="Delete connection"
|
|
65
57
|
onAction={() => {
|
|
66
|
-
focusReturnRef.current =
|
|
67
|
-
document.activeElement as HTMLElement | null;
|
|
58
|
+
focusReturnRef.current = document.activeElement as HTMLElement | null;
|
|
68
59
|
setConfirmOpen(true);
|
|
69
60
|
}}
|
|
70
61
|
>
|
|
@@ -83,12 +74,7 @@ export function ConnectionMenu({ connectionName }: ConnectionMenuProps) {
|
|
|
83
74
|
/>
|
|
84
75
|
</Menu>
|
|
85
76
|
|
|
86
|
-
<Form
|
|
87
|
-
method="delete"
|
|
88
|
-
action="/connections"
|
|
89
|
-
ref={formRef}
|
|
90
|
-
className="hidden"
|
|
91
|
-
>
|
|
77
|
+
<Form method="delete" action="/connections" ref={formRef} className="hidden">
|
|
92
78
|
<input type="hidden" name="connectionName" value={connectionName} />
|
|
93
79
|
</Form>
|
|
94
80
|
|
|
@@ -103,8 +89,8 @@ export function ConnectionMenu({ connectionName }: ConnectionMenuProps) {
|
|
|
103
89
|
confirmLabel="Remove"
|
|
104
90
|
>
|
|
105
91
|
<p>
|
|
106
|
-
This will remove <strong>{connectionName}</strong> and its associated
|
|
107
|
-
|
|
92
|
+
This will remove <strong>{connectionName}</strong> and its associated recents and pins.
|
|
93
|
+
The underlying storage is not affected.
|
|
108
94
|
</p>
|
|
109
95
|
</ConfirmDialog>
|
|
110
96
|
</>
|
|
@@ -3,21 +3,11 @@ import { useMemo } from "react";
|
|
|
3
3
|
|
|
4
4
|
import { TreeNode } from "./buildDirectoryTree";
|
|
5
5
|
import { DirectoryViewGrid } from "./DirectoryViewGrid";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
connectionColumns,
|
|
9
|
-
} from "./DirectoryViewTableConnection";
|
|
10
|
-
import {
|
|
11
|
-
DirectoryViewTableDirectory,
|
|
12
|
-
fileColumns,
|
|
13
|
-
} from "./DirectoryViewTableDirectory";
|
|
6
|
+
import { DirectoryViewTableConnection, connectionColumns } from "./DirectoryViewTableConnection";
|
|
7
|
+
import { DirectoryViewTableDirectory, fileColumns } from "./DirectoryViewTableDirectory";
|
|
14
8
|
import { DirectoryViewTree } from "./DirectoryViewTree";
|
|
15
9
|
import { FilterBar } from "./FilterBar";
|
|
16
|
-
import {
|
|
17
|
-
filterHiddenNodes,
|
|
18
|
-
filterNodes,
|
|
19
|
-
getNodeAccessors,
|
|
20
|
-
} from "./filterNodes";
|
|
10
|
+
import { filterHiddenNodes, filterNodes, getNodeAccessors } from "./filterNodes";
|
|
21
11
|
import { type ViewMode, useLayoutStore } from "./useLayoutStore";
|
|
22
12
|
import { Container, Section, SectionHeader } from "~/components/Container";
|
|
23
13
|
import { useColumnFilters } from "~/components/Table/useColumnFilters";
|
|
@@ -73,14 +63,7 @@ export function DirectoryView({
|
|
|
73
63
|
);
|
|
74
64
|
|
|
75
65
|
const filteredNodes = useMemo(
|
|
76
|
-
() =>
|
|
77
|
-
filterNodes(
|
|
78
|
-
visibleNodes,
|
|
79
|
-
columnFilters,
|
|
80
|
-
columns,
|
|
81
|
-
kind,
|
|
82
|
-
connections,
|
|
83
|
-
),
|
|
66
|
+
() => filterNodes(visibleNodes, columnFilters, columns, kind, connections),
|
|
84
67
|
[visibleNodes, columnFilters, columns, kind, connections],
|
|
85
68
|
);
|
|
86
69
|
|
|
@@ -109,8 +92,7 @@ export function DirectoryView({
|
|
|
109
92
|
// view at prefix level is an anti-pattern anyway — proper tree-based
|
|
110
93
|
// navigation belongs in a global sidebar per C-56
|
|
111
94
|
// (https://app.plane.so/cytario/browse/C-56/).
|
|
112
|
-
const nameFilter =
|
|
113
|
-
(columnFilters.find((f) => f.id === "name")?.value as string) ?? "";
|
|
95
|
+
const nameFilter = (columnFilters.find((f) => f.id === "name")?.value as string) ?? "";
|
|
114
96
|
|
|
115
97
|
return (
|
|
116
98
|
<Section flush={flush}>
|
|
@@ -120,37 +102,19 @@ export function DirectoryView({
|
|
|
120
102
|
|
|
121
103
|
{showFilters && viewMode !== "list" && (
|
|
122
104
|
<Container>
|
|
123
|
-
<FilterBar
|
|
124
|
-
columns={columns}
|
|
125
|
-
tableId={kind}
|
|
126
|
-
dynamicOptions={dynamicOptions}
|
|
127
|
-
/>
|
|
105
|
+
<FilterBar columns={columns} tableId={kind} dynamicOptions={dynamicOptions} />
|
|
128
106
|
</Container>
|
|
129
107
|
)}
|
|
130
108
|
|
|
131
109
|
<Container>
|
|
132
110
|
{isTree ? (
|
|
133
|
-
<DirectoryViewTree
|
|
134
|
-
nodes={visibleNodes}
|
|
135
|
-
searchTerm={nameFilter}
|
|
136
|
-
kind={kind}
|
|
137
|
-
/>
|
|
111
|
+
<DirectoryViewTree nodes={visibleNodes} searchTerm={nameFilter} kind={kind} />
|
|
138
112
|
) : isGrid ? (
|
|
139
|
-
<DirectoryViewGrid
|
|
140
|
-
nodes={filteredNodes}
|
|
141
|
-
viewMode={viewMode}
|
|
142
|
-
kind={kind}
|
|
143
|
-
/>
|
|
113
|
+
<DirectoryViewGrid nodes={filteredNodes} viewMode={viewMode} kind={kind} />
|
|
144
114
|
) : kind === "connections" ? (
|
|
145
|
-
<DirectoryViewTableConnection
|
|
146
|
-
nodes={filteredNodes}
|
|
147
|
-
showFilters={showFilters}
|
|
148
|
-
/>
|
|
115
|
+
<DirectoryViewTableConnection nodes={filteredNodes} showFilters={showFilters} />
|
|
149
116
|
) : (
|
|
150
|
-
<DirectoryViewTableDirectory
|
|
151
|
-
nodes={filteredNodes}
|
|
152
|
-
showFilters={showFilters}
|
|
153
|
-
/>
|
|
117
|
+
<DirectoryViewTableDirectory nodes={filteredNodes} showFilters={showFilters} />
|
|
154
118
|
)}
|
|
155
119
|
</Container>
|
|
156
120
|
</Section>
|