From 5f1476a9f69c7504ad45d41dd89faa706b7b69ff Mon Sep 17 00:00:00 2001 From: Jake Rosenberg Date: Fri, 2 Feb 2024 12:18:19 -0600 Subject: [PATCH] Route setup and placeholder layouts for Data Files (#1161) * add skeleton routes for data files * Update routing for 3rd party apps * Add boilerplate for _common and datafiles libs * add workspace layout and side nav * add placeholder layouts for data depot components * CSS/layout tweaks * Working listings for Tapis files * rename file listing hook * add hooks for listing params and current user * support navigation into directories * update file listing to use infinite scroll * refactor listing to use callback refs * handle netxpage tokens; use resize observer to handle scroll * listing style changes. * working preview modal * hook row selection up to global state * performance tweaks for listing table * Factor out table component and add performant checkbox * comments and css refactors * set up toolbar with preview modal * refactor modal children setup * add breadcrumbs * extract utility type for passing spread params to useQuery * Add placeholder copy modal * refactor preview modal to split out content display * enable override of listing table props * listings/breadcrumbs work in copy modal * get copy modal functional within My Data * Code cleanup and style tweaks * extract inline styles --- client/.eslintrc.base.json | 6 +- client/modules/_common_components/.babelrc | 12 + .../modules/_common_components/.eslintrc.json | 18 + client/modules/_common_components/README.md | 7 + .../modules/_common_components/project.json | 20 + .../modules/_common_components/src/index.ts | 1 + .../src/lib/common-components.module.css | 7 + .../src/lib/common-components.spec.tsx | 10 + .../src/lib/common-components.tsx | 14 + .../modules/_common_components/tsconfig.json | 20 + .../_common_components/tsconfig.lib.json | 23 + .../_common_components/tsconfig.spec.json | 26 + .../modules/_common_components/vite.config.ts | 27 + client/modules/_hooks/src/apiClient.ts | 6 +- client/modules/_hooks/src/datafiles/index.ts | 20 + .../_hooks/src/datafiles/useConsumePostit.ts | 38 + .../_hooks/src/datafiles/useFileCopy.ts | 27 + .../_hooks/src/datafiles/useFileListing.ts | 92 ++ .../datafiles/useFileListingRouteParams.ts | 33 + .../_hooks/src/datafiles/useFilePreview.ts | 60 ++ .../src/datafiles/usePathDisplayName.ts | 50 + .../_hooks/src/datafiles/useSelectedFiles.ts | 43 + client/modules/_hooks/src/index.ts | 2 + client/modules/_hooks/src/queryConfig.ts | 8 + .../_hooks/src/useAuthenticatedUser.ts | 16 + client/modules/datafiles/.babelrc | 12 + client/modules/datafiles/.eslintrc.json | 18 + client/modules/datafiles/README.md | 7 + client/modules/datafiles/project.json | 20 + .../DatafilesBreadcrumb.module.css | 9 + .../DatafilesBreadcrumb.tsx | 86 ++ .../CopyModal/CopyModal.module.css | 35 + .../DatafilesModal/CopyModal/CopyModal.tsx | 209 ++++ .../src/DatafilesModal/CopyModal/index.ts | 1 + .../src/DatafilesModal/DatafilesModal.tsx | 14 + .../PreviewModal/PreviewContent.tsx | 74 ++ .../PreviewModal/PreviewModal.module.css | 29 + .../PreviewModal/PreviewModal.tsx | 99 ++ .../src/DatafilesModal/PreviewModal/index.ts | 1 + .../DatafilesSideNav.module.css | 35 + .../src/DatafilesSideNav/DatafilesSideNav.tsx | 66 ++ .../DatafilesToolbar.module.css | 25 + .../src/DatafilesToolbar/DatafilesToolbar.tsx | 79 ++ .../src/FileListing/FileListing.module.css | 0 .../datafiles/src/FileListing/FileListing.tsx | 109 ++ .../FileListingTable.module.css | 122 +++ .../FileListingTable/FileListingTable.tsx | 151 +++ .../FileListingTableCheckbox.tsx | 29 + client/modules/datafiles/src/index.ts | 7 + .../datafiles/src/lib/datafiles.module.css | 7 + .../datafiles/src/lib/datafiles.spec.tsx | 10 + .../modules/datafiles/src/lib/datafiles.tsx | 14 + client/modules/datafiles/tsconfig.json | 20 + client/modules/datafiles/tsconfig.lib.json | 23 + client/modules/datafiles/tsconfig.spec.json | 26 + client/modules/datafiles/vite.config.ts | 27 + client/package-lock.json | 935 +++++++++++++++++- client/package.json | 1 + client/src/datafiles/datafilesRouter.tsx | 223 +++++ .../datafiles/layouts/DataFilesBaseLayout.tsx | 40 + .../datafiles/layouts/FileListingLayout.tsx | 54 + .../src/datafiles/layouts/layout.module.css | 11 + .../layouts/nees/NeesDetailLayout.tsx | 10 + .../layouts/nees/NeesListingLayout.tsx | 5 + .../projects/ProjectCurationLayout.tsx | 10 + .../layouts/projects/ProjectDetailLayout.tsx | 11 + .../layouts/projects/ProjectListingLayout.tsx | 5 + .../projects/ProjectPipelineLayout.tsx | 5 + .../layouts/projects/ProjectPreviewLayout.tsx | 5 + .../layouts/projects/ProjectWorkdirLayout.tsx | 5 + .../published/PublishedDetailLayout.tsx | 11 + .../published/PublishedListingLayout.tsx | 5 + client/src/main.tsx | 51 +- client/src/styles.css | 74 ++ .../{router.tsx => workspaceRouter.tsx} | 0 client/tsconfig.base.json | 2 + client/vite.config.ts | 7 +- designsafe/apps/api/datafiles/handlers.py | 1 + .../datafiles/operations/agave_operations.py | 2 +- designsafe/apps/api/datafiles/views.py | 3 +- .../apps/data/templates/data/data_depot.j2 | 20 +- .../designsafe/apps/workspace/index.j2 | 6 +- designsafe/static/styles/main.css | 6 +- designsafe/templates/base.j2 | 8 + designsafe/urls.py | 1 + 85 files changed, 3435 insertions(+), 32 deletions(-) create mode 100644 client/modules/_common_components/.babelrc create mode 100644 client/modules/_common_components/.eslintrc.json create mode 100644 client/modules/_common_components/README.md create mode 100644 client/modules/_common_components/project.json create mode 100644 client/modules/_common_components/src/index.ts create mode 100644 client/modules/_common_components/src/lib/common-components.module.css create mode 100644 client/modules/_common_components/src/lib/common-components.spec.tsx create mode 100644 client/modules/_common_components/src/lib/common-components.tsx create mode 100644 client/modules/_common_components/tsconfig.json create mode 100644 client/modules/_common_components/tsconfig.lib.json create mode 100644 client/modules/_common_components/tsconfig.spec.json create mode 100644 client/modules/_common_components/vite.config.ts create mode 100644 client/modules/_hooks/src/datafiles/index.ts create mode 100644 client/modules/_hooks/src/datafiles/useConsumePostit.ts create mode 100644 client/modules/_hooks/src/datafiles/useFileCopy.ts create mode 100644 client/modules/_hooks/src/datafiles/useFileListing.ts create mode 100644 client/modules/_hooks/src/datafiles/useFileListingRouteParams.ts create mode 100644 client/modules/_hooks/src/datafiles/useFilePreview.ts create mode 100644 client/modules/_hooks/src/datafiles/usePathDisplayName.ts create mode 100644 client/modules/_hooks/src/datafiles/useSelectedFiles.ts create mode 100644 client/modules/_hooks/src/queryConfig.ts create mode 100644 client/modules/_hooks/src/useAuthenticatedUser.ts create mode 100644 client/modules/datafiles/.babelrc create mode 100644 client/modules/datafiles/.eslintrc.json create mode 100644 client/modules/datafiles/README.md create mode 100644 client/modules/datafiles/project.json create mode 100644 client/modules/datafiles/src/DatafilesBreadcrumb/DatafilesBreadcrumb.module.css create mode 100644 client/modules/datafiles/src/DatafilesBreadcrumb/DatafilesBreadcrumb.tsx create mode 100644 client/modules/datafiles/src/DatafilesModal/CopyModal/CopyModal.module.css create mode 100644 client/modules/datafiles/src/DatafilesModal/CopyModal/CopyModal.tsx create mode 100644 client/modules/datafiles/src/DatafilesModal/CopyModal/index.ts create mode 100644 client/modules/datafiles/src/DatafilesModal/DatafilesModal.tsx create mode 100644 client/modules/datafiles/src/DatafilesModal/PreviewModal/PreviewContent.tsx create mode 100644 client/modules/datafiles/src/DatafilesModal/PreviewModal/PreviewModal.module.css create mode 100644 client/modules/datafiles/src/DatafilesModal/PreviewModal/PreviewModal.tsx create mode 100644 client/modules/datafiles/src/DatafilesModal/PreviewModal/index.ts create mode 100644 client/modules/datafiles/src/DatafilesSideNav/DatafilesSideNav.module.css create mode 100644 client/modules/datafiles/src/DatafilesSideNav/DatafilesSideNav.tsx create mode 100644 client/modules/datafiles/src/DatafilesToolbar/DatafilesToolbar.module.css create mode 100644 client/modules/datafiles/src/DatafilesToolbar/DatafilesToolbar.tsx create mode 100644 client/modules/datafiles/src/FileListing/FileListing.module.css create mode 100644 client/modules/datafiles/src/FileListing/FileListing.tsx create mode 100644 client/modules/datafiles/src/FileListing/FileListingTable/FileListingTable.module.css create mode 100644 client/modules/datafiles/src/FileListing/FileListingTable/FileListingTable.tsx create mode 100644 client/modules/datafiles/src/FileListing/FileListingTable/FileListingTableCheckbox.tsx create mode 100644 client/modules/datafiles/src/index.ts create mode 100644 client/modules/datafiles/src/lib/datafiles.module.css create mode 100644 client/modules/datafiles/src/lib/datafiles.spec.tsx create mode 100644 client/modules/datafiles/src/lib/datafiles.tsx create mode 100644 client/modules/datafiles/tsconfig.json create mode 100644 client/modules/datafiles/tsconfig.lib.json create mode 100644 client/modules/datafiles/tsconfig.spec.json create mode 100644 client/modules/datafiles/vite.config.ts create mode 100644 client/src/datafiles/datafilesRouter.tsx create mode 100644 client/src/datafiles/layouts/DataFilesBaseLayout.tsx create mode 100644 client/src/datafiles/layouts/FileListingLayout.tsx create mode 100644 client/src/datafiles/layouts/layout.module.css create mode 100644 client/src/datafiles/layouts/nees/NeesDetailLayout.tsx create mode 100644 client/src/datafiles/layouts/nees/NeesListingLayout.tsx create mode 100644 client/src/datafiles/layouts/projects/ProjectCurationLayout.tsx create mode 100644 client/src/datafiles/layouts/projects/ProjectDetailLayout.tsx create mode 100644 client/src/datafiles/layouts/projects/ProjectListingLayout.tsx create mode 100644 client/src/datafiles/layouts/projects/ProjectPipelineLayout.tsx create mode 100644 client/src/datafiles/layouts/projects/ProjectPreviewLayout.tsx create mode 100644 client/src/datafiles/layouts/projects/ProjectWorkdirLayout.tsx create mode 100644 client/src/datafiles/layouts/published/PublishedDetailLayout.tsx create mode 100644 client/src/datafiles/layouts/published/PublishedListingLayout.tsx rename client/src/workspace/{router.tsx => workspaceRouter.tsx} (100%) diff --git a/client/.eslintrc.base.json b/client/.eslintrc.base.json index 8b6f768ab5..349442d356 100644 --- a/client/.eslintrc.base.json +++ b/client/.eslintrc.base.json @@ -10,7 +10,11 @@ "error", { "enforceBuildableLibDependency": true, - "allow": ["@client/hooks", "@client/test-fixtures"], + "allow": [ + "@client/hooks", + "@client/test-fixtures", + "@client/common-components" + ], "depConstraints": [ { "sourceTag": "*", diff --git a/client/modules/_common_components/.babelrc b/client/modules/_common_components/.babelrc new file mode 100644 index 0000000000..1ea870ead4 --- /dev/null +++ b/client/modules/_common_components/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/client/modules/_common_components/.eslintrc.json b/client/modules/_common_components/.eslintrc.json new file mode 100644 index 0000000000..3ebb9c6f3a --- /dev/null +++ b/client/modules/_common_components/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["plugin:@nx/react", "../../.eslintrc.base.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/client/modules/_common_components/README.md b/client/modules/_common_components/README.md new file mode 100644 index 0000000000..13e3786567 --- /dev/null +++ b/client/modules/_common_components/README.md @@ -0,0 +1,7 @@ +# common-components + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test common-components` to execute the unit tests via [Vitest](https://vitest.dev/). diff --git a/client/modules/_common_components/project.json b/client/modules/_common_components/project.json new file mode 100644 index 0000000000..87249c6262 --- /dev/null +++ b/client/modules/_common_components/project.json @@ -0,0 +1,20 @@ +{ + "name": "common-components", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "modules/_common_components/src", + "projectType": "library", + "tags": [], + "targets": { + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"] + }, + "test": { + "executor": "@nx/vite:test", + "outputs": ["{options.reportsDirectory}"], + "options": { + "reportsDirectory": "../../coverage/modules/_common_components" + } + } + } +} diff --git a/client/modules/_common_components/src/index.ts b/client/modules/_common_components/src/index.ts new file mode 100644 index 0000000000..e84894599d --- /dev/null +++ b/client/modules/_common_components/src/index.ts @@ -0,0 +1 @@ +export * from './lib/common-components'; diff --git a/client/modules/_common_components/src/lib/common-components.module.css b/client/modules/_common_components/src/lib/common-components.module.css new file mode 100644 index 0000000000..45c2aa47e9 --- /dev/null +++ b/client/modules/_common_components/src/lib/common-components.module.css @@ -0,0 +1,7 @@ +/* + * Replace this with your own classes + * + * e.g. + * .container { + * } +*/ diff --git a/client/modules/_common_components/src/lib/common-components.spec.tsx b/client/modules/_common_components/src/lib/common-components.spec.tsx new file mode 100644 index 0000000000..4c256d749e --- /dev/null +++ b/client/modules/_common_components/src/lib/common-components.spec.tsx @@ -0,0 +1,10 @@ +import { render } from '@testing-library/react'; + +import CommonComponents from './common-components'; + +describe('CommonComponents', () => { + it('should render successfully', () => { + const { baseElement } = render(); + expect(baseElement).toBeTruthy(); + }); +}); diff --git a/client/modules/_common_components/src/lib/common-components.tsx b/client/modules/_common_components/src/lib/common-components.tsx new file mode 100644 index 0000000000..6a8bc0826a --- /dev/null +++ b/client/modules/_common_components/src/lib/common-components.tsx @@ -0,0 +1,14 @@ +import styles from './common-components.module.css'; + +/* eslint-disable-next-line */ +export interface CommonComponentsProps {} + +export function CommonComponents(props: CommonComponentsProps) { + return ( +
+

Welcome to CommonComponents!

+
+ ); +} + +export default CommonComponents; diff --git a/client/modules/_common_components/tsconfig.json b/client/modules/_common_components/tsconfig.json new file mode 100644 index 0000000000..3c41f10fb2 --- /dev/null +++ b/client/modules/_common_components/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../tsconfig.base.json" +} diff --git a/client/modules/_common_components/tsconfig.lib.json b/client/modules/_common_components/tsconfig.lib.json new file mode 100644 index 0000000000..a6ed0a0c2b --- /dev/null +++ b/client/modules/_common_components/tsconfig.lib.json @@ -0,0 +1,23 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": [ + "node", + + "@nx/react/typings/cssmodule.d.ts", + "@nx/react/typings/image.d.ts" + ] + }, + "exclude": [ + "**/*.spec.ts", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx" + ], + "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] +} diff --git a/client/modules/_common_components/tsconfig.spec.json b/client/modules/_common_components/tsconfig.spec.json new file mode 100644 index 0000000000..3c002c215a --- /dev/null +++ b/client/modules/_common_components/tsconfig.spec.json @@ -0,0 +1,26 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ] + }, + "include": [ + "vite.config.ts", + "vitest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ] +} diff --git a/client/modules/_common_components/vite.config.ts b/client/modules/_common_components/vite.config.ts new file mode 100644 index 0000000000..7ae90ca9df --- /dev/null +++ b/client/modules/_common_components/vite.config.ts @@ -0,0 +1,27 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + +export default defineConfig({ + root: __dirname, + cacheDir: '../../node_modules/.vite/modules/_common_components', + + plugins: [react(), nxViteTsPaths()], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + test: { + globals: true, + cache: { dir: '../../node_modules/.vitest' }, + environment: 'jsdom', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: '../../coverage/modules/_common_components', + provider: 'v8', + }, + }, +}); diff --git a/client/modules/_hooks/src/apiClient.ts b/client/modules/_hooks/src/apiClient.ts index e053107efe..f3473c5525 100644 --- a/client/modules/_hooks/src/apiClient.ts +++ b/client/modules/_hooks/src/apiClient.ts @@ -1,6 +1,8 @@ -import axios from 'axios'; +import axios, { AxiosError } from 'axios'; -const apiClient = axios.create({ +export type TApiError = AxiosError<{ message?: string }>; + +export const apiClient = axios.create({ timeout: 30000, xsrfHeaderName: 'X-CSRFToken', xsrfCookieName: 'csrftoken', diff --git a/client/modules/_hooks/src/datafiles/index.ts b/client/modules/_hooks/src/datafiles/index.ts new file mode 100644 index 0000000000..226519dc12 --- /dev/null +++ b/client/modules/_hooks/src/datafiles/index.ts @@ -0,0 +1,20 @@ +export { default as useFileListing } from './useFileListing'; +export type { FileListingResponse, TFileListing } from './useFileListing'; +export { default as useFileListingRouteParams } from './useFileListingRouteParams'; +export { useFilePreview } from './useFilePreview'; +export type { + TPreviewParams, + TPreviewFileType, + TFilePreviewResponse, +} from './useFilePreview'; +export { useConsumePostit } from './useConsumePostit'; +export { + useSelectedFiles, + useSelectedFilesForSystem, +} from './useSelectedFiles'; + +export { useFileCopy } from './useFileCopy'; +export { + usePathDisplayName, + getSystemRootDisplayName, +} from './usePathDisplayName'; diff --git a/client/modules/_hooks/src/datafiles/useConsumePostit.ts b/client/modules/_hooks/src/datafiles/useConsumePostit.ts new file mode 100644 index 0000000000..ee1c448106 --- /dev/null +++ b/client/modules/_hooks/src/datafiles/useConsumePostit.ts @@ -0,0 +1,38 @@ +import { useQuery } from '@tanstack/react-query'; +import apiClient from '../apiClient'; +import { AxiosError } from 'axios'; +import { TQueryOptionExtras } from '../queryConfig'; + +type TPostitParams = { + href: string; + responseType?: 'text' | 'blob'; + queryOptions: TQueryOptionExtras; +}; + +async function fetchPostit({ + href, + signal, + responseType = 'text', +}: { + href: string; + signal: AbortSignal; + responseType: 'text' | 'blob'; +}) { + const resp = await apiClient.get(href, { + signal, + responseType, + }); + return resp.data; +} + +export function useConsumePostit({ + href, + responseType = 'text', + queryOptions, +}: TPostitParams) { + return useQuery({ + queryKey: ['datafiles', 'preview', 'postit', href], + queryFn: ({ signal }) => fetchPostit({ href, signal, responseType }), + ...queryOptions, + }); +} diff --git a/client/modules/_hooks/src/datafiles/useFileCopy.ts b/client/modules/_hooks/src/datafiles/useFileCopy.ts new file mode 100644 index 0000000000..3e84d25017 --- /dev/null +++ b/client/modules/_hooks/src/datafiles/useFileCopy.ts @@ -0,0 +1,27 @@ +import { useMutation } from '@tanstack/react-query'; +import apiClient from '../apiClient'; + +type TCopyParam = { api: string; system: string; path: string }; + +function copyFn(src: TCopyParam, dest: TCopyParam, doi?: string) { + return apiClient.put( + `/api/datafiles/${src.api}/private/copy/${src.system}/${src.path}/${ + doi ? `?doi=${doi}` : '' + }`, + { dest_system: dest.system, dest_path: dest.path } + ); +} + +export function useFileCopy() { + return useMutation({ + mutationFn: ({ + src, + dest, + doi, + }: { + src: TCopyParam; + dest: TCopyParam; + doi?: string; + }) => copyFn(src, dest, doi), + }); +} diff --git a/client/modules/_hooks/src/datafiles/useFileListing.ts b/client/modules/_hooks/src/datafiles/useFileListing.ts new file mode 100644 index 0000000000..7d8b43ef50 --- /dev/null +++ b/client/modules/_hooks/src/datafiles/useFileListing.ts @@ -0,0 +1,92 @@ +import { useInfiniteQuery } from '@tanstack/react-query'; +import apiClient from '../apiClient'; +import { AxiosError } from 'axios'; + +export type TFileListing = { + system: string; + name: string; + path: string; + format: 'folder' | 'raw'; + type: 'dir' | 'file'; + mimeType: string; + lastModified: string; + length: number; + permissions: string; +}; + +export type FileListingResponse = { + listing: TFileListing[]; + reachedEnd: boolean; + nextPageToken?: string; +}; + +async function getFileListing( + api: string, + system: string, + path: string, + scheme: string = 'private', + limit: number = 100, + page: number = 0, + nextPageToken: string | undefined, + { signal }: { signal: AbortSignal } +) { + const offset = page * limit; + + const res = await apiClient.get( + `/api/datafiles/${api}/${scheme}/listing/${system}/${path}`, + { signal, params: { offset, limit, nextPageToken } } + ); + return res.data; +} + +type TFileListingHookArgs = { + api: string; + system: string; + path: string; + scheme: string; + pageSize: number; +}; + +type TFileListingPageParam = { + page: number; + nextPageToken?: string; +}; + +function useFileListing({ + api, + system, + path, + scheme = 'private', + pageSize = 100, +}: TFileListingHookArgs) { + return useInfiniteQuery< + FileListingResponse, + AxiosError<{ message?: string }> + >({ + initialPageParam: 0, + queryKey: ['datafiles', 'fileListing', api, system, path], + queryFn: ({ pageParam, signal }) => + getFileListing( + api, + system || '-', // Backend throws errors if an empty string is passed. + path, + scheme, + pageSize, + (pageParam as TFileListingPageParam).page, + (pageParam as TFileListingPageParam).nextPageToken, + { + signal, + } + ), + getNextPageParam: (lastPage, allpages): TFileListingPageParam | null => { + return lastPage.listing.length >= pageSize + ? { page: allpages.length, nextPageToken: lastPage.nextPageToken } + : null; + }, + retry: (failureCount, error) => + // only retry on 5XX errors + (error.response?.status ?? 0) > 500 && failureCount < 3, + }); +} + +export default useFileListing; diff --git a/client/modules/_hooks/src/datafiles/useFileListingRouteParams.ts b/client/modules/_hooks/src/datafiles/useFileListingRouteParams.ts new file mode 100644 index 0000000000..6cfe9fdef6 --- /dev/null +++ b/client/modules/_hooks/src/datafiles/useFileListingRouteParams.ts @@ -0,0 +1,33 @@ +import { useMatches, useParams } from 'react-router-dom'; + +export function useFileListingRouteParams() { + // Parse routes of the form :api/:system/:path- + const { + api: paramApi, + system, + path, + } = useParams<{ api: string; system: string; path: string }>(); + const matches = useMatches(); + + // If API isn't passed as a param, read it from the route ID. + const api = paramApi ?? matches.slice(-1)[0].id; + + const scheme: 'public' | 'private' = [ + 'designsafe.storage.published', + 'designsafe.storage.community', + 'nees.public', + ].includes(system ?? '') + ? 'public' + : 'private'; + + return { + api, + scheme, + system: system ?? '', + path: encodeURIComponent(path ?? ''), + }; +} + +export type TFileListingParams = ReturnType; + +export default useFileListingRouteParams; diff --git a/client/modules/_hooks/src/datafiles/useFilePreview.ts b/client/modules/_hooks/src/datafiles/useFilePreview.ts new file mode 100644 index 0000000000..b0660b4732 --- /dev/null +++ b/client/modules/_hooks/src/datafiles/useFilePreview.ts @@ -0,0 +1,60 @@ +import { useQuery } from '@tanstack/react-query'; +import apiClient from '../apiClient'; +import { TQueryOptionExtras } from '../queryConfig'; + +export type TPreviewParams = { + api: string; + system: string; + scheme?: string; + path: string; +}; + +export type TPreviewFileType = + | 'text' + | 'image' + | 'object' + | 'ms-office' + | 'video' + | 'ipynb' + | 'box' + | 'other'; + +export type TFilePreviewResponse = { + href: string; + fileType: TPreviewFileType; + + fileMeta: Record; +}; + +async function getFilePreview({ + api, + system, + scheme, + path, + signal, +}: TPreviewParams & { signal: AbortSignal }) { + const res = await apiClient.get( + `/api/datafiles/${api}/${scheme}/preview/${system}/${path}`, + { signal } + ); + return res.data; +} + +export function useFilePreview({ + api, + system, + scheme = 'private', + path, + queryOptions, +}: TPreviewParams & { + queryOptions?: TQueryOptionExtras; +}) { + return useQuery({ + queryKey: ['datafiles', 'preview', api, system, path], + queryFn: ({ signal }) => + getFilePreview({ api, system, scheme, path, signal }), + refetchOnMount: false, + refetchOnWindowFocus: false, + ...queryOptions, + }); +} diff --git a/client/modules/_hooks/src/datafiles/usePathDisplayName.ts b/client/modules/_hooks/src/datafiles/usePathDisplayName.ts new file mode 100644 index 0000000000..c84b8c6cc2 --- /dev/null +++ b/client/modules/_hooks/src/datafiles/usePathDisplayName.ts @@ -0,0 +1,50 @@ +import { useCallback } from 'react'; +import { useAuthenticatedUser } from '../useAuthenticatedUser'; + +export function getSystemRootDisplayName(api: string, system: string): string { + if (api === 'googledrive') return 'Google Drive'; + if (api === 'box') return 'Box'; + if (api === 'dropbox') return 'Dropbox'; + return ( + { + 'designsafe.storage.default': 'My Data', + 'designsafe.storage.frontera.work': 'My Data (Work)', + 'designsafe.storage.community': 'Community Data', + }[system] ?? 'Data Files' + ); +} + +function _getPathDisplayName( + api: string, + system: string, + path: string, + username?: string +) { + const usernamePath = encodeURIComponent('/' + username); + + if (!path) return getSystemRootDisplayName(api, system); + if (api === 'googledrive' && !path) return 'Google Drive'; + if (api === 'dropbox' && !path) return 'Dropbox'; + if (api === 'box' && !path) return 'Box'; + + if (system === 'designsafe.storage.default' && path === usernamePath) { + return 'My Data'; + } + if (system === 'designsafe.storage.frontera.work' && path === usernamePath) { + return 'My Data (Work)'; + } + + return decodeURIComponent(path).split('/').slice(-1)[0] || 'Data Files'; +} + +export function usePathDisplayName() { + const { user } = useAuthenticatedUser(); + + const getPathDisplayName = useCallback( + (api: string, system: string, path: string) => + _getPathDisplayName(api, system, path, user?.username), + [user] + ); + + return getPathDisplayName; +} diff --git a/client/modules/_hooks/src/datafiles/useSelectedFiles.ts b/client/modules/_hooks/src/datafiles/useSelectedFiles.ts new file mode 100644 index 0000000000..27b5f371d9 --- /dev/null +++ b/client/modules/_hooks/src/datafiles/useSelectedFiles.ts @@ -0,0 +1,43 @@ +import { useCallback, useMemo } from 'react'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { TFileListing } from './useFileListing'; + +export function useSelectedFiles( + api: string, + system: string, + basePath: string +) { + const queryKey = useMemo( + () => ['selected-rows', api, system, basePath], + [api, system, basePath] + ); + const selectedRowsQuery = useQuery({ + queryKey, + initialData: [], + enabled: false, + }); + + const queryClient = useQueryClient(); + const setSelectedFiles = useCallback( + (selection: TFileListing[]) => + queryClient.setQueryData(queryKey, selection), + [queryKey, queryClient] + ); + + return { selectedFiles: selectedRowsQuery.data, setSelectedFiles }; +} + +export function useSelectedFilesForSystem(api: string, system: string) { + // Get all selected files matching a given system. + // Used when multiple listings can be present in a single page, e.g. publications. + const queryKey = ['selected-rows', api, system]; + const queryClient = useQueryClient(); + const selections = queryClient.getQueriesData({ queryKey }); + + const reducedSelections = useMemo(() => { + const allSelections: TFileListing[] = []; + selections.forEach((s) => s[1] && allSelections.push(...s[1])); + return allSelections; + }, [selections]); + return reducedSelections; +} diff --git a/client/modules/_hooks/src/index.ts b/client/modules/_hooks/src/index.ts index b8c8563bdd..b5c26de8b1 100644 --- a/client/modules/_hooks/src/index.ts +++ b/client/modules/_hooks/src/index.ts @@ -1 +1,3 @@ +export { useAuthenticatedUser } from './useAuthenticatedUser'; export * from './workspace'; +export * from './datafiles'; diff --git a/client/modules/_hooks/src/queryConfig.ts b/client/modules/_hooks/src/queryConfig.ts new file mode 100644 index 0000000000..0a7d7e1694 --- /dev/null +++ b/client/modules/_hooks/src/queryConfig.ts @@ -0,0 +1,8 @@ +import { UseQueryOptions } from '@tanstack/react-query'; +import { TApiError } from './apiClient'; + +// Convenience type for passing spread params to useQuery +export type TQueryOptionExtras = Omit< + UseQueryOptions, + 'queryKey' | 'queryFn' +>; diff --git a/client/modules/_hooks/src/useAuthenticatedUser.ts b/client/modules/_hooks/src/useAuthenticatedUser.ts new file mode 100644 index 0000000000..9e73f7457d --- /dev/null +++ b/client/modules/_hooks/src/useAuthenticatedUser.ts @@ -0,0 +1,16 @@ +declare global { + interface Window { + __authenticatedUser__?: { + username: string; + firstName: string; + lastName: string; + email: string; + }; + } +} + +export const useAuthenticatedUser = () => { + return { + user: window.__authenticatedUser__, + }; +}; diff --git a/client/modules/datafiles/.babelrc b/client/modules/datafiles/.babelrc new file mode 100644 index 0000000000..1ea870ead4 --- /dev/null +++ b/client/modules/datafiles/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/client/modules/datafiles/.eslintrc.json b/client/modules/datafiles/.eslintrc.json new file mode 100644 index 0000000000..3ebb9c6f3a --- /dev/null +++ b/client/modules/datafiles/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["plugin:@nx/react", "../../.eslintrc.base.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/client/modules/datafiles/README.md b/client/modules/datafiles/README.md new file mode 100644 index 0000000000..55694b6770 --- /dev/null +++ b/client/modules/datafiles/README.md @@ -0,0 +1,7 @@ +# datafiles + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test datafiles` to execute the unit tests via [Vitest](https://vitest.dev/). diff --git a/client/modules/datafiles/project.json b/client/modules/datafiles/project.json new file mode 100644 index 0000000000..49dae182ca --- /dev/null +++ b/client/modules/datafiles/project.json @@ -0,0 +1,20 @@ +{ + "name": "datafiles", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "modules/datafiles/src", + "projectType": "library", + "tags": [], + "targets": { + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"] + }, + "test": { + "executor": "@nx/vite:test", + "outputs": ["{options.reportsDirectory}"], + "options": { + "reportsDirectory": "../../coverage/modules/datafiles" + } + } + } +} diff --git a/client/modules/datafiles/src/DatafilesBreadcrumb/DatafilesBreadcrumb.module.css b/client/modules/datafiles/src/DatafilesBreadcrumb/DatafilesBreadcrumb.module.css new file mode 100644 index 0000000000..a8ceb6c06c --- /dev/null +++ b/client/modules/datafiles/src/DatafilesBreadcrumb/DatafilesBreadcrumb.module.css @@ -0,0 +1,9 @@ +.datafilesBreadcrumb { + padding: 10px; + background-color: #f5f5f5; +} + +:global(.breadcrumb-link) { + text-decoration: none !important; + color: #337ab7 !important; +} diff --git a/client/modules/datafiles/src/DatafilesBreadcrumb/DatafilesBreadcrumb.tsx b/client/modules/datafiles/src/DatafilesBreadcrumb/DatafilesBreadcrumb.tsx new file mode 100644 index 0000000000..cc01ec4d8e --- /dev/null +++ b/client/modules/datafiles/src/DatafilesBreadcrumb/DatafilesBreadcrumb.tsx @@ -0,0 +1,86 @@ +import { Breadcrumb, BreadcrumbProps } from 'antd'; +import React from 'react'; +import styles from './DatafilesBreadcrumb.module.css'; +import { getSystemRootDisplayName, useAuthenticatedUser } from '@client/hooks'; + +function getPathRoutes( + baseRoute: string, + path: string = '', + systemRoot: string = '', + systemRootAlias?: string +) { + const pathComponents = decodeURIComponent(path.replace(systemRoot, '')) + .split('/') + .filter((p) => !!p); + + const systemRootBreadcrumb = { + path: `${baseRoute}/${systemRoot}`, + title: systemRootAlias ?? 'Data Files', + }; + + return [ + systemRootBreadcrumb, + ...pathComponents.map((comp, i) => ({ + title: comp, + path: `${baseRoute}/${systemRoot}${encodeURIComponent( + '/' + pathComponents.slice(0, i + 1).join('/') + )}`, + })), + ]; +} + +export const DatafilesBreadcrumb: React.FC< + { + initialBreadcrumbs: { title: string; path: string }[]; + path: string; + baseRoute: string; + systemRoot: string; + systemRootAlias?: string; + } & BreadcrumbProps +> = ({ + initialBreadcrumbs, + path, + baseRoute, + systemRoot, + systemRootAlias, + ...props +}) => { + const breadcrumbItems = [ + ...initialBreadcrumbs, + ...getPathRoutes(baseRoute, path, systemRoot, systemRootAlias), + ]; + + return ( + + ); +}; + +function isUserHomeSystem(system: string) { + return [ + 'designsafe.storage.default', + 'designsafe.storage.frontera.work', + ].includes(system); +} + +export const BaseFileListingBreadcrumb: React.FC< + { api: string; system: string; path: string } & BreadcrumbProps +> = ({ api, system, path, ...props }) => { + const { user } = useAuthenticatedUser(); + + return ( + + ); +}; diff --git a/client/modules/datafiles/src/DatafilesModal/CopyModal/CopyModal.module.css b/client/modules/datafiles/src/DatafilesModal/CopyModal/CopyModal.module.css new file mode 100644 index 0000000000..0d3f1691ea --- /dev/null +++ b/client/modules/datafiles/src/DatafilesModal/CopyModal/CopyModal.module.css @@ -0,0 +1,35 @@ +.copyModalContent { + display: flex; + max-height: 60vh; + min-height: 400px; + gap: 50px; +} + +.copyModalContent td { + vertical-align: middle; +} + +.srcFilesSection { + flex: 1; + overflow: auto; + border: 1px solid #707070; +} + +.srcFilesTable { + height: 100%; +} + +.destFilesSection { + display: flex; + flex: 1; + flex-direction: column; + overflow: auto; + border: 1px solid #707070; +} + +.destFilesTableContainer { + display: flex; + flex: 1; + flex-direction: column; + overflow: auto; +} diff --git a/client/modules/datafiles/src/DatafilesModal/CopyModal/CopyModal.tsx b/client/modules/datafiles/src/DatafilesModal/CopyModal/CopyModal.tsx new file mode 100644 index 0000000000..ffa8e16615 --- /dev/null +++ b/client/modules/datafiles/src/DatafilesModal/CopyModal/CopyModal.tsx @@ -0,0 +1,209 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { TModalChildren } from '../DatafilesModal'; +import { Button, Modal, Table } from 'antd'; +import { + useAuthenticatedUser, + useFileCopy, + usePathDisplayName, + useSelectedFiles, +} from '@client/hooks'; +import { + FileListingTable, + TFileListingColumns, +} from '../../FileListing/FileListingTable/FileListingTable'; +import { BaseFileListingBreadcrumb } from '../../DatafilesBreadcrumb/DatafilesBreadcrumb'; +import styles from './CopyModal.module.css'; +import { toBytes } from '../../FileListing/FileListing'; + +const SelectedFilesColumns: TFileListingColumns = [ + { + title: 'Files/Folders to Copy', + dataIndex: 'name', + }, + { + title: , + dataIndex: 'length', + render: (value) => toBytes(value), + }, +]; + +const DestHeaderTitle: React.FC<{ + api: string; + system: string; + path: string; +}> = ({ api, system, path }) => { + const getPathName = usePathDisplayName(); + return ( + + +    + + {getPathName(api, system, path)} + + ); +}; + +function getDestFilesColumns( + api: string, + system: string, + path: string, + mutationCallback: (path: string) => void, + navCallback: (path: string) => void +): TFileListingColumns { + return [ + { + title: , + dataIndex: 'name', + ellipsis: true, + + render: (data, record) => ( + + ), + }, + { + dataIndex: 'path', + align: 'end', + title: ( + + ), + render: (_, record) => ( + + ), + }, + ]; +} + +export const CopyModal: React.FC<{ + api: string; + system: string; + path: string; + children: TModalChildren; +}> = ({ api, system, path, children }) => { + const [isModalOpen, setIsModalOpen] = useState(false); + + const showModal = () => setIsModalOpen(true); + const handleClose = () => setIsModalOpen(false); + + const { selectedFiles } = useSelectedFiles(api, system, path); + const { user } = useAuthenticatedUser(); + + const defaultDestParams = useMemo( + () => ({ + destApi: 'tapis', + destSystem: 'designsafe.storage.default', + destPath: encodeURIComponent('/' + user?.username), + }), + [user] + ); + + const [dest, setDest] = useState(defaultDestParams); + const { destApi, destSystem, destPath } = dest; + useEffect(() => setDest(defaultDestParams), [isModalOpen, defaultDestParams]); + + const navCallback = useCallback( + (path: string) => { + const newPath = path.split('/').slice(-1)[0]; + setDest({ ...dest, destPath: newPath }); + }, + [dest] + ); + const { mutate } = useFileCopy(); + + const mutateCallback = useCallback( + (dPath: string) => { + selectedFiles.forEach((f) => + mutate({ + src: { api, system, path: encodeURIComponent(f.path) }, + dest: { api: destApi, system: destSystem, path: dPath }, + }) + ); + handleClose(); + }, + [selectedFiles, mutate, destApi, destSystem, api, system] + ); + + const DestFilesColumns = useMemo( + () => + getDestFilesColumns( + destApi, + destSystem, + destPath, + (dPath: string) => mutateCallback(dPath), + navCallback + ), + [navCallback, destApi, destSystem, destPath, mutateCallback] + ); + + return ( + <> + {React.createElement(children, { onClick: showModal })} + Copy Files} + footer={null} + > +
+
+ record.path} + scroll={{ y: '100%' }} + /> + +
+ { + return ( + + ); + }} + /> +
+ listing.filter((f) => f.type === 'dir')} + scroll={undefined} + /> +
+
+ + + + ); +}; diff --git a/client/modules/datafiles/src/DatafilesModal/CopyModal/index.ts b/client/modules/datafiles/src/DatafilesModal/CopyModal/index.ts new file mode 100644 index 0000000000..5ea378f9ff --- /dev/null +++ b/client/modules/datafiles/src/DatafilesModal/CopyModal/index.ts @@ -0,0 +1 @@ +export * from './CopyModal'; diff --git a/client/modules/datafiles/src/DatafilesModal/DatafilesModal.tsx b/client/modules/datafiles/src/DatafilesModal/DatafilesModal.tsx new file mode 100644 index 0000000000..4bf6bd3786 --- /dev/null +++ b/client/modules/datafiles/src/DatafilesModal/DatafilesModal.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { PreviewModal } from './PreviewModal'; +import { CopyModal } from './CopyModal'; + +export type TModalChildren = (props: { + onClick: React.MouseEventHandler; +}) => React.ReactElement; + +const DatafilesModal = () => Data Files Modal Root; + +DatafilesModal.Preview = PreviewModal; +DatafilesModal.Copy = CopyModal; + +export default DatafilesModal; diff --git a/client/modules/datafiles/src/DatafilesModal/PreviewModal/PreviewContent.tsx b/client/modules/datafiles/src/DatafilesModal/PreviewModal/PreviewContent.tsx new file mode 100644 index 0000000000..41a61ae3aa --- /dev/null +++ b/client/modules/datafiles/src/DatafilesModal/PreviewModal/PreviewContent.tsx @@ -0,0 +1,74 @@ +import { useConsumePostit, TPreviewFileType } from '@client/hooks'; +import { Spin } from 'antd'; +import React, { useState } from 'react'; +import styles from './PreviewModal.module.css'; + +export const PreviewSpinner: React.FC = () => ( + +); + +export type TPreviewContent = React.FC<{ + href: string; + fileType: TPreviewFileType; +}>; +export const PreviewContent: TPreviewContent = ({ href, fileType }) => { + const [iframeLoading, setIframeLoading] = useState(true); + + const { data: PostitData, isLoading: isConsumingPostit } = useConsumePostit({ + href, + responseType: fileType === 'video' ? 'blob' : 'text', + queryOptions: { + enabled: (!!href && fileType === 'text') || fileType === 'video', + }, + }); + + if (isConsumingPostit) return ; + + switch (fileType) { + case 'text': + return ( + PostitData && ( +
+
{PostitData as string}
+
+ ) + ); + case 'video': + return ( + PostitData && ( +
+ +
+ ) + ); + case 'image': + return ( +
+ {iframeLoading && } + {href} setIframeLoading(false)} /> +
+ ); + case 'box': + case 'ms-office': + case 'ipynb': + case 'object': + return ( +
+ {iframeLoading && } + +
+ ); + default: + return Error.; + } +}; diff --git a/client/modules/datafiles/src/DatafilesModal/PreviewModal/PreviewModal.module.css b/client/modules/datafiles/src/DatafilesModal/PreviewModal/PreviewModal.module.css new file mode 100644 index 0000000000..1e5a1b0a3a --- /dev/null +++ b/client/modules/datafiles/src/DatafilesModal/PreviewModal/PreviewModal.module.css @@ -0,0 +1,29 @@ +.spinner { + position: absolute; + top: 50%; + width: 100%; +} + +.modalContentContainer { + min-height: 200px; + max-height: 75vh; + padding-top: 10px; + position: relative; +} + +.previewContainer { + position: relative; + max-height: 50vh; + min-height: 200px; + overflow: auto; +} + +.previewContainer iframe { + width: 100%; + height: 50vh; + border: 0; +} +.previewContainer video, +.previewContainer img { + width: 100%; +} diff --git a/client/modules/datafiles/src/DatafilesModal/PreviewModal/PreviewModal.tsx b/client/modules/datafiles/src/DatafilesModal/PreviewModal/PreviewModal.tsx new file mode 100644 index 0000000000..c3dd3bd6a8 --- /dev/null +++ b/client/modules/datafiles/src/DatafilesModal/PreviewModal/PreviewModal.tsx @@ -0,0 +1,99 @@ +import { useQueryClient } from '@tanstack/react-query'; +import { useFilePreview } from '@client/hooks'; +import { Button, Modal } from 'antd'; +import React, { useCallback, useState } from 'react'; +import styles from './PreviewModal.module.css'; +import { TModalChildren } from '../DatafilesModal'; +import { PreviewSpinner, PreviewContent } from './PreviewContent'; + +export const PreviewModalBody: React.FC<{ + isOpen: boolean; + api: string; + system: string; + scheme?: string; + path: string; + handleCancel: () => void; +}> = ({ isOpen, api, system, scheme, path, handleCancel }) => { + /* + Typically modals are rendered in the same component as the button that manages the + open/closed state. The modal body is exported separately for file previews, since + the modal might be rendered hundreds of times in a listing and impact performance. + */ + const queryClient = useQueryClient(); + const { data, isLoading } = useFilePreview({ + api, + system, + scheme, + path, + queryOptions: { enabled: isOpen }, + }); + + const handleClose = useCallback(() => { + // Flush queries on close to prevent stale postits being read from cache. + queryClient.removeQueries({ queryKey: ['datafiles', 'preview'] }); + handleCancel(); + }, [handleCancel, queryClient]); + + return ( + File Preview: {path}} + width="60%" + open={isOpen} + footer={() => ( + + )} + onCancel={handleClose} + > +
+ {isLoading && } + {data && isOpen && ( + + )} +
+
+ ); +}; + +type TPreviewModal = React.FC<{ + api: string; + system: string; + scheme?: string; + path: string; + children: TModalChildren; +}>; +export const PreviewModal: TPreviewModal = ({ + api, + system, + scheme, + path, + children, +}) => { + const [isModalOpen, setIsModalOpen] = useState(false); + + const showModal = () => { + setIsModalOpen(true); + }; + + const handleCancel = () => { + setIsModalOpen(false); + }; + + return ( + <> + {React.createElement(children, { onClick: showModal })} + + + ); +}; diff --git a/client/modules/datafiles/src/DatafilesModal/PreviewModal/index.ts b/client/modules/datafiles/src/DatafilesModal/PreviewModal/index.ts new file mode 100644 index 0000000000..1ae2f49721 --- /dev/null +++ b/client/modules/datafiles/src/DatafilesModal/PreviewModal/index.ts @@ -0,0 +1 @@ +export * from './PreviewModal'; diff --git a/client/modules/datafiles/src/DatafilesSideNav/DatafilesSideNav.module.css b/client/modules/datafiles/src/DatafilesSideNav/DatafilesSideNav.module.css new file mode 100644 index 0000000000..281e8c997a --- /dev/null +++ b/client/modules/datafiles/src/DatafilesSideNav/DatafilesSideNav.module.css @@ -0,0 +1,35 @@ +:root { + --nav-link-bg-color--unselected: #eee; + --nav-link-bg-color--selected: transparent; + + --nav-link-hover-color--selected: #e1e1e1; + --nav-link-hover-color--unselected: transparent; + + --nav-link-l-border-color--selected: #337ab7; + --nav-link-l-border-color--unselected: #ccc; +} + +a.navLink { + color: #555555; + text-decoration: none; +} +a.navLink > div { + background-color: var(--nav-link-bg-color--unselected); + height: 40px; + line-height: 40px; + + border-left: 5px solid var(--nav-link-bg-color--unselected); + padding-left: 10px; + padding-right: 15px; +} + +.navLink:global(.active) > div { + background-color: transparent; + border-left: 5px solid var(--nav-link-l-border-color--selected); + padding-left: 10px; +} + +a.navLink:not(:global(.active)):hover > div { + background-color: var(--nav-link-hover-color--selected); + border-left: 5px solid var(--nav-link-l-border-color--unselected); +} diff --git a/client/modules/datafiles/src/DatafilesSideNav/DatafilesSideNav.tsx b/client/modules/datafiles/src/DatafilesSideNav/DatafilesSideNav.tsx new file mode 100644 index 0000000000..815e748c5b --- /dev/null +++ b/client/modules/datafiles/src/DatafilesSideNav/DatafilesSideNav.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { NavLink } from 'react-router-dom'; +import styles from './DatafilesSideNav.module.css'; +import { useAuthenticatedUser } from '@client/hooks'; + +const DataFilesNavLink: React.FC> = ({ + to, + children, +}) => { + return ( +
  • + +
    {children}
    +
    +
  • + ); +}; + +export const DatafilesSideNav: React.FC = () => { + const { user } = useAuthenticatedUser(); + return ( +
      + {user && ( + <> + + My Data + + + My Data (Work) + + My Projects + + Shared with Me + + +
      + + Box.com + Dropbox.com + Google Drive + +
      + + )} + + + Published + + +
      + + + Published (NEES) + + + Community Data + +
    + ); +}; diff --git a/client/modules/datafiles/src/DatafilesToolbar/DatafilesToolbar.module.css b/client/modules/datafiles/src/DatafilesToolbar/DatafilesToolbar.module.css new file mode 100644 index 0000000000..c71cb5c3f8 --- /dev/null +++ b/client/modules/datafiles/src/DatafilesToolbar/DatafilesToolbar.module.css @@ -0,0 +1,25 @@ +.toolbarRoot { + background-color: #f0f0f0; + border: 1px solid #dddddd; + display: flex; + flex-flow: row nowrap; + overflow-x: auto; + align-items: center; + padding: 5px; + justify-content: space-between; +} + +.toolbarButtonContainer { + display: flex; + gap: 5px; +} + +.toolbarButton { + display: flex; + flex-direction: column; + gap: 3px; + align-items: center; + height: fit-content; + font-size: 12px; + padding-top: 10px; +} diff --git a/client/modules/datafiles/src/DatafilesToolbar/DatafilesToolbar.tsx b/client/modules/datafiles/src/DatafilesToolbar/DatafilesToolbar.tsx new file mode 100644 index 0000000000..4115cf125c --- /dev/null +++ b/client/modules/datafiles/src/DatafilesToolbar/DatafilesToolbar.tsx @@ -0,0 +1,79 @@ +import React, { useMemo } from 'react'; +import styles from './DatafilesToolbar.module.css'; +import { + useAuthenticatedUser, + useFileListingRouteParams, + useSelectedFiles, +} from '@client/hooks'; +import DatafilesModal from '../DatafilesModal/DatafilesModal'; +import { Button, ButtonProps, ConfigProvider, ThemeConfig } from 'antd'; + +const toolbarTheme: ThemeConfig = { + components: { + Button: { + colorPrimaryHover: 'rgba(0, 0, 0, 0.88)', + }, + }, +}; +const ToolbarButton: React.FC = (props) => { + return ( + + + ), + }, + { + title: 'Size', + dataIndex: 'length', + render: (d) => toBytes(d), + }, + { + title: 'Last Modified', + dataIndex: 'lastModified', + ellipsis: true, + render: (d) => new Date(d).toLocaleString(), + }, + ], + [setPreviewModalState] + ); + + return ( + <> + + {previewModalState.path && ( + setPreviewModalState({ isOpen: false })} + /> + )} + + ); +}; diff --git a/client/modules/datafiles/src/FileListing/FileListingTable/FileListingTable.module.css b/client/modules/datafiles/src/FileListing/FileListingTable/FileListingTable.module.css new file mode 100644 index 0000000000..7351fa651a --- /dev/null +++ b/client/modules/datafiles/src/FileListing/FileListingTable/FileListingTable.module.css @@ -0,0 +1,122 @@ +.listing-table-base { + height: 100%; +} + +.checkbox-wrapper { + max-width: 100%; + font-weight: bold; + margin: 0; + padding: 0; + color: rgba(0, 0, 0, 0.88); + font-size: 14px; + line-height: 1.5714285714285714; + list-style: none; + display: inline-flex; + align-items: baseline; + cursor: pointer; + box-sizing: border-box; +} +.checkbox-wrapper::after { + display: inline-block; + width: 0; + overflow: hidden; + content: '\a0'; +} + +.checkbox { + font-weight: bold; + margin: 0; + padding: 0; + color: rgba(0, 0, 0, 0.88); + font-size: 14px; + line-height: 1; + list-style: none; + position: relative; + white-space: nowrap; + cursor: pointer; + border-radius: 0; + align-self: center; + box-sizing: border-box; +} + +.checkbox-inner { + font-weight: bold; + color: rgba(0, 0, 0, 0.88); + font-size: 14px; + line-height: 1; + list-style: none; + white-space: nowrap; + cursor: pointer; + box-sizing: border-box; + display: block; + width: 16px; + height: 16px; + direction: ltr; + background-color: #ffffff; + border: 1px solid #d9d9d9; + border-radius: 0; + border-collapse: separate; + transition: all 0.3s; +} + +.checkbox-inner::after { + box-sizing: border-box; + position: absolute; + top: 50%; + inset-inline-start: 25%; + display: table; + width: 5.7142857142857135px; + height: 9.142857142857142px; + border: 2px solid #fff; + border-top: 0; + border-inline-start: 0; + transform: rotate(45deg) scale(0) translate(-50%, -50%); + opacity: 0; + content: ''; + transition: all 0.1s cubic-bezier(0.71, -0.46, 0.88, 0.6), opacity 0.1s; +} + +.checkbox-input { + list-style: none; + white-space: nowrap; + color: inherit; + vertical-align: baseline; + font-family: Roboto, 'Helvetica Neue', sans-serif; + font-size: 100%; + padding: 0; + line-height: normal; + box-sizing: border-box; + position: absolute; + inset: 0; + z-index: 1; + cursor: pointer; + opacity: 0; + margin: 0; +} + +.checkbox-wrapper-checked { + max-width: 100%; + font-weight: bold; + margin: 0; + padding: 0; + color: rgba(0, 0, 0, 0.88); + font-size: 14px; + line-height: 1.5714285714285714; + list-style: none; + display: inline-flex; + align-items: baseline; + cursor: pointer; + box-sizing: border-box; + margin-inline-start: 0; +} + +.checkbox-checked .checkbox-inner::after { + opacity: 1; + transform: rotate(45deg) scale(1) translate(-50%, -50%); + transition: all 0.2s cubic-bezier(0.12, 0.4, 0.29, 1.46) 0.1s; +} + +.checkbox-checked .checkbox-inner { + background-color: #337ab7; + border-color: #337ab7; +} diff --git a/client/modules/datafiles/src/FileListing/FileListingTable/FileListingTable.tsx b/client/modules/datafiles/src/FileListing/FileListingTable/FileListingTable.tsx new file mode 100644 index 0000000000..61ed0b01b0 --- /dev/null +++ b/client/modules/datafiles/src/FileListing/FileListingTable/FileListingTable.tsx @@ -0,0 +1,151 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import styles from './FileListingTable.module.css'; +import { Table, TableColumnType, TableProps } from 'antd'; +import { useFileListing, TFileListing, useSelectedFiles } from '@client/hooks'; +import { FileListingTableCheckbox } from './FileListingTableCheckbox'; + +type TableRef = { + nativeElement: HTMLDivElement; + scrollTo: (config: { index?: number; key?: React.Key; top?: number }) => void; +}; + +export type TFileListingColumns = (TableColumnType & { + dataIndex: keyof TFileListing; +})[]; + +export const FileListingTable: React.FC< + { + api: string; + system?: string; + path?: string; + scheme?: string; + columns: TFileListingColumns; + filterFn?: (listing: TFileListing[]) => TFileListing[]; + className?: string; + } & Omit +> = ({ + api, + system, + path = '', + scheme = 'private', + filterFn, + columns, + className, + ...props +}) => { + const limit = 100; + const [scrollElement, setScrollElement] = useState( + undefined + ); + + /* FETCH FILE LISTINGS */ + const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } = + useFileListing({ + api, + system: system ?? '-', + path: path ?? '', + scheme, + pageSize: limit, + }); + + const combinedListing = useMemo(() => { + const cl: TFileListing[] = []; + data?.pages.forEach((page) => cl.push(...page.listing)); + if (filterFn) { + return filterFn(cl); + } + return cl; + }, [data, filterFn]); + + /* HANDLE FILE SELECTION */ + const { selectedFiles, setSelectedFiles } = useSelectedFiles( + api, + system ?? '-', + path + ); + const onSelectionChange = useCallback( + (_: React.Key[], selection: TFileListing[]) => setSelectedFiles(selection), + [setSelectedFiles] + ); + const selectedRowKeys = useMemo( + () => selectedFiles.map((s) => s.path), + [selectedFiles] + ); + + /* HANDLE INFINITE SCROLL */ + const scrollRefCallback = useCallback( + (node: TableRef) => { + if (node !== null) { + const lastRow = node.nativeElement.querySelectorAll( + '.ant-table-row:last-child' + )[0]; + setScrollElement(lastRow); + } + }, + [setScrollElement] + ); + useEffect(() => { + // Set and clean up scroll event listener on the table ref. + const observer = new IntersectionObserver((entries) => { + // Fetch the next page when the final listing item enters the viewport. + entries.forEach((entry) => { + if ( + entry.isIntersecting && + hasNextPage && + !(isFetchingNextPage || isLoading) + ) { + fetchNextPage(); + } + }); + }); + scrollElement && observer.observe(scrollElement); + + return () => { + observer.disconnect(); + }; + }, [ + scrollElement, + hasNextPage, + fetchNextPage, + isFetchingNextPage, + isLoading, + ]); + + /* RENDER THE TABLE */ + return ( +
    0 ? 'table--pull-spinner-bottom' : '' + } ${className ?? ''}`} + rowSelection={{ + type: 'checkbox', + onChange: onSelectionChange, + selectedRowKeys, + renderCell: (checked, _rc, _idx, node) => ( + + ), + }} + scroll={{ y: '100%', x: '500px' }} // set to undefined to disable sticky header + columns={columns} + rowKey={(record) => record.path} + dataSource={combinedListing} + pagination={false} + loading={isLoading || isFetchingNextPage} + locale={{ + emptyText: + isLoading || isFetchingNextPage ? ( +
     
    + ) : ( +
    Placeholder for empty data.
    + ), + }} + {...props} + > + placeholder +
    + ); +}; diff --git a/client/modules/datafiles/src/FileListing/FileListingTable/FileListingTableCheckbox.tsx b/client/modules/datafiles/src/FileListing/FileListingTable/FileListingTableCheckbox.tsx new file mode 100644 index 0000000000..970bb4995f --- /dev/null +++ b/client/modules/datafiles/src/FileListing/FileListingTable/FileListingTableCheckbox.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import styles from './FileListingTable.module.css'; + +export const FileListingTableCheckbox: React.FC<{ + checked?: boolean; + onChange: React.ChangeEventHandler; +}> = ({ checked, onChange }) => { + /* + This checkbox component is more barebones than the checkbox exported by Ant, + and is suited to large file listings where it will be rerendered extensively. + */ + return ( + + ); +}; diff --git a/client/modules/datafiles/src/index.ts b/client/modules/datafiles/src/index.ts new file mode 100644 index 0000000000..d3397d03df --- /dev/null +++ b/client/modules/datafiles/src/index.ts @@ -0,0 +1,7 @@ +export * from './lib/datafiles'; + +export * from './DatafilesSideNav/DatafilesSideNav'; +export * from './FileListing/FileListing'; +export { default as DatafilesModal } from './DatafilesModal/DatafilesModal'; +export * from './DatafilesToolbar/DatafilesToolbar'; +export * from './DatafilesBreadcrumb/DatafilesBreadcrumb'; diff --git a/client/modules/datafiles/src/lib/datafiles.module.css b/client/modules/datafiles/src/lib/datafiles.module.css new file mode 100644 index 0000000000..45c2aa47e9 --- /dev/null +++ b/client/modules/datafiles/src/lib/datafiles.module.css @@ -0,0 +1,7 @@ +/* + * Replace this with your own classes + * + * e.g. + * .container { + * } +*/ diff --git a/client/modules/datafiles/src/lib/datafiles.spec.tsx b/client/modules/datafiles/src/lib/datafiles.spec.tsx new file mode 100644 index 0000000000..c9dd2004cb --- /dev/null +++ b/client/modules/datafiles/src/lib/datafiles.spec.tsx @@ -0,0 +1,10 @@ +import { render } from '@testing-library/react'; + +import Datafiles from './datafiles'; + +describe('Datafiles', () => { + it('should render successfully', () => { + const { baseElement } = render(); + expect(baseElement).toBeTruthy(); + }); +}); diff --git a/client/modules/datafiles/src/lib/datafiles.tsx b/client/modules/datafiles/src/lib/datafiles.tsx new file mode 100644 index 0000000000..9fe1c86d07 --- /dev/null +++ b/client/modules/datafiles/src/lib/datafiles.tsx @@ -0,0 +1,14 @@ +import styles from './datafiles.module.css'; + +/* eslint-disable-next-line */ +export interface DatafilesProps {} + +export function Datafiles(props: DatafilesProps) { + return ( +
    +

    Welcome to Datafiles!

    +
    + ); +} + +export default Datafiles; diff --git a/client/modules/datafiles/tsconfig.json b/client/modules/datafiles/tsconfig.json new file mode 100644 index 0000000000..3c41f10fb2 --- /dev/null +++ b/client/modules/datafiles/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../tsconfig.base.json" +} diff --git a/client/modules/datafiles/tsconfig.lib.json b/client/modules/datafiles/tsconfig.lib.json new file mode 100644 index 0000000000..a6ed0a0c2b --- /dev/null +++ b/client/modules/datafiles/tsconfig.lib.json @@ -0,0 +1,23 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": [ + "node", + + "@nx/react/typings/cssmodule.d.ts", + "@nx/react/typings/image.d.ts" + ] + }, + "exclude": [ + "**/*.spec.ts", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx" + ], + "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] +} diff --git a/client/modules/datafiles/tsconfig.spec.json b/client/modules/datafiles/tsconfig.spec.json new file mode 100644 index 0000000000..3c002c215a --- /dev/null +++ b/client/modules/datafiles/tsconfig.spec.json @@ -0,0 +1,26 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ] + }, + "include": [ + "vite.config.ts", + "vitest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ] +} diff --git a/client/modules/datafiles/vite.config.ts b/client/modules/datafiles/vite.config.ts new file mode 100644 index 0000000000..699fc113b5 --- /dev/null +++ b/client/modules/datafiles/vite.config.ts @@ -0,0 +1,27 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + +export default defineConfig({ + root: __dirname, + cacheDir: '../../node_modules/.vite/modules/datafiles', + + plugins: [react(), nxViteTsPaths()], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + test: { + globals: true, + cache: { dir: '../../node_modules/.vitest' }, + environment: 'jsdom', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: '../../coverage/modules/datafiles', + provider: 'v8', + }, + }, +}); diff --git a/client/package-lock.json b/client/package-lock.json index f53fc78883..bb94c2091a 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@swc/helpers": "~0.5.2", "@tanstack/react-query": "^5.15.0", + "antd": "^5.13.2", "axios": "^1.6.3", "react": "18.2.0", "react-dom": "18.2.0", @@ -80,6 +81,71 @@ "node": ">=6.0.0" } }, + "node_modules/@ant-design/colors": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.0.2.tgz", + "integrity": "sha512-7KJkhTiPiLHSu+LmMJnehfJ6242OCxSlR3xHVBecYxnMW8MS/878NXct1GqYARyL59fyeFdKRxXTfvR9SnDgJg==", + "dependencies": { + "@ctrl/tinycolor": "^3.6.1" + } + }, + "node_modules/@ant-design/cssinjs": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-1.18.2.tgz", + "integrity": "sha512-514V9rjLaFYb3v4s55/8bg2E6fb81b99s3crDZf4nSwtiDLLXs8axnIph+q2TVkY2hbJPZOn/cVsVcnLkzFy7w==", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@emotion/hash": "^0.8.0", + "@emotion/unitless": "^0.7.5", + "classnames": "^2.3.1", + "csstype": "^3.1.3", + "rc-util": "^5.35.0", + "stylis": "^4.0.13" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/icons": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.2.6.tgz", + "integrity": "sha512-4wn0WShF43TrggskBJPRqCD0fcHbzTYjnaoskdiJrVHg86yxoZ8ZUqsXvyn4WUqehRiFKnaclOhqk9w4Ui2KVw==", + "dependencies": { + "@ant-design/colors": "^7.0.0", + "@ant-design/icons-svg": "^4.3.0", + "@babel/runtime": "^7.11.2", + "classnames": "^2.2.6", + "rc-util": "^5.31.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/icons-svg": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.3.1.tgz", + "integrity": "sha512-4QBZg8ccyC6LPIRii7A0bZUk3+lEDCLnhB+FVsflGdcWPPmV+j3fire4AwwoqHV/BibgvBmR9ZIo4s867smv+g==" + }, + "node_modules/@ant-design/react-slick": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-1.0.2.tgz", + "integrity": "sha512-Wj8onxL/T8KQLFFiCA4t8eIRGpRR+UPgOdac2sYzonv+i0n3kXHmvHLLiOYL655DQx2Umii9Y9nNgL7ssu5haQ==", + "dependencies": { + "@babel/runtime": "^7.10.4", + "classnames": "^2.2.5", + "json2mq": "^0.2.0", + "resize-observer-polyfill": "^1.5.1", + "throttle-debounce": "^5.0.0" + }, + "peerDependencies": { + "react": ">=16.9.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.23.5", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", @@ -2108,7 +2174,6 @@ "version": "7.23.6", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.6.tgz", "integrity": "sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ==", - "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -2220,6 +2285,24 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@ctrl/tinycolor": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", + "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==" + }, + "node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.19.10", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.10.tgz", @@ -3400,6 +3483,118 @@ "integrity": "sha512-2LuNTFBIO0m7kKIQvvPHN6UE63VjpmL9rnEEaOOaiSPbZK+zUOYIzBAWcED+3XYzhYsd/0mD57VdxAEqqV52CQ==", "dev": true }, + "node_modules/@rc-component/color-picker": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-1.5.1.tgz", + "integrity": "sha512-onyAFhWKXuG4P162xE+7IgaJkPkwM94XlOYnQuu69XdXWMfxpeFi6tpJBsieIMV7EnyLV5J3lDzdLiFeK0iEBA==", + "dependencies": { + "@babel/runtime": "^7.23.6", + "@ctrl/tinycolor": "^3.6.1", + "classnames": "^2.2.6", + "rc-util": "^5.38.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/context": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@rc-component/context/-/context-1.4.0.tgz", + "integrity": "sha512-kFcNxg9oLRMoL3qki0OMxK+7g5mypjgaaJp/pkOis/6rVxma9nJBF/8kCIuTYHUQNr0ii7MxqE33wirPZLJQ2w==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mini-decimal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.0.tgz", + "integrity": "sha512-jS4E7T9Li2GuYwI6PyiVXmxTiM6b07rlD9Ge8uGZSCz3WlzcG5ZK7g5bbuKNeZ9pgUuPK/5guV781ujdVpm4HQ==", + "dependencies": { + "@babel/runtime": "^7.18.0" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@rc-component/mutate-observer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-1.1.0.tgz", + "integrity": "sha512-QjrOsDXQusNwGZPf4/qRQasg7UFEj06XiCJ8iuiq/Io7CrHrgVi6Uuetw60WAMG1799v+aM8kyc+1L/GBbHSlw==", + "dependencies": { + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/portal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-1.1.2.tgz", + "integrity": "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg==", + "dependencies": { + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/tour": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-1.12.3.tgz", + "integrity": "sha512-U4mf1FiUxGCwrX4ed8op77Y8VKur+8Y/61ylxtqGbcSoh1EBC7bWd/DkLu0ClTUrKZInqEi1FL7YgFtnT90vHA==", + "dependencies": { + "@babel/runtime": "^7.18.0", + "@rc-component/portal": "^1.0.0-9", + "@rc-component/trigger": "^1.3.6", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/trigger": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-1.18.2.tgz", + "integrity": "sha512-jRLYgFgjLEPq3MvS87fIhcfuywFSRDaDrYw1FLku7Cm4esszvzTbA0JBsyacAyLrK9rF3TiHFcvoEDMzoD3CTA==", + "dependencies": { + "@babel/runtime": "^7.23.2", + "@rc-component/portal": "^1.1.0", + "classnames": "^2.3.2", + "rc-motion": "^2.0.0", + "rc-resize-observer": "^1.3.1", + "rc-util": "^5.38.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, "node_modules/@remix-run/router": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.14.1.tgz", @@ -5432,6 +5627,68 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/antd": { + "version": "5.13.2", + "resolved": "https://registry.npmjs.org/antd/-/antd-5.13.2.tgz", + "integrity": "sha512-P+N8gc0NOPy2WqJj/57Ey3dZUmb7nEUwAM+CIJaR5SOEjZnhEtMGRJSt+3lnhJ3MNRR39aR6NYkRVp2mYfphiA==", + "dependencies": { + "@ant-design/colors": "^7.0.2", + "@ant-design/cssinjs": "^1.18.2", + "@ant-design/icons": "^5.2.6", + "@ant-design/react-slick": "~1.0.2", + "@ctrl/tinycolor": "^3.6.1", + "@rc-component/color-picker": "~1.5.1", + "@rc-component/mutate-observer": "^1.1.0", + "@rc-component/tour": "~1.12.2", + "@rc-component/trigger": "^1.18.2", + "classnames": "^2.5.1", + "copy-to-clipboard": "^3.3.3", + "dayjs": "^1.11.10", + "qrcode.react": "^3.1.0", + "rc-cascader": "~3.21.0", + "rc-checkbox": "~3.1.0", + "rc-collapse": "~3.7.2", + "rc-dialog": "~9.3.4", + "rc-drawer": "~7.0.0", + "rc-dropdown": "~4.1.0", + "rc-field-form": "~1.41.0", + "rc-image": "~7.5.1", + "rc-input": "~1.4.3", + "rc-input-number": "~8.6.1", + "rc-mentions": "~2.10.1", + "rc-menu": "~9.12.4", + "rc-motion": "^2.9.0", + "rc-notification": "~5.3.0", + "rc-pagination": "~4.0.4", + "rc-picker": "~3.14.6", + "rc-progress": "~3.5.1", + "rc-rate": "~2.12.0", + "rc-resize-observer": "^1.4.0", + "rc-segmented": "~2.2.2", + "rc-select": "~14.11.0", + "rc-slider": "~10.5.0", + "rc-steps": "~6.0.1", + "rc-switch": "~4.1.0", + "rc-table": "~7.37.0", + "rc-tabs": "~14.0.0", + "rc-textarea": "~1.6.3", + "rc-tooltip": "~6.1.3", + "rc-tree": "~5.8.2", + "rc-tree-select": "~5.17.0", + "rc-upload": "~4.5.2", + "rc-util": "^5.38.1", + "scroll-into-view-if-needed": "^3.1.0", + "throttle-debounce": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ant-design" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -5518,6 +5775,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-tree-filter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-tree-filter/-/array-tree-filter-2.1.0.tgz", + "integrity": "sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw==" + }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -5618,6 +5880,11 @@ "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", "dev": true }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -6215,6 +6482,11 @@ "node": ">=6.0" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -6340,6 +6612,11 @@ "node": ">= 10" } }, + "node_modules/compute-scroll-into-view": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz", + "integrity": "sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -6399,6 +6676,14 @@ "node": ">= 0.6" } }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, "node_modules/core-js-compat": { "version": "3.34.0", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.34.0.tgz", @@ -6556,8 +6841,7 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/damerau-levenshtein": { "version": "1.0.8", @@ -6579,6 +6863,11 @@ "node": ">=14" } }, + "node_modules/dayjs": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -8157,9 +8446,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", "funding": [ { "type": "individual", @@ -9537,6 +9826,14 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/json2mq": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz", + "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==", + "dependencies": { + "string-convert": "^0.2.0" + } + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -10878,6 +11175,14 @@ "node": ">=6" } }, + "node_modules/qrcode.react": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-3.1.0.tgz", + "integrity": "sha512-oyF+Urr3oAMUG/OiOuONL3HXM+53wvuH3mtIWQrYmsXoAq0DkvZp2RYUWFSMFtbdOpuS++9v+WAkzNVkMlNW6Q==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/qs": { "version": "6.11.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", @@ -10941,6 +11246,576 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/rc-cascader": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.21.0.tgz", + "integrity": "sha512-7aADjbfqiR4HrTHG9S019p2jeKM/AxISPA5+sBJR7Mlhm/i+lR7VjBju3KQulJNJLKNEnQYg4TFhcPf2SLua9g==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "array-tree-filter": "^2.1.0", + "classnames": "^2.3.1", + "rc-select": "~14.11.0-0", + "rc-tree": "~5.8.1", + "rc-util": "^5.37.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-checkbox": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/rc-checkbox/-/rc-checkbox-3.1.0.tgz", + "integrity": "sha512-PAwpJFnBa3Ei+5pyqMMXdcKYKNBMS+TvSDiLdDnARnMJHC8ESxwPfm4Ao1gJiKtWLdmGfigascnCpwrHFgoOBQ==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.3.2", + "rc-util": "^5.25.2" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-collapse": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/rc-collapse/-/rc-collapse-3.7.2.tgz", + "integrity": "sha512-ZRw6ipDyOnfLFySxAiCMdbHtb5ePAsB9mT17PA6y1mRD/W6KHRaZeb5qK/X9xDV1CqgyxMpzw0VdS74PCcUk4A==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.3.4", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-dialog": { + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/rc-dialog/-/rc-dialog-9.3.4.tgz", + "integrity": "sha512-975X3018GhR+EjZFbxA2Z57SX5rnu0G0/OxFgMMvZK4/hQWEm3MHaNvP4wXpxYDoJsp+xUvVW+GB9CMMCm81jA==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/portal": "^1.0.0-8", + "classnames": "^2.2.6", + "rc-motion": "^2.3.0", + "rc-util": "^5.21.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-drawer": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-7.0.0.tgz", + "integrity": "sha512-ePcS4KtQnn57bCbVXazHN2iC8nTPCXlWEIA/Pft87Pd9U7ZeDkdRzG47jWG2/TAFXFlFltRAMcslqmUM8NPCGA==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/portal": "^1.1.1", + "classnames": "^2.2.6", + "rc-motion": "^2.6.1", + "rc-util": "^5.36.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-dropdown": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/rc-dropdown/-/rc-dropdown-4.1.0.tgz", + "integrity": "sha512-VZjMunpBdlVzYpEdJSaV7WM7O0jf8uyDjirxXLZRNZ+tAC+NzD3PXPEtliFwGzVwBBdCmGuSqiS9DWcOLxQ9tw==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@rc-component/trigger": "^1.7.0", + "classnames": "^2.2.6", + "rc-util": "^5.17.0" + }, + "peerDependencies": { + "react": ">=16.11.0", + "react-dom": ">=16.11.0" + } + }, + "node_modules/rc-field-form": { + "version": "1.41.0", + "resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-1.41.0.tgz", + "integrity": "sha512-k9AS0wmxfJfusWDP/YXWTpteDNaQ4isJx9UKxx4/e8Dub4spFeZ54/EuN2sYrMRID/+hUznPgVZeg+Gf7XSYCw==", + "dependencies": { + "@babel/runtime": "^7.18.0", + "async-validator": "^4.1.0", + "rc-util": "^5.32.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-image": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/rc-image/-/rc-image-7.5.1.tgz", + "integrity": "sha512-Z9loECh92SQp0nSipc0MBuf5+yVC05H/pzC+Nf8xw1BKDFUJzUeehYBjaWlxly8VGBZJcTHYri61Fz9ng1G3Ag==", + "dependencies": { + "@babel/runtime": "^7.11.2", + "@rc-component/portal": "^1.0.2", + "classnames": "^2.2.6", + "rc-dialog": "~9.3.4", + "rc-motion": "^2.6.2", + "rc-util": "^5.34.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-input": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.4.3.tgz", + "integrity": "sha512-aHyQUAIRmTlOnvk5EcNqEpJ+XMtfMpYRAJayIlJfsvvH9cAKUWboh4egm23vgMA7E+c/qm4BZcnrDcA960GC1w==", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-util": "^5.18.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/rc-input-number": { + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-8.6.1.tgz", + "integrity": "sha512-gaAMUKtUKLktJ3Yx93tjgYY1M0HunnoqzPEqkb9//Ydup4DcG0TFL9yHBA3pgVdNIt5f0UWyHCgFBj//JxeD6A==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/mini-decimal": "^1.0.1", + "classnames": "^2.2.5", + "rc-input": "~1.4.0", + "rc-util": "^5.28.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-mentions": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.10.1.tgz", + "integrity": "sha512-72qsEcr/7su+a07ndJ1j8rI9n0Ka/ngWOLYnWMMv0p2mi/5zPwPrEDTt6Uqpe8FWjWhueDJx/vzunL6IdKDYMg==", + "dependencies": { + "@babel/runtime": "^7.22.5", + "@rc-component/trigger": "^1.5.0", + "classnames": "^2.2.6", + "rc-input": "~1.4.0", + "rc-menu": "~9.12.0", + "rc-textarea": "~1.6.1", + "rc-util": "^5.34.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-menu": { + "version": "9.12.4", + "resolved": "https://registry.npmjs.org/rc-menu/-/rc-menu-9.12.4.tgz", + "integrity": "sha512-t2NcvPLV1mFJzw4F21ojOoRVofK2rWhpKPx69q2raUsiHPDP6DDevsBILEYdsIegqBeSXoWs2bf6CueBKg3BFg==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/trigger": "^1.17.0", + "classnames": "2.x", + "rc-motion": "^2.4.3", + "rc-overflow": "^1.3.1", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-motion": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/rc-motion/-/rc-motion-2.9.0.tgz", + "integrity": "sha512-XIU2+xLkdIr1/h6ohPZXyPBMvOmuyFZQ/T0xnawz+Rh+gh4FINcnZmMT5UTIj6hgI0VLDjTaPeRd+smJeSPqiQ==", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-util": "^5.21.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-notification": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/rc-notification/-/rc-notification-5.3.0.tgz", + "integrity": "sha512-WCf0uCOkZ3HGfF0p1H4Sgt7aWfipxORWTPp7o6prA3vxwtWhtug3GfpYls1pnBp4WA+j8vGIi5c2/hQRpGzPcQ==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.9.0", + "rc-util": "^5.20.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-overflow": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/rc-overflow/-/rc-overflow-1.3.2.tgz", + "integrity": "sha512-nsUm78jkYAoPygDAcGZeC2VwIg/IBGSodtOY3pMof4W3M9qRJgqaDYm03ZayHlde3I6ipliAxbN0RUcGf5KOzw==", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.37.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-pagination": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/rc-pagination/-/rc-pagination-4.0.4.tgz", + "integrity": "sha512-GGrLT4NgG6wgJpT/hHIpL9nELv27A1XbSZzECIuQBQTVSf4xGKxWr6I/jhpRPauYEWEbWVw22ObG6tJQqwJqWQ==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.3.2", + "rc-util": "^5.38.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-picker": { + "version": "3.14.6", + "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-3.14.6.tgz", + "integrity": "sha512-AdKKW0AqMwZsKvIpwUWDUnpuGKZVrbxVTZTNjcO+pViGkjC1EBcjMgxVe8tomOEaIHJL5Gd13vS8Rr3zzxWmag==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/trigger": "^1.5.0", + "classnames": "^2.2.1", + "rc-util": "^5.30.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "date-fns": ">= 2.x", + "dayjs": ">= 1.x", + "luxon": ">= 3.x", + "moment": ">= 2.x", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + }, + "peerDependenciesMeta": { + "date-fns": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + } + } + }, + "node_modules/rc-progress": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-3.5.1.tgz", + "integrity": "sha512-V6Amx6SbLRwPin/oD+k1vbPrO8+9Qf8zW1T8A7o83HdNafEVvAxPV5YsgtKFP+Ud5HghLj33zKOcEHrcrUGkfw==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.6", + "rc-util": "^5.16.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-rate": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/rc-rate/-/rc-rate-2.12.0.tgz", + "integrity": "sha512-g092v5iZCdVzbjdn28FzvWebK2IutoVoiTeqoLTj9WM7SjA/gOJIw5/JFZMRyJYYVe1jLAU2UhAfstIpCNRozg==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.5", + "rc-util": "^5.0.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-resize-observer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-1.4.0.tgz", + "integrity": "sha512-PnMVyRid9JLxFavTjeDXEXo65HCRqbmLBw9xX9gfC4BZiSzbLXKzW3jPz+J0P71pLbD5tBMTT+mkstV5gD0c9Q==", + "dependencies": { + "@babel/runtime": "^7.20.7", + "classnames": "^2.2.1", + "rc-util": "^5.38.0", + "resize-observer-polyfill": "^1.5.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-segmented": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/rc-segmented/-/rc-segmented-2.2.2.tgz", + "integrity": "sha512-Mq52M96QdHMsNdE/042ibT5vkcGcD5jxKp7HgPC2SRofpia99P5fkfHy1pEaajLMF/kj0+2Lkq1UZRvqzo9mSA==", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-motion": "^2.4.4", + "rc-util": "^5.17.0" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/rc-select": { + "version": "14.11.0", + "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.11.0.tgz", + "integrity": "sha512-8J8G/7duaGjFiTXCBLWfh5P+KDWyA3KTlZDfV3xj/asMPqB2cmxfM+lH50wRiPIRsCQ6EbkCFBccPuaje3DHIg==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/trigger": "^1.5.0", + "classnames": "2.x", + "rc-motion": "^2.0.1", + "rc-overflow": "^1.3.1", + "rc-util": "^5.16.1", + "rc-virtual-list": "^3.5.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-slider": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-10.5.0.tgz", + "integrity": "sha512-xiYght50cvoODZYI43v3Ylsqiw14+D7ELsgzR40boDZaya1HFa1Etnv9MDkQE8X/UrXAffwv2AcNAhslgYuDTw==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.5", + "rc-util": "^5.27.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-steps": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rc-steps/-/rc-steps-6.0.1.tgz", + "integrity": "sha512-lKHL+Sny0SeHkQKKDJlAjV5oZ8DwCdS2hFhAkIjuQt1/pB81M0cA0ErVFdHq9+jmPmFw1vJB2F5NBzFXLJxV+g==", + "dependencies": { + "@babel/runtime": "^7.16.7", + "classnames": "^2.2.3", + "rc-util": "^5.16.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-switch": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/rc-switch/-/rc-switch-4.1.0.tgz", + "integrity": "sha512-TI8ufP2Az9oEbvyCeVE4+90PDSljGyuwix3fV58p7HV2o4wBnVToEyomJRVyTaZeqNPAp+vqeo4Wnj5u0ZZQBg==", + "dependencies": { + "@babel/runtime": "^7.21.0", + "classnames": "^2.2.1", + "rc-util": "^5.30.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-table": { + "version": "7.37.0", + "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.37.0.tgz", + "integrity": "sha512-hEB17ktLRVfVmdo+U8MjGr+PuIgdQ8Cxj/N5lwMvP/Az7TOrQxwTMLVEDoj207tyPYLTWifHIF9EJREWwyk67g==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/context": "^1.4.0", + "classnames": "^2.2.5", + "rc-resize-observer": "^1.1.0", + "rc-util": "^5.37.0", + "rc-virtual-list": "^3.11.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tabs": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-14.0.0.tgz", + "integrity": "sha512-lp1YWkaPnjlyhOZCPrAWxK6/P6nMGX/BAZcAC3nuVwKz0Byfp+vNnQKK8BRCP2g/fzu+SeB5dm9aUigRu3tRkQ==", + "dependencies": { + "@babel/runtime": "^7.11.2", + "classnames": "2.x", + "rc-dropdown": "~4.1.0", + "rc-menu": "~9.12.0", + "rc-motion": "^2.6.2", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.34.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-textarea": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.6.3.tgz", + "integrity": "sha512-8k7+8Y2GJ/cQLiClFMg8kUXOOdvcFQrnGeSchOvI2ZMIVvX5a3zQpLxoODL0HTrvU63fPkRmMuqaEcOF9dQemA==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.1", + "rc-input": "~1.4.0", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tooltip": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-6.1.3.tgz", + "integrity": "sha512-HMSbSs5oieZ7XddtINUddBLSVgsnlaSb3bZrzzGWjXa7/B7nNedmsuz72s7EWFEro9mNa7RyF3gOXKYqvJiTcQ==", + "dependencies": { + "@babel/runtime": "^7.11.2", + "@rc-component/trigger": "^1.18.0", + "classnames": "^2.3.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tree": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-5.8.2.tgz", + "integrity": "sha512-xH/fcgLHWTLmrSuNphU8XAqV7CdaOQgm4KywlLGNoTMhDAcNR3GVNP6cZzb0GrKmIZ9yae+QLot/cAgUdPRMzg==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.0.1", + "rc-util": "^5.16.1", + "rc-virtual-list": "^3.5.1" + }, + "engines": { + "node": ">=10.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-tree-select": { + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-5.17.0.tgz", + "integrity": "sha512-7sRGafswBhf7n6IuHyCEFCildwQIgyKiV8zfYyUoWfZEFdhuk7lCH+DN0aHt+oJrdiY9+6Io/LDXloGe01O8XQ==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-select": "~14.11.0-0", + "rc-tree": "~5.8.1", + "rc-util": "^5.16.1" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-upload": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/rc-upload/-/rc-upload-4.5.2.tgz", + "integrity": "sha512-QO3ne77DwnAPKFn0bA5qJM81QBjQi0e0NHdkvpFyY73Bea2NfITiotqJqVjHgeYPOJu5lLVR32TNGP084aSoXA==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "classnames": "^2.2.5", + "rc-util": "^5.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-util": { + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.38.1.tgz", + "integrity": "sha512-e4ZMs7q9XqwTuhIK7zBIVFltUtMSjphuPPQXHoHlzRzNdOwUxDejo0Zls5HYaJfRKNURcsS/ceKVULlhjBrxng==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "react-is": "^18.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-virtual-list": { + "version": "3.11.3", + "resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.11.3.tgz", + "integrity": "sha512-tu5UtrMk/AXonHwHxUogdXAWynaXsrx1i6dsgg+lOo/KJSF8oBAcprh1z5J3xgnPJD5hXxTL58F8s8onokdt0Q==", + "dependencies": { + "@babel/runtime": "^7.20.0", + "classnames": "^2.2.6", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.36.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, "node_modules/react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -10967,8 +11842,7 @@ "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, "node_modules/react-refresh": { "version": "0.14.0", @@ -11072,8 +11946,7 @@ "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/regenerator-transform": { "version": "0.15.2", @@ -11154,6 +12027,11 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -11394,6 +12272,14 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/scroll-into-view-if-needed": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", + "dependencies": { + "compute-scroll-into-view": "^3.0.2" + } + }, "node_modules/secure-compare": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz", @@ -11705,6 +12591,11 @@ } ] }, + "node_modules/string-convert": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", + "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==" + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -11893,6 +12784,11 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/stylis": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.1.tgz", + "integrity": "sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ==" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -12072,6 +12968,14 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/throttle-debounce": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.0.tgz", + "integrity": "sha512-2iQTSgkkc1Zyk0MeVrt/3BvuOXYPl/R8Z0U2xxo9rjwNciaHDG3R+Lm6dh4EeUci49DanvBnuqI6jshoQQRGEg==", + "engines": { + "node": ">=12.22" + } + }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -12135,6 +13039,11 @@ "node": ">=8.0" } }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" + }, "node_modules/token-types": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/token-types/-/token-types-5.0.1.tgz", @@ -12582,9 +13491,9 @@ } }, "node_modules/vite": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.10.tgz", - "integrity": "sha512-2P8J7WWgmc355HUMlFrwofacvr98DAjoE52BfdbwQtyLH06XKwaL/FMnmKM2crF0iX4MpmMKoDlNCB1ok7zHCw==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz", + "integrity": "sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==", "dev": true, "dependencies": { "esbuild": "^0.19.3", diff --git a/client/package.json b/client/package.json index 090bca7362..7c4d60dc6b 100644 --- a/client/package.json +++ b/client/package.json @@ -11,6 +11,7 @@ "dependencies": { "@swc/helpers": "~0.5.2", "@tanstack/react-query": "^5.15.0", + "antd": "^5.13.2", "axios": "^1.6.3", "react": "18.2.0", "react-dom": "18.2.0", diff --git a/client/src/datafiles/datafilesRouter.tsx b/client/src/datafiles/datafilesRouter.tsx new file mode 100644 index 0000000000..54423fd5c9 --- /dev/null +++ b/client/src/datafiles/datafilesRouter.tsx @@ -0,0 +1,223 @@ +import React from 'react'; +import DataFilesRoot from './layouts/DataFilesBaseLayout'; + +import { + createBrowserRouter, + Navigate, + useParams, + NavigateProps, + generatePath, + useLocation, +} from 'react-router-dom'; +import { ProjectPreviewLayout } from './layouts/projects/ProjectPreviewLayout'; +import { NEESListingLayout } from './layouts/nees/NeesListingLayout'; +import { NeesDetailLayout } from './layouts/nees/NeesDetailLayout'; +import { FileListingLayout } from './layouts/FileListingLayout'; +import { ProjectDetailLayout } from './layouts/projects/ProjectDetailLayout'; +import { PublishedDetailLayout } from './layouts/published/PublishedDetailLayout'; +import { PublishedListingLayout } from './layouts/published/PublishedListingLayout'; +import { ProjectListingLayout } from './layouts/projects/ProjectListingLayout'; +import { ProjectPipelineLayout } from './layouts/projects/ProjectPipelineLayout'; +import { ProjectCurationLayout } from './layouts/projects/ProjectCurationLayout'; +import { ProjectWorkdirLayout } from './layouts/projects/ProjectWorkdirLayout'; + +const NavigateToUrlSafePath: React.FC = ({ + to, + relative, + replace, + state, +}) => { + const { '*': splatPath, ...params } = useParams(); + + return ( + + ); +}; + +const RedirectAgaveToTapis: React.FC> = ({ + relative, + replace, + state, +}) => { + const location = useLocation(); + return ( + + ); +}; + +const datafilesRouter = createBrowserRouter( + [ + { + path: '/', + element: , + children: [ + { + path: 'public-legacy', + element: , + }, + { + path: 'public/nees.public', + children: [ + { + path: '', + element: , + }, + { + path: ':neesid', + element: , + children: [{ path: ':path', element: }], + }, + ], + }, + { + path: 'public/designsafe.storage.community/*', + element: ( + + ), + }, + { + path: 'public', + children: [ + { + path: '', + element: , + }, + { + path: 'designsafe.storage.published', + element: , + }, + { + path: 'designsafe.storage.published/:projectId', + element: , + children: [ + { + path: ':path', + element: , + }, + { + path: '*', + element: , + }, + ], + }, + ], + }, + + { + path: 'projects', + children: [ + { + path: '', + element: , + }, + { + path: ':uuid/prepare-to-publish', + element: , + }, + { + path: ':uuid/preview', + element: , + }, + { + path: ':uuid', + element: , + children: [ + { + path: 'curation/:path?', + element: , + }, + { + path: 'workdir/:path?', + element: , + }, + { + path: '*', + element: , + }, + ], + }, + ], + }, + { + path: 'googledrive', + children: [ + { + id: 'googledrive', + path: ':path?', + element: , + }, + { + path: '*', + element: , + }, + ], + }, + { + path: 'dropbox', + children: [ + { + path: ':path?', + id: 'dropbox', + element: , + }, + { + path: '*', + element: , + }, + ], + }, + { + path: 'box', + children: [ + { + path: ':path?', + id: 'box', + element: , + }, + { + path: '*', + element: , + }, + ], + }, + { + path: 'agave/*', + element: , + }, + { + path: ':api/:system', + children: [ + { + path: ':path?', + id: 'tapis', + element: , + }, + { + path: '*', + element: , + }, + ], + }, + ], + }, + ], + { basename: '/data/browser/' } +); + +export default datafilesRouter; diff --git a/client/src/datafiles/layouts/DataFilesBaseLayout.tsx b/client/src/datafiles/layouts/DataFilesBaseLayout.tsx new file mode 100644 index 0000000000..09f815fa1d --- /dev/null +++ b/client/src/datafiles/layouts/DataFilesBaseLayout.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { Navigate, Outlet, useLocation } from 'react-router-dom'; +import { Layout } from 'antd'; +import { DatafilesSideNav } from '@client/datafiles'; +import { useAuthenticatedUser } from '@client/hooks'; + +const { Sider } = Layout; + +const DataFilesRoot: React.FC = () => { + const { user } = useAuthenticatedUser(); + const defaultPath = user?.username + ? '/tapis/designsafe.storage.default' + : '/tapis/designsafe.storage.community'; + const { pathname } = useLocation(); + + return ( + + +

    + Data Depot +

    + + +
    + {pathname === '/' && } + +
    + ); +}; + +export default DataFilesRoot; diff --git a/client/src/datafiles/layouts/FileListingLayout.tsx b/client/src/datafiles/layouts/FileListingLayout.tsx new file mode 100644 index 0000000000..4dbcc432d5 --- /dev/null +++ b/client/src/datafiles/layouts/FileListingLayout.tsx @@ -0,0 +1,54 @@ +import { + BaseFileListingBreadcrumb, + DatafilesToolbar, + FileListing, +} from '@client/datafiles'; +import { useAuthenticatedUser, useFileListingRouteParams } from '@client/hooks'; +import { Layout } from 'antd'; +import React from 'react'; +import { Link, Navigate } from 'react-router-dom'; +import styles from './layout.module.css'; + +export const FileListingLayout: React.FC = () => { + const { api, path, scheme, system } = useFileListingRouteParams(); + const { user } = useAuthenticatedUser(); + + const isUserHomeSystem = [ + 'designsafe.storage.default', + 'designsafe.storage.frontera.work', + ].includes(system); + + const redirectHome = + user?.username && !path && api === 'tapis' && isUserHomeSystem; + return ( + + + {true && ( + { + return ( + + {obj.title} + + ); + }} + /> + )} + +
    + {redirectHome && ( + + )} + +
    +
    +
    + ); +}; +// diff --git a/client/src/datafiles/layouts/layout.module.css b/client/src/datafiles/layouts/layout.module.css new file mode 100644 index 0000000000..4b7a7d4bd3 --- /dev/null +++ b/client/src/datafiles/layouts/layout.module.css @@ -0,0 +1,11 @@ +.listing-main { + display: flex; + flex-direction: column; + padding-bottom: 16px; +} + +.listing-container { + max-height: 1000px; + overflow: auto; + flex: 1 0 0; +} diff --git a/client/src/datafiles/layouts/nees/NeesDetailLayout.tsx b/client/src/datafiles/layouts/nees/NeesDetailLayout.tsx new file mode 100644 index 0000000000..269286e60f --- /dev/null +++ b/client/src/datafiles/layouts/nees/NeesDetailLayout.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { Outlet } from 'react-router-dom'; + +export const NeesDetailLayout: React.FC = () => { + return ( +
    + Placeholder for the NEES detail view. +
    + ); +}; diff --git a/client/src/datafiles/layouts/nees/NeesListingLayout.tsx b/client/src/datafiles/layouts/nees/NeesListingLayout.tsx new file mode 100644 index 0000000000..a71036a9e5 --- /dev/null +++ b/client/src/datafiles/layouts/nees/NeesListingLayout.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export const NEESListingLayout: React.FC = () => { + return
    Placeholder for the NEES listing view.
    ; +}; diff --git a/client/src/datafiles/layouts/projects/ProjectCurationLayout.tsx b/client/src/datafiles/layouts/projects/ProjectCurationLayout.tsx new file mode 100644 index 0000000000..27546303fa --- /dev/null +++ b/client/src/datafiles/layouts/projects/ProjectCurationLayout.tsx @@ -0,0 +1,10 @@ +import React from 'react'; + +export const ProjectCurationLayout: React.FC = () => { + return ( +
    + Placeholder for the project curation view (listing and toolbar for + handling associations) +
    + ); +}; diff --git a/client/src/datafiles/layouts/projects/ProjectDetailLayout.tsx b/client/src/datafiles/layouts/projects/ProjectDetailLayout.tsx new file mode 100644 index 0000000000..cca46c5fa0 --- /dev/null +++ b/client/src/datafiles/layouts/projects/ProjectDetailLayout.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { Outlet } from 'react-router-dom'; + +export const ProjectDetailLayout: React.FC = () => { + return ( +
    + Placeholder for the project detail layout (handles workdir and curation).{' '} + +
    + ); +}; diff --git a/client/src/datafiles/layouts/projects/ProjectListingLayout.tsx b/client/src/datafiles/layouts/projects/ProjectListingLayout.tsx new file mode 100644 index 0000000000..5325ec957c --- /dev/null +++ b/client/src/datafiles/layouts/projects/ProjectListingLayout.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export const ProjectListingLayout: React.FC = () => { + return
    Placeholder for the project listing layout
    ; +}; diff --git a/client/src/datafiles/layouts/projects/ProjectPipelineLayout.tsx b/client/src/datafiles/layouts/projects/ProjectPipelineLayout.tsx new file mode 100644 index 0000000000..22262f4a42 --- /dev/null +++ b/client/src/datafiles/layouts/projects/ProjectPipelineLayout.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export const ProjectPipelineLayout: React.FC = () => { + return
    Placeholder for the Publication Pipeline layout.
    ; +}; diff --git a/client/src/datafiles/layouts/projects/ProjectPreviewLayout.tsx b/client/src/datafiles/layouts/projects/ProjectPreviewLayout.tsx new file mode 100644 index 0000000000..721781da10 --- /dev/null +++ b/client/src/datafiles/layouts/projects/ProjectPreviewLayout.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export const ProjectPreviewLayout: React.FC = () => { + return
    Placeholder for the Publication Preview layout.
    ; +}; diff --git a/client/src/datafiles/layouts/projects/ProjectWorkdirLayout.tsx b/client/src/datafiles/layouts/projects/ProjectWorkdirLayout.tsx new file mode 100644 index 0000000000..41044fdb17 --- /dev/null +++ b/client/src/datafiles/layouts/projects/ProjectWorkdirLayout.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export const ProjectWorkdirLayout: React.FC = () => { + return
    Placeholder for the project workdir layout
    ; +}; diff --git a/client/src/datafiles/layouts/published/PublishedDetailLayout.tsx b/client/src/datafiles/layouts/published/PublishedDetailLayout.tsx new file mode 100644 index 0000000000..2061d0f6c2 --- /dev/null +++ b/client/src/datafiles/layouts/published/PublishedDetailLayout.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { Outlet } from 'react-router-dom'; + +export const PublishedDetailLayout: React.FC = () => { + return ( +
    + Placeholder for the Publication detail layout. + +
    + ); +}; diff --git a/client/src/datafiles/layouts/published/PublishedListingLayout.tsx b/client/src/datafiles/layouts/published/PublishedListingLayout.tsx new file mode 100644 index 0000000000..6b047fad15 --- /dev/null +++ b/client/src/datafiles/layouts/published/PublishedListingLayout.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export const PublishedListingLayout: React.FC = () => { + return
    Placeholder for the Publication listing layout.
    ; +}; diff --git a/client/src/main.tsx b/client/src/main.tsx index 5c32f1799a..7de19dab0c 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -1,10 +1,41 @@ +import './styles.css'; import { StrictMode } from 'react'; import * as ReactDOM from 'react-dom/client'; import { RouterProvider } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import workspaceRouter from './workspace/router'; +import workspaceRouter from './workspace/workspaceRouter'; +import datafilesRouter from './datafiles/datafilesRouter'; +import { ConfigProvider, ThemeConfig } from 'antd'; const queryClient = new QueryClient(); +const themeConfig: ThemeConfig = { + token: { + borderRadius: 0, + colorPrimary: '#337ab7', + colorPrimaryTextHover: 'black', + }, + components: { + Table: { + cellPaddingBlock: 8, + headerBg: 'transparent', + headerColor: '#333333', + headerSplitColor: 'transparent', + rowHoverBg: 'rgb(230, 246, 255)', + borderColor: 'rgb(215, 215, 215)', + colorText: 'rgb(112, 112, 112)', + }, + Layout: { + bodyBg: 'transparent', + }, + Menu: { + itemHeight: 60, + itemMarginInline: 0, + itemSelectedColor: 'black', + itemHoverBg: 'rgba(96, 57, 204, 0.25)', + itemSelectedBg: 'rgba(96, 57, 204, 0.25)', + }, + }, +}; const appsElement = document.getElementById('apps-root'); if (appsElement) { @@ -12,7 +43,23 @@ if (appsElement) { appsRoot.render( - + + + + + + ); +} + +const datafilesElement = document.getElementById('datafiles-root'); +if (datafilesElement) { + const datafilesRoot = ReactDOM.createRoot(datafilesElement as HTMLElement); + datafilesRoot.render( + + + + + ); diff --git a/client/src/styles.css b/client/src/styles.css index 90d4ee0072..e4ab0a4744 100644 --- a/client/src/styles.css +++ b/client/src/styles.css @@ -1 +1,75 @@ /* You can add global styles to this file, and also import other style files */ +.ant-spin-nested-loading { + height: 100%; +} + +.ant-spin.ant-spin.ant-spin { + max-height: 100%; +} + +.table--pull-spinner-bottom .ant-spin.ant-spin { + height: fit-content; + top: calc(100% - 20px); +} + +.ant-spin-container { + height: 100%; + display: flex; + flex-flow: column nowrap; +} + +.ant-table { + overflow: auto; +} + +.ant-table .ant-table-body .ant-table-placeholder > td { + border-bottom-color: transparent; +} +.ant-table .ant-table-tbody .ant-table-placeholder > td { + border-bottom-color: transparent; +} + +.ant-table-container { + height: 100%; + display: flex; + flex-flow: column nowrap; +} + +.ant-table-header th { + border-bottom: 1px solid #333333 !important; +} + +.ant-table-header { + flex: none; +} + +.ant-table-body { + overflow: scroll; +} + +.ant-table-pagination { + flex: none; +} + +.ant-layout-sider-collapsed { + margin-right: 30px; +} +.ant-layout-sider-collapsed .ant-layout-sider-zero-width-trigger { + right: -20px !important; +} + +.ant-layout-sider-zero-width-trigger { + right: 0px !important; + top: 20px !important; +} + +.ant-btn-link { + padding-inline: 0px !important; + height: unset; + padding: 0px !important; + color: #337ab7 !important; +} + +.listing-nav-link { + text-decoration: none !important; +} diff --git a/client/src/workspace/router.tsx b/client/src/workspace/workspaceRouter.tsx similarity index 100% rename from client/src/workspace/router.tsx rename to client/src/workspace/workspaceRouter.tsx diff --git a/client/tsconfig.base.json b/client/tsconfig.base.json index 7d48d5afee..c66e21b834 100644 --- a/client/tsconfig.base.json +++ b/client/tsconfig.base.json @@ -15,6 +15,8 @@ "skipDefaultLibCheck": true, "baseUrl": ".", "paths": { + "@client/common-components": ["modules/_common_components/src/index.ts"], + "@client/datafiles": ["modules/datafiles/src/index.ts"], "@client/hooks": ["modules/_hooks/src/index.ts"], "@client/test-fixtures": ["modules/_test-fixtures/src/index.ts"], "@client/workspace": ["modules/workspace/src/index.ts"] diff --git a/client/vite.config.ts b/client/vite.config.ts index 4f37e6d291..dd9bf3b56f 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -2,7 +2,6 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; -import fs from 'fs'; export default defineConfig({ root: __dirname, @@ -10,11 +9,7 @@ export default defineConfig({ server: { port: 4200, - host: 'designsafe.dev', - https: { - key: fs.readFileSync('../conf/nginx/certificates/designsafe.dev.key'), - cert: fs.readFileSync('../conf/nginx/certificates/designsafe.dev.crt'), - }, + origin: 'designsafe.dev', }, preview: { diff --git a/designsafe/apps/api/datafiles/handlers.py b/designsafe/apps/api/datafiles/handlers.py index 31a98a8c0c..fd67122694 100644 --- a/designsafe/apps/api/datafiles/handlers.py +++ b/designsafe/apps/api/datafiles/handlers.py @@ -21,6 +21,7 @@ operations_mapping = { 'agave': agave_operations, + 'tapis': agave_operations, 'googledrive': googledrive_operations, 'box': box_operations, 'dropbox': dropbox_operations, diff --git a/designsafe/apps/api/datafiles/operations/agave_operations.py b/designsafe/apps/api/datafiles/operations/agave_operations.py index 46ba18221d..875518c16c 100644 --- a/designsafe/apps/api/datafiles/operations/agave_operations.py +++ b/designsafe/apps/api/datafiles/operations/agave_operations.py @@ -512,7 +512,7 @@ def upload(client, system, path, uploaded_file, webkit_relative_path=None, *args return dict(resp) -def preview(client, system, path, href, max_uses=3, lifetime=600, *args, **kwargs): +def preview(client, system, path, href="", max_uses=3, lifetime=600, *args, **kwargs): """Preview a file. Params ------ diff --git a/designsafe/apps/api/datafiles/views.py b/designsafe/apps/api/datafiles/views.py index 5890bf6db0..594b5304e5 100644 --- a/designsafe/apps/api/datafiles/views.py +++ b/designsafe/apps/api/datafiles/views.py @@ -24,6 +24,7 @@ def get_client(user, api): client_mappings = { 'agave': 'agave_oauth', + 'tapis': 'agave_oauth', 'shared': 'agave_oauth', 'googledrive': 'googledrive_user_token', 'box': 'box_user_token', @@ -57,7 +58,7 @@ def get(self, request, api, operation=None, scheme='private', system=None, path= client = get_client(request.user, api) except AttributeError: raise resource_unconnected_handler(api) - elif api == 'agave' and system in (settings.COMMUNITY_SYSTEM, + elif api in ('agave', 'tapis') and system in (settings.COMMUNITY_SYSTEM, settings.PUBLISHED_SYSTEM, settings.NEES_PUBLIC_SYSTEM): client = service_account() diff --git a/designsafe/apps/data/templates/data/data_depot.j2 b/designsafe/apps/data/templates/data/data_depot.j2 index 31a99e0f66..457ea7bee0 100644 --- a/designsafe/apps/data/templates/data/data_depot.j2 +++ b/designsafe/apps/data/templates/data/data_depot.j2 @@ -95,16 +95,34 @@ Data Depot {% endblock %} {% block content %} +
    + + {%if not react_flag%}
    + {%endif%} {% addtoblock "css" %} {% endaddtoblock %} {% addtoblock "js" %} - + {% if debug and react_flag %} + + + + {% else %} + + {% endif %} + {% if not react_flag %} + {% endif %} {% endaddtoblock %} {% endblock %} diff --git a/designsafe/apps/workspace/templates/designsafe/apps/workspace/index.j2 b/designsafe/apps/workspace/templates/designsafe/apps/workspace/index.j2 index daa1ba81b2..cddad0d58e 100644 --- a/designsafe/apps/workspace/templates/designsafe/apps/workspace/index.j2 +++ b/designsafe/apps/workspace/templates/designsafe/apps/workspace/index.j2 @@ -56,14 +56,14 @@ {% addtoblock "js" %} {% if debug and react_flag %} - - + + {% else %} {% endif %} diff --git a/designsafe/static/styles/main.css b/designsafe/static/styles/main.css index 8c35c988c9..8be0737f54 100644 --- a/designsafe/static/styles/main.css +++ b/designsafe/static/styles/main.css @@ -933,11 +933,13 @@ li .popover.right { } .o-site { - display: grid; height: 100vh; - grid-template-rows: auto 1fr auto; + display: flex; + flex-direction: column; } .o-site__body { + display: flex; + flex-direction: column; flex-grow: 1; flex-shrink: 0; flex-basis: auto; diff --git a/designsafe/templates/base.j2 b/designsafe/templates/base.j2 index d2bfcc2ac3..f04e9740b8 100644 --- a/designsafe/templates/base.j2 +++ b/designsafe/templates/base.j2 @@ -137,6 +137,14 @@ ") } +