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 &&
}
+
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 (
+
+
+
+ );
+};
+
+export const DatafilesToolbar: React.FC = () => {
+ const { api, system, scheme, path } = useFileListingRouteParams();
+ const { selectedFiles } = useSelectedFiles(api, system, path);
+ const { user } = useAuthenticatedUser();
+
+ const rules = useMemo(
+ function () {
+ // Rules for which toolbar buttons are active for a given selection.
+ return {
+ canPreview:
+ selectedFiles.length === 1 && selectedFiles[0].type === 'file',
+ canCopy: user && selectedFiles.length >= 1,
+ };
+ },
+ [selectedFiles, user]
+ );
+
+ return (
+
+
(search bar goes here)
+
+
+ {({ onClick }) => (
+
+
+ Preview
+
+ )}
+
+
+ {({ onClick }) => (
+
+
+ Copy
+
+ )}
+
+
+
+ );
+};
diff --git a/client/modules/datafiles/src/FileListing/FileListing.module.css b/client/modules/datafiles/src/FileListing/FileListing.module.css
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/client/modules/datafiles/src/FileListing/FileListing.tsx b/client/modules/datafiles/src/FileListing/FileListing.tsx
new file mode 100644
index 0000000000..45a9a406ac
--- /dev/null
+++ b/client/modules/datafiles/src/FileListing/FileListing.tsx
@@ -0,0 +1,109 @@
+import React, { useMemo, useState } from 'react';
+//import styles from './FileListing.module.css';
+import { Button } from 'antd';
+import {
+ FileListingTable,
+ TFileListingColumns,
+} from './FileListingTable/FileListingTable';
+import { NavLink } from 'react-router-dom';
+import { PreviewModalBody } from '../DatafilesModal/PreviewModal';
+
+export function toBytes(bytes?: number) {
+ if (bytes === 0) return '0 bytes';
+ if (!bytes) return '-';
+ const units = ['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'];
+ const orderOfMagnitude = Math.floor(Math.log(bytes) / Math.log(1024));
+ const precision = orderOfMagnitude === 0 ? 0 : 1;
+ const bytesInUnits = bytes / Math.pow(1024, orderOfMagnitude);
+ return `${bytesInUnits.toFixed(precision)} ${units[orderOfMagnitude]}`;
+}
+
+export const FileListing: React.FC<{
+ api: string;
+ system: string;
+ path?: string;
+ scheme?: string;
+}> = ({ api, system, path = '', scheme = 'private' }) => {
+ // Base file listing for use with My Data/Community Data
+ const [previewModalState, setPreviewModalState] = useState<{
+ isOpen: boolean;
+ path?: string;
+ }>({ isOpen: false });
+
+ const columns: TFileListingColumns = useMemo(
+ () => [
+ {
+ title: 'File Name',
+ dataIndex: 'name',
+ ellipsis: true,
+ width: '50%',
+ render: (data, record) =>
+ record.type === 'dir' ? (
+
+
+
+
+ {data}
+
+ ) : (
+
+ ),
+ },
+ {
+ 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 @@
")
}
+