diff --git a/package.json b/package.json index e01151cac..19d1db043 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,7 @@ "react-dropzone": "^14.2.3", "react-error-boundary": "^4.0.13", "react-force-graph-2d": "^1.25.5", - "@chhsiao1981/use-thunk": "^9.0.3", + "@chhsiao1981/use-thunk": "^10.0.4", "react-redux": "^9.1.2", "react-resizable-panels": "^2.1.4", "react-responsive": "^10.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6ca2b3266..5f7e792e7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: specifier: ^5.5.1 version: 5.5.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@chhsiao1981/use-thunk': - specifier: ^9.0.3 - version: 9.0.3 + specifier: ^10.0.4 + version: 10.0.4 '@cornerstonejs/core': specifier: ^1.86.0 version: 1.86.1(@babel/preset-env@7.26.0(@babel/core@7.26.0))(autoprefixer@10.4.20(postcss@8.5.1))(webpack@5.97.1)(wslink@2.2.2) @@ -203,7 +203,7 @@ importers: devDependencies: '@biomejs/biome': specifier: ^2.1.4 - version: 2.1.4 + version: 2.3.11 '@faker-js/faker': specifier: ^9.0.1 version: 9.4.0 @@ -221,7 +221,7 @@ importers: version: 6.6.3 '@testing-library/react': specifier: ^16.0.1 - version: 16.2.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 16.2.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.7(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/d3-hierarchy': specifier: 3.1.7 version: 3.1.7 @@ -248,7 +248,7 @@ importers: version: 18.3.18 '@types/react-dom': specifier: ^18.3.0 - version: 18.3.5(@types/react@18.3.18) + version: 18.3.7(@types/react@18.3.18) '@types/redux-logger': specifier: ^3.0.13 version: 3.0.13 @@ -828,61 +828,61 @@ packages: resolution: {integrity: sha512-L6mZmwFDK6Cjh1nRCLXpa6no13ZIioJDz7mdkzHv399pThrTa/k0nUlNaenOeh2kWu/iaOQYElEpKPUswUa9Vg==} engines: {node: '>=6.9.0'} - '@biomejs/biome@2.1.4': - resolution: {integrity: sha512-QWlrqyxsU0FCebuMnkvBIkxvPqH89afiJzjMl+z67ybutse590jgeaFdDurE9XYtzpjRGTI1tlUZPGWmbKsElA==} + '@biomejs/biome@2.3.11': + resolution: {integrity: sha512-/zt+6qazBWguPG6+eWmiELqO+9jRsMZ/DBU3lfuU2ngtIQYzymocHhKiZRyrbra4aCOoyTg/BmY+6WH5mv9xmQ==} engines: {node: '>=14.21.3'} hasBin: true - '@biomejs/cli-darwin-arm64@2.1.4': - resolution: {integrity: sha512-sCrNENE74I9MV090Wq/9Dg7EhPudx3+5OiSoQOkIe3DLPzFARuL1dOwCWhKCpA3I5RHmbrsbNSRfZwCabwd8Qg==} + '@biomejs/cli-darwin-arm64@2.3.11': + resolution: {integrity: sha512-/uXXkBcPKVQY7rc9Ys2CrlirBJYbpESEDme7RKiBD6MmqR2w3j0+ZZXRIL2xiaNPsIMMNhP1YnA+jRRxoOAFrA==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [darwin] - '@biomejs/cli-darwin-x64@2.1.4': - resolution: {integrity: sha512-gOEICJbTCy6iruBywBDcG4X5rHMbqCPs3clh3UQ+hRKlgvJTk4NHWQAyHOXvaLe+AxD1/TNX1jbZeffBJzcrOw==} + '@biomejs/cli-darwin-x64@2.3.11': + resolution: {integrity: sha512-fh7nnvbweDPm2xEmFjfmq7zSUiox88plgdHF9OIW4i99WnXrAC3o2P3ag9judoUMv8FCSUnlwJCM1B64nO5Fbg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [darwin] - '@biomejs/cli-linux-arm64-musl@2.1.4': - resolution: {integrity: sha512-nYr7H0CyAJPaLupFE2cH16KZmRC5Z9PEftiA2vWxk+CsFkPZQ6dBRdcC6RuS+zJlPc/JOd8xw3uCCt9Pv41WvQ==} + '@biomejs/cli-linux-arm64-musl@2.3.11': + resolution: {integrity: sha512-XPSQ+XIPZMLaZ6zveQdwNjbX+QdROEd1zPgMwD47zvHV+tCGB88VH+aynyGxAHdzL+Tm/+DtKST5SECs4iwCLg==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - '@biomejs/cli-linux-arm64@2.1.4': - resolution: {integrity: sha512-juhEkdkKR4nbUi5k/KRp1ocGPNWLgFRD4NrHZSveYrD6i98pyvuzmS9yFYgOZa5JhaVqo0HPnci0+YuzSwT2fw==} + '@biomejs/cli-linux-arm64@2.3.11': + resolution: {integrity: sha512-l4xkGa9E7Uc0/05qU2lMYfN1H+fzzkHgaJoy98wO+b/7Gl78srbCRRgwYSW+BTLixTBrM6Ede5NSBwt7rd/i6g==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - '@biomejs/cli-linux-x64-musl@2.1.4': - resolution: {integrity: sha512-lvwvb2SQQHctHUKvBKptR6PLFCM7JfRjpCCrDaTmvB7EeZ5/dQJPhTYBf36BE/B4CRWR2ZiBLRYhK7hhXBCZAg==} + '@biomejs/cli-linux-x64-musl@2.3.11': + resolution: {integrity: sha512-vU7a8wLs5C9yJ4CB8a44r12aXYb8yYgBn+WeyzbMjaCMklzCv1oXr8x+VEyWodgJt9bDmhiaW/I0RHbn7rsNmw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - '@biomejs/cli-linux-x64@2.1.4': - resolution: {integrity: sha512-Eoy9ycbhpJVYuR+LskV9s3uyaIkp89+qqgqhGQsWnp/I02Uqg2fXFblHJOpGZR8AxdB9ADy87oFVxn9MpFKUrw==} + '@biomejs/cli-linux-x64@2.3.11': + resolution: {integrity: sha512-/1s9V/H3cSe0r0Mv/Z8JryF5x9ywRxywomqZVLHAoa/uN0eY7F8gEngWKNS5vbbN/BsfpCG5yeBT5ENh50Frxg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - '@biomejs/cli-win32-arm64@2.1.4': - resolution: {integrity: sha512-3WRYte7orvyi6TRfIZkDN9Jzoogbv+gSvR+b9VOXUg1We1XrjBg6WljADeVEaKTvOcpVdH0a90TwyOQ6ue4fGw==} + '@biomejs/cli-win32-arm64@2.3.11': + resolution: {integrity: sha512-PZQ6ElCOnkYapSsysiTy0+fYX+agXPlWugh6+eQ6uPKI3vKAqNp6TnMhoM3oY2NltSB89hz59o8xIfOdyhi9Iw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [win32] - '@biomejs/cli-win32-x64@2.1.4': - resolution: {integrity: sha512-tBc+W7anBPSFXGAoQW+f/+svkpt8/uXfRwDzN1DvnatkRMt16KIYpEi/iw8u9GahJlFv98kgHcIrSsZHZTR0sw==} + '@biomejs/cli-win32-x64@2.3.11': + resolution: {integrity: sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [win32] - '@chhsiao1981/use-thunk@9.0.3': - resolution: {integrity: sha512-0yAcJ6l4EKeXjzEhUCpq7mKx3FKJcZCjzvbNkW3U7VKV8NRTfQaDxxOprC4mrzP+lRhOPzCYr0tm7wL51X+a1A==} + '@chhsiao1981/use-thunk@10.0.4': + resolution: {integrity: sha512-+7ZKMC7royrqY7poOljZ/z/DARg4Ke+81blIrqsnSKIzT/OAEAnEKtgnxbNmNalgCgznHWY7wLYHzWHwYfGQFQ==} '@cornerstonejs/codec-charls@1.2.3': resolution: {integrity: sha512-qKUe6DN0dnGzhhfZLYhH9UZacMcudjxcaLXCrpxJImT/M/PQvZCT2rllu6VGJbWKJWG+dMVV2zmmleZcdJ7/cA==} @@ -1630,8 +1630,8 @@ packages: '@types/prop-types@15.7.14': resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==} - '@types/react-dom@18.3.5': - resolution: {integrity: sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==} + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} peerDependencies: '@types/react': ^18.0.0 @@ -4682,42 +4682,42 @@ snapshots: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 - '@biomejs/biome@2.1.4': + '@biomejs/biome@2.3.11': optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.1.4 - '@biomejs/cli-darwin-x64': 2.1.4 - '@biomejs/cli-linux-arm64': 2.1.4 - '@biomejs/cli-linux-arm64-musl': 2.1.4 - '@biomejs/cli-linux-x64': 2.1.4 - '@biomejs/cli-linux-x64-musl': 2.1.4 - '@biomejs/cli-win32-arm64': 2.1.4 - '@biomejs/cli-win32-x64': 2.1.4 - - '@biomejs/cli-darwin-arm64@2.1.4': + '@biomejs/cli-darwin-arm64': 2.3.11 + '@biomejs/cli-darwin-x64': 2.3.11 + '@biomejs/cli-linux-arm64': 2.3.11 + '@biomejs/cli-linux-arm64-musl': 2.3.11 + '@biomejs/cli-linux-x64': 2.3.11 + '@biomejs/cli-linux-x64-musl': 2.3.11 + '@biomejs/cli-win32-arm64': 2.3.11 + '@biomejs/cli-win32-x64': 2.3.11 + + '@biomejs/cli-darwin-arm64@2.3.11': optional: true - '@biomejs/cli-darwin-x64@2.1.4': + '@biomejs/cli-darwin-x64@2.3.11': optional: true - '@biomejs/cli-linux-arm64-musl@2.1.4': + '@biomejs/cli-linux-arm64-musl@2.3.11': optional: true - '@biomejs/cli-linux-arm64@2.1.4': + '@biomejs/cli-linux-arm64@2.3.11': optional: true - '@biomejs/cli-linux-x64-musl@2.1.4': + '@biomejs/cli-linux-x64-musl@2.3.11': optional: true - '@biomejs/cli-linux-x64@2.1.4': + '@biomejs/cli-linux-x64@2.3.11': optional: true - '@biomejs/cli-win32-arm64@2.1.4': + '@biomejs/cli-win32-arm64@2.3.11': optional: true - '@biomejs/cli-win32-x64@2.1.4': + '@biomejs/cli-win32-x64@2.3.11': optional: true - '@chhsiao1981/use-thunk@9.0.3': + '@chhsiao1981/use-thunk@10.0.4': dependencies: react: 18.3.1 uuid: 11.1.0 @@ -5338,7 +5338,7 @@ snapshots: lodash: 4.17.21 redent: 3.0.0 - '@testing-library/react@16.2.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@testing-library/react@16.2.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.7(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.26.0 '@testing-library/dom': 10.4.0 @@ -5346,7 +5346,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.18 - '@types/react-dom': 18.3.5(@types/react@18.3.18) + '@types/react-dom': 18.3.7(@types/react@18.3.18) '@tweenjs/tween.js@25.0.0': {} @@ -5455,7 +5455,7 @@ snapshots: '@types/prop-types@15.7.14': {} - '@types/react-dom@18.3.5(@types/react@18.3.18)': + '@types/react-dom@18.3.7(@types/react@18.3.18)': dependencies: '@types/react': 18.3.18 diff --git a/src/api/serverApi/dataTag.ts b/src/api/serverApi/dataTag.ts new file mode 100644 index 000000000..e760d803d --- /dev/null +++ b/src/api/serverApi/dataTag.ts @@ -0,0 +1,29 @@ +import api from "../api"; +import type { DataTag } from "../types"; + +export const getDataTags = (username = "", name = "") => { + const query: any = {}; + if (username) { + query.owner_username = username; + } + if (name) { + query.name = name; + } + + return api({ + endpoint: `/tags/search/`, + method: "get", + query, + }); +}; + +export const createDataTag = (username: string, name: string, color = "gray") => + api({ + endpoint: `/tags/`, + method: "post", + json: { + name, + owner_username: username, + color, + }, + }); diff --git a/src/api/serverApi/index.ts b/src/api/serverApi/index.ts index 34d274287..005eb60a6 100644 --- a/src/api/serverApi/index.ts +++ b/src/api/serverApi/index.ts @@ -4,6 +4,7 @@ import { updateDataName, updateDataPublic, } from "./data"; +import { createDataTag, getDataTags } from "./dataTag"; import { createDownloadToken, getLinkMap } from "./misc"; import { getPACSSeriesListBySeriesUID, @@ -42,4 +43,6 @@ export { queryPFDCMSeries, queryPFDCMStudies, retrievePFDCMPACS, + getDataTags, + createDataTag, }; diff --git a/src/api/types/dataTags.ts b/src/api/types/dataTags.ts new file mode 100644 index 000000000..42b4696d0 --- /dev/null +++ b/src/api/types/dataTags.ts @@ -0,0 +1,8 @@ +import type { ID } from "./id"; + +export interface DataTag { + id: ID; + name: string; + color: string; + owner_username: string; +} diff --git a/src/api/types/index.ts b/src/api/types/index.ts index 97a5b3829..1d15da7fb 100644 --- a/src/api/types/index.ts +++ b/src/api/types/index.ts @@ -1,4 +1,5 @@ import type { Data } from "./data"; +import type { DataTag } from "./dataTags"; import type { Datetime } from "./datetime"; import type { ID } from "./id"; import type { List } from "./list"; @@ -13,7 +14,7 @@ import type { PYPXResult, PYPXSeriesData, } from "./pacs"; -import type { UploadPipeline } from "./pipeline"; +import type { Pipeline, UploadPipeline } from "./pipeline"; import type { Pkg } from "./pkg"; import { type PkgInstance, PkgInstanceStatus } from "./pkgInstance"; import type { @@ -41,6 +42,7 @@ export type { UploadPkgNodeInfo, PkgInstance, Pkg, + Pipeline, UploadPipeline, Data, DownloadToken, @@ -48,6 +50,7 @@ export type { ID, Datetime, List, + DataTag, }; export { PkgInstanceStatus }; diff --git a/src/components/ComputePage/index.tsx b/src/components/ComputePage/index.tsx index 9e9ec315a..9bb3e110e 100644 --- a/src/components/ComputePage/index.tsx +++ b/src/components/ComputePage/index.tsx @@ -1,41 +1,16 @@ -import type { ThunkModuleToFunc, UseThunk } from "@chhsiao1981/use-thunk"; import { PageSection } from "@patternfly/react-core"; import { useEffect } from "react"; -import type * as DoDrawer from "../../reducers/drawer"; -import type * as DoFeed from "../../reducers/feed"; -import type * as DoUI from "../../reducers/ui"; -import type * as DoUser from "../../reducers/user"; import Wrapper from "../Wrapper"; import ComputeCatalog from "./ComputeCatalog"; import Title from "./Title"; -type TDoUI = ThunkModuleToFunc; -type TDoUser = ThunkModuleToFunc; -type TDoDrawer = ThunkModuleToFunc; -type TDoFeed = ThunkModuleToFunc; - -type Props = { - useUI: UseThunk; - useUser: UseThunk; - useDrawer: UseThunk; - useFeed: UseThunk; -}; - -export default (props: Props) => { - const { useUI, useUser, useDrawer, useFeed } = props; - +export default () => { useEffect(() => { document.title = "Compute Catalog"; }, []); return ( - + diff --git a/src/components/Dashboard/index.tsx b/src/components/Dashboard/index.tsx index 10687bbfa..ca4b16b36 100644 --- a/src/components/Dashboard/index.tsx +++ b/src/components/Dashboard/index.tsx @@ -17,12 +17,9 @@ import "./dashboard.css"; import { getState, type ThunkModuleToFunc, - type UseThunk, + useThunk, } from "@chhsiao1981/use-thunk"; import { useNavigate } from "react-router"; -import type * as DoDrawer from "../../reducers/drawer"; -import type * as DoFeed from "../../reducers/feed"; -import type * as DoUI from "../../reducers/ui"; import * as DoUser from "../../reducers/user"; import Title from "./Title"; import { @@ -31,20 +28,10 @@ import { lldDataset, } from "./util"; -type TDoUI = ThunkModuleToFunc; type TDoUser = ThunkModuleToFunc; -type TDoDrawer = ThunkModuleToFunc; -type TDoFeed = ThunkModuleToFunc; -type Props = { - useUI: UseThunk; - useUser: UseThunk; - useDrawer: UseThunk; - useFeed: UseThunk; -}; - -export default (props: Props) => { - const { useUI, useUser, useDrawer, useFeed } = props; +export default () => { + const useUser = useThunk(DoUser); const [classStateUser, _] = useUser; const user = getState(classStateUser) || DoUser.defaultState; const { isLoggedIn } = user; @@ -61,13 +48,7 @@ export default (props: Props) => { }, [isLoggedIn]); return ( - + diff --git a/src/components/FeedDetails/index.tsx b/src/components/FeedDetails/index.tsx index 11e29b2d9..0b4c32ea4 100644 --- a/src/components/FeedDetails/index.tsx +++ b/src/components/FeedDetails/index.tsx @@ -19,6 +19,7 @@ import { getState, type ThunkModuleToFunc, type UseThunk, + useThunk, } from "@chhsiao1981/use-thunk"; import * as DoDrawer from "../../reducers/drawer"; import * as DoFeed from "../../reducers/feed"; @@ -26,17 +27,13 @@ import * as DoFeed from "../../reducers/feed"; type TDoDrawer = ThunkModuleToFunc; type TDoFeed = ThunkModuleToFunc; -type Props = { - useDrawer: UseThunk; - useFeed: UseThunk; -}; - -export default (props: Props) => { - const { useDrawer, useFeed } = props; +export default () => { + const useDrawer = useThunk(DoDrawer); const [classStateDrawer, doDrawer] = useDrawer; const drawerState = getState(classStateDrawer) || DoDrawer.defaultState; const drawerID = getRootID(classStateDrawer); + const useFeed = useThunk(DoFeed); const [classStateFeed, _] = useFeed; const feedState = getState(classStateFeed) || DoFeed.defaultState; diff --git a/src/components/Feeds/FeedListView.tsx b/src/components/Feeds/FeedListView.tsx index 81dc4773a..3cd43a75f 100644 --- a/src/components/Feeds/FeedListView.tsx +++ b/src/components/Feeds/FeedListView.tsx @@ -2,6 +2,7 @@ import { getState, type ThunkModuleToFunc, type UseThunk, + useThunk, } from "@chhsiao1981/use-thunk"; import type { Feed, FileBrowserFolder } from "@fnndsc/chrisapi"; import { ChartDonutUtilization } from "@patternfly/react-charts"; @@ -106,14 +107,11 @@ const COLUMN_DEFINITIONS: ColumnDefinition[] = [ type Props = { title: string; isShared: boolean; - useUI: UseThunk; - useUser: UseThunk; - useDrawer: UseThunk; - useFeed: UseThunk; }; export default (props: Props) => { - const { title, isShared, useUI, useUser, useDrawer, useFeed } = props; + const { title, isShared } = props; + const useUser = useThunk(DoUser); const [classStateUser, _] = useUser; const user = getState(classStateUser) || DoUser.defaultState; const { isLoggedIn, username, isInit, isStaff } = user; @@ -276,13 +274,7 @@ export default (props: Props) => { const isMobile = useMediaQuery({ maxWidth: 768 }); return ( - + ; type TDoUser = ThunkModuleToFunc; type TDoDrawer = ThunkModuleToFunc; type TDoExplorer = ThunkModuleToFunc; type TDoFeed = ThunkModuleToFunc; -type Props = { - useUI: UseThunk; - useUser: UseThunk; - useDrawer: UseThunk; - useExplorer: UseThunk; - useFeed: UseThunk; -}; - -export default (props: Props) => { - const { useUI, useUser, useDrawer, useExplorer, useFeed } = props; - +export default () => { + const useUser = useThunk(DoUser); const [classStateUser, _] = useUser; const user = getState(classStateUser) || DoUser.defaultState; const { role, isLoggedIn, isInit, isStaff } = user; + const useDrawer = useThunk(DoDrawer); const [classStateDrawer, doDrawer] = useDrawer; const drawerState = getState(classStateDrawer) || DoDrawer.defaultState; const drawerID = getRootID(classStateDrawer); + const useExplorer = useThunk(DoExplorer); const [classStateExplorer, doExplorer] = useExplorer; const explorerID = getRootID(classStateExplorer); + const useFeed = useThunk(DoFeed); const [classStateFeed, doFeed] = useFeed; const feedID = getRootID(classStateFeed); @@ -123,7 +116,7 @@ export default (props: Props) => { const lastPluginInstance: PluginInstanceType = collectionJsonToJson( treeQuery.pluginInstances[treeQuery.pluginInstances.length - 1], - ); + ) as PluginInstanceType; const isSuccess = lastPluginInstance.status === PkgInstanceStatus.SUCCESS; @@ -196,13 +189,7 @@ export default (props: Props) => { } return ( - + {contextHolder} {/* Top Panels: Graph and Node Details */} diff --git a/src/components/GnomeLibrary/index.tsx b/src/components/GnomeLibrary/index.tsx index b7dcdbe1b..197a4d36c 100644 --- a/src/components/GnomeLibrary/index.tsx +++ b/src/components/GnomeLibrary/index.tsx @@ -1,15 +1,12 @@ import { getState, type ThunkModuleToFunc, - type UseThunk, + useThunk, } from "@chhsiao1981/use-thunk"; import type { FileBrowserFolder } from "@fnndsc/chrisapi"; import { useQueryClient } from "@tanstack/react-query"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useLocation, useNavigate } from "react-router-dom"; -import type * as DoDrawer from "../../reducers/drawer"; -import type * as DoFeed from "../../reducers/feed"; -import type * as DoUI from "../../reducers/ui"; import * as DoUser from "../../reducers/user"; import { EmptyStateComponent, InfoSection, SpinContainer } from "../Common"; import { OperationContext, OperationsProvider } from "../NewLibrary/context"; @@ -20,20 +17,10 @@ import GnomeLibrarySidebar from "./GnomeSidebar"; import styles from "./gnome.module.css"; import useFolders from "./utils/hooks/useFolders"; -type TDoDrawer = ThunkModuleToFunc; -type TDoUI = ThunkModuleToFunc; type TDoUser = ThunkModuleToFunc; -type TDoFeed = ThunkModuleToFunc; -type Props = { - useUI: UseThunk; - useUser: UseThunk; - useDrawer: UseThunk; - useFeed: UseThunk; -}; - -export default (props: Props) => { - const { useUI, useUser, useDrawer, useFeed } = props; +export default () => { + const useUser = useThunk(DoUser); const [classStateUser, _] = useUser; const user = getState(classStateUser) || DoUser.defaultState; const { username } = user; @@ -154,10 +141,6 @@ export default (props: Props) => { return ( ; type Status = "idle" | "loading" | "success" | "error"; -type Props = { - useUser: UseThunk; -}; - -export default (props: Props) => { - const { - useUser: [classStateUser, doUser], - } = props; +export default () => { + const useUser = useThunk(DoUser); + const [classStateUser, doUser] = useUser; const userID = getRootID(classStateUser); const user = getState(classStateUser) || DoUser.defaultState; + console.info("Login.index: userID:", userID, "user:", user); + const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [status, setStatus] = useState("idle"); @@ -59,7 +56,7 @@ export default (props: Props) => { e: MouseEvent, ) => { e.preventDefault(); - doUser.login(userID, username, password); + doUser.loginLegacy(userID, username, password); }; const onChangeUsername = (e: FormEvent, value: string) => { @@ -92,7 +89,7 @@ export default (props: Props) => { footerListItems={FooterListItems} textContent="ChRIS is a general-purpose, open source, distributed data and computation platform that connects a community of researchers, developers, and clinicians together." loginTitle="Log in to your account" - loginSubtitle="Enter your single sign-on LDAP credentials." + loginSubtitle="Enter your credentials." signUpForAccountMessage={signUpForAccountMessage} forgotCredentials={forgotCredentials} > diff --git a/src/components/NewStore/Title.tsx b/src/components/NewStore/Title.tsx new file mode 100644 index 000000000..36e795137 --- /dev/null +++ b/src/components/NewStore/Title.tsx @@ -0,0 +1,8 @@ +import { InfoSection } from "../Common"; + +export default () => ( + +); diff --git a/src/components/NewStore/hooks/useInfiniteScroll.ts b/src/components/NewStore/hooks/useInfiniteScroll.ts index b7f2da80e..11cf95c05 100644 --- a/src/components/NewStore/hooks/useInfiniteScroll.ts +++ b/src/components/NewStore/hooks/useInfiniteScroll.ts @@ -1,11 +1,10 @@ -// utils/useInfiniteScroll.ts import { type RefObject, useEffect } from "react"; -interface UseInfiniteScrollOptions { +type UseInfiniteScrollOptions = { fetchNextPage: () => void; hasNextPage?: boolean; threshold?: number; -} +}; /** * useInfiniteScroll @@ -13,10 +12,14 @@ interface UseInfiniteScrollOptions { * When the observed element is in view and `hasNextPage` is true, * calls `fetchNextPage`. */ -export function useInfiniteScroll( + +export const useInfiniteScroll = ( ref: RefObject, - { fetchNextPage, hasNextPage, threshold = 0.5 }: UseInfiniteScrollOptions, -) { + options: UseInfiniteScrollOptions, +) => { + const { fetchNextPage, hasNextPage, threshold: propsThreshold } = options; + const threshold = propsThreshold ?? 0.5; + useEffect(() => { if (!ref.current) return; @@ -35,4 +38,4 @@ export function useInfiniteScroll( observer.disconnect(); }; }, [ref, fetchNextPage, hasNextPage, threshold]); -} +}; diff --git a/src/components/NewStore/index.tsx b/src/components/NewStore/index.tsx index a1171b310..c1892e0c6 100644 --- a/src/components/NewStore/index.tsx +++ b/src/components/NewStore/index.tsx @@ -1,7 +1,7 @@ import { getState, type ThunkModuleToFunc, - type UseThunk, + useThunk, } from "@chhsiao1981/use-thunk"; import type { ComputeResource, Plugin } from "@fnndsc/chrisapi"; import { @@ -17,12 +17,9 @@ import { notification } from "antd"; import { useCallback, useMemo, useRef, useState } from "react"; import { useCookies } from "react-cookie"; import { createPkg } from "../../api/serverApi"; -import type { Pkg as PluginType, UploadPipeline } from "../../api/types"; -import type * as DoDrawer from "../../reducers/drawer"; -import type * as DoFeed from "../../reducers/feed"; -import type * as DoUI from "../../reducers/ui"; +import type { Pkg as PkgType, UploadPipeline } from "../../api/types"; import * as DoUser from "../../reducers/user"; -import { InfoSection, SpinContainer } from "../Common"; +import { SpinContainer } from "../Common"; import { handleInstallPlugin as onInstallPlugin } from "../PipelinesCopy/utils"; import Wrapper from "../Wrapper"; import postModifyComputeResource from "./hooks/updateComputeResource"; @@ -37,28 +34,20 @@ import { useInfiniteScroll } from "./hooks/useInfiniteScroll"; import PluginCard from "./PluginCard"; import { StoreConfigModal } from "./StoreConfigModal"; import StoreToggle from "./StoreToggle"; +import Title from "./Title"; -type TDoUI = ThunkModuleToFunc; type TDoUser = ThunkModuleToFunc; -type TDoDrawer = ThunkModuleToFunc; -type TDoFeed = ThunkModuleToFunc; - -type Props = { - useUI: UseThunk; - useUser: UseThunk; - useDrawer: UseThunk; - useFeed: UseThunk; -}; const DEFAULT_SEARCH_FIELD = "name"; const COOKIE_NAME = "storeCreds"; const COOKIE_MAX_AGE = 30 * 24 * 60 * 60; // seconds -export default (props: Props) => { - const { useUI, useUser, useDrawer, useFeed } = props; +export default () => { + const useUser = useThunk(DoUser); const [classStateUser, _] = useUser; const user = getState(classStateUser) || DoUser.defaultState; const { isStaff, isLoggedIn, token } = user; + const [cookies, setCookie, removeCookie] = useCookies([COOKIE_NAME]); const [selectedEnv, setSelectedEnv] = useState("PUBLIC ChRIS"); const [searchTerm, setSearchTerm] = useState(""); @@ -115,7 +104,7 @@ export default (props: Props) => { ) => { const hdr = getAuthHeaderOrPrompt(plugin, resources, false); if (!hdr) return; - const result: PluginType = await onInstallPlugin( + const result: PkgType = await onInstallPlugin( hdr, // @ts-expect-error { name: plugin.name, version: plugin.version, url: plugin.url }, @@ -275,15 +264,7 @@ export default (props: Props) => { return ( <> - - } - > + }> {}}> ; -type TDoUI = ThunkModuleToFunc; -type TDoUser = ThunkModuleToFunc; -type TDoFeed = ThunkModuleToFunc; - -type Props = { - useDrawer: UseThunk; - useUI: UseThunk; - useUser: UseThunk; - useFeed: UseThunk; -}; - -export default (props: Props) => { - const { useUI, useUser, useDrawer, useFeed } = props; +export default () => { return ( - + ; + services: string[]; service: string; studies: PacsStudyState[] | null; setService: (service: string) => void; diff --git a/src/components/Pacs/components/ServiceDropdown.tsx b/src/components/Pacs/components/ServiceDropdown.tsx index 1f7fc4945..2bc06c5b5 100644 --- a/src/components/Pacs/components/ServiceDropdown.tsx +++ b/src/components/Pacs/components/ServiceDropdown.tsx @@ -1,8 +1,7 @@ import { Select } from "antd"; -import type { ReadonlyNonEmptyArray } from "fp-ts/lib/ReadonlyNonEmptyArray"; type Props = { - services: ReadonlyNonEmptyArray; + services: string[]; service: string; setService: (service: string) => void; }; diff --git a/src/components/Pacs/components/helpers.ts b/src/components/Pacs/components/helpers.ts index 2438104ab..b0b8538c8 100644 --- a/src/components/Pacs/components/helpers.ts +++ b/src/components/Pacs/components/helpers.ts @@ -1,7 +1,3 @@ -import { - extract as extractFromNonEmpty, - type ReadonlyNonEmptyArray, -} from "fp-ts/ReadonlyNonEmptyArray"; import type { useSearchParams } from "react-router-dom"; import { type PacsSeriesState, SeriesPullState } from "../types.ts"; @@ -34,9 +30,10 @@ function useBooleanSearchParam( * 2. Attempts to select the first value which is not "default" (a useless, legacy pfdcm behavior) * 3. Selects the first value */ -function getDefaultPacsService( - services: ReadonlyNonEmptyArray, -): string { +function getDefaultPacsService(services: string[]): string { + if (services.length === 0) { + return ""; + } if (services.includes("PACSDCM")) { return "PACSDCM"; } @@ -45,7 +42,7 @@ function getDefaultPacsService( return service; } } - return extractFromNonEmpty(services); + return services[0]; } function isSeriesLoading({ diff --git a/src/components/Pacs/index.tsx b/src/components/Pacs/index.tsx index 15c6aa331..fbe9d2552 100644 --- a/src/components/Pacs/index.tsx +++ b/src/components/Pacs/index.tsx @@ -1,34 +1,10 @@ -import type { ThunkModuleToFunc, UseThunk } from "@chhsiao1981/use-thunk"; -import type * as DoDrawer from "../../reducers/drawer"; -import type * as DoFeed from "../../reducers/feed"; -import type * as DoUI from "../../reducers/ui"; -import type * as DoUser from "../../reducers/user"; import Wrapper from "../Wrapper"; import PacsApp from "./PacsApp.tsx"; import Title from "./Title"; -type TDoUI = ThunkModuleToFunc; -type TDoUser = ThunkModuleToFunc; -type TDoDrawer = ThunkModuleToFunc; -type TDoFeed = ThunkModuleToFunc; - -type Props = { - useUI: UseThunk; - useUser: UseThunk; - useDrawer: UseThunk; - useFeed: UseThunk; -}; - -export default (props: Props) => { - const { useUI, useUser, useDrawer, useFeed } = props; +export default () => { return ( - } - > + }> ); diff --git a/src/components/Pacs/types.ts b/src/components/Pacs/types.ts index a21d64249..b4d109c55 100644 --- a/src/components/Pacs/types.ts +++ b/src/components/Pacs/types.ts @@ -1,6 +1,6 @@ import type { PACSqueryCore } from "../../api/pfdcm"; -import type { Series, Study } from "../../api/pfdcm/models.ts"; -import type { PACSSeries } from "../../api/types.ts"; +import type { Series, Study } from "../../api/pfdcm/models"; +import type { PACSSeries } from "../../api/types"; export type StudyKey = { pacs_name: string; diff --git a/src/components/PipelinesPage/index.tsx b/src/components/PipelinesPage/index.tsx index f9870abdd..845806bd8 100644 --- a/src/components/PipelinesPage/index.tsx +++ b/src/components/PipelinesPage/index.tsx @@ -1,33 +1,20 @@ import { getState, type ThunkModuleToFunc, - type UseThunk, + useThunk, } from "@chhsiao1981/use-thunk"; import { PageSection } from "@patternfly/react-core"; import { useEffect } from "react"; -import type * as DoDrawer from "../../reducers/drawer"; -import type * as DoFeed from "../../reducers/feed"; -import type * as DoUI from "../../reducers/ui"; import * as DoUser from "../../reducers/user"; import { InfoSection } from "../Common"; import Pipelines from "../PipelinesCopy"; import { PipelineProvider } from "../PipelinesCopy/context"; import Wrapper from "../Wrapper"; -type TDoUI = ThunkModuleToFunc; type TDoUser = ThunkModuleToFunc; -type TDoDrawer = ThunkModuleToFunc; -type TDoFeed = ThunkModuleToFunc; -type Props = { - useUI: UseThunk; - useUser: UseThunk; - useDrawer: UseThunk; - useFeed: UseThunk; -}; - -export default (props: Props) => { - const { useUI, useUser, useDrawer, useFeed } = props; +export default () => { + const useUser = useThunk(DoUser); const [classStateUser, _] = useUser; const user = getState(classStateUser) || DoUser.defaultState; const { isStaff } = user; @@ -37,13 +24,7 @@ export default (props: Props) => { }, []); return ( - } - > + }> diff --git a/src/components/PluginInstall/index.tsx b/src/components/PluginInstall/index.tsx index 2d702e63b..a999c60e2 100644 --- a/src/components/PluginInstall/index.tsx +++ b/src/components/PluginInstall/index.tsx @@ -2,6 +2,7 @@ import { getState, type ThunkModuleToFunc, type UseThunk, + useThunk, } from "@chhsiao1981/use-thunk"; import { Button, @@ -15,9 +16,6 @@ import { type FormEvent, type MouseEvent, useEffect, useState } from "react"; import { Cookies, useCookies } from "react-cookie"; import { useNavigate } from "react-router"; import ChrisAPIClient from "../../api/chrisapiclient"; -import type * as DoDrawer from "../../reducers/drawer"; -import type * as DoFeed from "../../reducers/feed"; -import type * as DoUI from "../../reducers/ui"; import * as DoUser from "../../reducers/user"; import { Alert } from "../Antd"; import { SpinContainer } from "../Common"; @@ -25,23 +23,14 @@ import { useSearchQueryParams } from "../Feeds/usePaginate"; import { ExclamationCircleIcon } from "../Icons"; import Wrapper from "../Wrapper"; -type TDoUI = ThunkModuleToFunc; type TDoUser = ThunkModuleToFunc; -type TDoDrawer = ThunkModuleToFunc; -type TDoFeed = ThunkModuleToFunc; - -type Props = { - useUI: UseThunk; - useUser: UseThunk; - useDrawer: UseThunk; - useFeed: UseThunk; -}; -export default (props: Props) => { - const { useUI, useUser, useDrawer, useFeed } = props; +export default () => { + const useUser = useThunk(DoUser); const [classStateUser, _] = useUser; const user = getState(classStateUser) || DoUser.defaultState; const { isStaff } = user; + const [_cookie, setCookie] = useCookies(); const navigate = useNavigate(); const [showHelperText, setShowHelperText] = useState(false); @@ -211,12 +200,7 @@ export default (props: Props) => { ); return ( - + ; type Props = { children: JSX.Element; - useUser: UseThunk; }; export default (props: Props) => { - const { children, useUser } = props; + const { children } = props; + + const useUser = useThunk(DoUser); const [classStateUser, _] = useUser; const user = getState(classStateUser) || DoUser.defaultState; const { isLoggedIn, isInit } = user; + const redirectTo = encodeURIComponent( `${window.location.pathname}${window.location.search}`, ); diff --git a/src/components/Signup/SignUpForm.tsx b/src/components/Signup/SignUpForm.tsx index d724bf06f..b5125c0da 100644 --- a/src/components/Signup/SignUpForm.tsx +++ b/src/components/Signup/SignUpForm.tsx @@ -2,6 +2,7 @@ import { getRootID, type ThunkModuleToFunc, type UseThunk, + useThunk, } from "@chhsiao1981/use-thunk"; import { ActionGroup, @@ -19,7 +20,7 @@ import { EyeIcon, EyeSlashIcon } from "@patternfly/react-icons"; import { validate } from "email-validator"; import { type FormEvent, useState } from "react"; import { Link } from "react-router-dom"; -import type * as DoUser from "../../reducers/user"; +import * as DoUser from "../../reducers/user"; type TDoUser = ThunkModuleToFunc; @@ -28,7 +29,6 @@ type Validated = { }; type Props = { - useUser: UseThunk; isShowPasswordEnabled?: boolean; showPasswordAriaLabel?: string; hidePasswordAriaLabel?: string; @@ -36,11 +36,13 @@ type Props = { export default (props: Props) => { const { - useUser: [classStateUser, doUser], isShowPasswordEnabled: propsIsShowPasswordEnabled, showPasswordAriaLabel: propsShowPasswordAriaLabel, hidePasswordAriaLabel: propsHidePasswordAriaLabel, } = props; + const useUser = useThunk(DoUser); + const [classStateUser, doUser] = useUser; + const userID = getRootID(classStateUser); const isShowPasswordEnabled = diff --git a/src/components/Signup/index.tsx b/src/components/Signup/index.tsx index cfc500a5e..8b85a5118 100644 --- a/src/components/Signup/index.tsx +++ b/src/components/Signup/index.tsx @@ -1,20 +1,11 @@ -import type { ThunkModuleToFunc, UseThunk } from "@chhsiao1981/use-thunk"; import { LoginPage } from "@patternfly/react-core"; import { App, Spin } from "antd"; import React from "react"; import { useNavigate } from "react-router-dom"; -import type * as DoUser from "../../reducers/user"; import { useSignUpAllowed } from "../../store/hooks"; import SignUpForm from "./SignUpForm"; -type TDoUser = ThunkModuleToFunc; - -type Props = { - useUser: UseThunk; -}; -export default (props: Props) => { - const { useUser } = props; - +export default () => { const { signUpAllowed, isLoading, isError } = useSignUpAllowed(); const navigate = useNavigate(); @@ -70,7 +61,7 @@ export default (props: Props) => { ); } else { // If sign-ups are allowed, render the sign-up form - content = ; + content = ; } return ( diff --git a/src/components/SinglePlugin/index.tsx b/src/components/SinglePlugin/index.tsx index 0ad5fbe99..74a837498 100644 --- a/src/components/SinglePlugin/index.tsx +++ b/src/components/SinglePlugin/index.tsx @@ -24,27 +24,14 @@ import "./singlePlugin.css"; import { getState, type ThunkModuleToFunc, - type UseThunk, + useThunk, } from "@chhsiao1981/use-thunk"; -import type * as DoDrawer from "../../reducers/drawer"; -import type * as DoFeed from "../../reducers/feed"; -import type * as DoUI from "../../reducers/ui"; import * as DoUser from "../../reducers/user"; -type TDoUI = ThunkModuleToFunc; type TDoUser = ThunkModuleToFunc; -type TDoDrawer = ThunkModuleToFunc; -type TDoFeed = ThunkModuleToFunc; - -type Props = { - useUI: UseThunk; - useUser: UseThunk; - useDrawer: UseThunk; - useFeed: UseThunk; -}; -export default (props: Props) => { - const { useUI, useUser, useDrawer, useFeed } = props; +export default () => { + const useUser = useThunk(DoUser); const [classStateUser, _] = useUser; const user = getState(classStateUser) || DoUser.defaultState; const { isLoggedIn } = user; @@ -190,12 +177,7 @@ export default (props: Props) => { }, [data?.plugins[0], setPluginParameters]); return ( - + {isLoading || isFetching ? ( ) : isError ? ( diff --git a/src/components/Wrapper/Header.tsx b/src/components/Wrapper/Header.tsx index b5c480338..e5c9b2676 100644 --- a/src/components/Wrapper/Header.tsx +++ b/src/components/Wrapper/Header.tsx @@ -1,7 +1,9 @@ import { + getRootID, getState, type ThunkModuleToFunc, type UseThunk, + useThunk, } from "@chhsiao1981/use-thunk"; import { Masthead, @@ -10,15 +12,12 @@ import { PageToggleButton, } from "@patternfly/react-core"; import type React from "react"; -import type * as DoDrawer from "../../reducers/drawer"; import * as DoFeed from "../../reducers/feed"; import * as DoUser from "../../reducers/user"; -import { useAppSelector } from "../../store/hooks"; import { BarsIcon } from "../Icons"; import Toolbar from "./Toolbar"; type TDoUser = ThunkModuleToFunc; -type TDoDrawer = ThunkModuleToFunc; type TDoFeed = ThunkModuleToFunc; type Props = { @@ -26,26 +25,20 @@ type Props = { titleComponent?: React.ReactElement; isNavOpen?: boolean; - - useUser: UseThunk; - useDrawer: UseThunk; - useFeed: UseThunk; }; export default (props: Props) => { - const { - useUser, - useDrawer, - useFeed, - onNavToggle, - titleComponent, - isNavOpen, - } = props; + const { onNavToggle, titleComponent, isNavOpen } = props; + const useUser = useThunk(DoUser); const [classStateUser, _] = useUser; + const userID = getRootID(classStateUser); const user = getState(classStateUser) || DoUser.defaultState; + console.info("Header: user:", user, "userID:", userID); + + const useFeed = useThunk(DoFeed); const [classStateFeed, _2] = useFeed; const feed = getState(classStateFeed) || DoFeed.defaultState; @@ -74,9 +67,6 @@ export default (props: Props) => { showToolbar={showToolbar} token={user.token} title={titleComponent} - useUser={useUser} - useDrawer={useDrawer} - useFeed={useFeed} /> diff --git a/src/components/Wrapper/Sidebar.tsx b/src/components/Wrapper/Sidebar.tsx index 8b8c4afc2..3e1018bb4 100644 --- a/src/components/Wrapper/Sidebar.tsx +++ b/src/components/Wrapper/Sidebar.tsx @@ -48,15 +48,15 @@ export default (props: Props) => { const ui = getState(classStateUI) || DoUI.defaultState; const uiID = getRootID(classStateUI); const user = getState(classStateUser) || DoUser.defaultState; - const { sidebarActiveItem, isNavOpen, isTagExpanded, isPackageTagExpanded } = + const { sidebarActiveItem, isNavOpen, isTagExpanded, isPipelineTagExpanded } = ui; const { role, username } = user; - const onTagToggle = (e: FormEvent) => { + const onToggleTag = (e: FormEvent) => { doUI.setIsTagExpanded(uiID, !isTagExpanded); }; - const onPackageTagToggle = (e: FormEvent) => { - doUI.setIsPackageTagExpanded(uiID, !isPackageTagExpanded); + const onTogglePipelineTag = (e: FormEvent) => { + doUI.setIsPipelineTagExpanded(uiID, !isPipelineTagExpanded); }; const onSelect = ( _event: React.FormEvent, @@ -83,12 +83,16 @@ export default (props: Props) => { const renderTag = ( tag: TagInfo, idx: number, - onClick: (e: FormEvent) => void, + onClickMore: (e: FormEvent) => void, prefix: string, ) => { if (typeof tag.title === "undefined") { return ( - + (more) ); @@ -96,7 +100,7 @@ export default (props: Props) => { const tagIdx = `tag${idx}`; - const tagLink = tag.title === "(none)" ? "" : `/${tag.title}`; + const tagLink = tag.title === "(none)" ? "" : `${tag.title}`; return ( { itemId={tagIdx} isActive={sidebarActiveItem === tagIdx} > - {renderLink(`/${prefix}${tagLink}`, tag.title, tagIdx)} + {renderLink(`${prefix}${tagLink}`, tag.title, tagIdx)} ); }; @@ -128,21 +132,23 @@ export default (props: Props) => { return ( <> - {tagList.map((each, idx) => renderTag(each, idx, onTagToggle, "tag"))} + {tagList.map((each, idx) => + renderTag(each, idx, onToggleTag, "/data/tag/"), + )} ); }; - const renderPackageTags = () => { + const renderPipelineTags = () => { const tagList: TagInfo[] = [{ title: "imported" }, { title: "composite" }]; - if (!isPackageTagExpanded) { + if (!isPipelineTagExpanded) { tagList.push({}); } return ( <> {tagList.map((each, idx) => - renderTag(each, idx, onPackageTagToggle, "packagetag"), + renderTag(each, idx, onTogglePipelineTag, "/pipelines/tag/"), )} ); @@ -167,9 +173,9 @@ export default (props: Props) => { sidebarActiveItem === "uploadData" ? "#ffffff" : "#aaaaaa"; // only the admin can import package. - const classNameImportPackage = role === Role.Admin ? undefined : styles.hide; + const classNameImportPipeline = role === Role.Admin ? undefined : styles.hide; // only the clinician cannot compose package. - const classNameComposePackage = + const classNameComposePipeline = role === Role.Clinician ? styles.hide : undefined; return ( @@ -233,22 +239,22 @@ export default (props: Props) => { {renderLink("/pacs", "Query and Retrieve PACS", "pacs")} - + - {renderLink("/package", "Browse Packages", "catalog")} + {renderLink("/pipelines", "Browse Pipelines", "pipeline")} - {renderPackageTags()} + {renderPipelineTags()} {!isEmpty(import.meta.env.VITE_CHRIS_STORE_URL) && ( @@ -256,18 +262,18 @@ export default (props: Props) => { key="store" itemId="store" isActive={sidebarActiveItem === "store"} - className={classNameImportPackage} + className={classNameImportPipeline} > - {renderLink("/import", "Import Package", "store")} + {renderLink("/import", "Import Pipeline", "store")} )} - {renderLink("/compose", "Compose Package", "compose")} + {renderLink("/compose", "Compose Pipeline", "compose")} diff --git a/src/components/Wrapper/Toolbar.tsx b/src/components/Wrapper/Toolbar.tsx index 849a708cc..93d772936 100644 --- a/src/components/Wrapper/Toolbar.tsx +++ b/src/components/Wrapper/Toolbar.tsx @@ -2,7 +2,7 @@ import { getRootID, getState, type ThunkModuleToFunc, - type UseThunk, + useThunk, } from "@chhsiao1981/use-thunk"; import { Button, @@ -20,8 +20,6 @@ import { BarsIcon } from "@patternfly/react-icons"; // Add a tools icon import { type ReactElement, useContext, useState } from "react"; import { useMediaQuery } from "react-responsive"; import { useNavigate } from "react-router"; -import type * as DoDrawer from "../../reducers/drawer"; -import type * as DoFeed from "../../reducers/feed"; import { type Role, Roles, StaffRoles } from "../../reducers/types"; import * as DoUser from "../../reducers/user"; import { useSignUpAllowed } from "../../store/hooks"; @@ -31,33 +29,23 @@ import CartNotify from "./CartNotify"; import styles from "./Toolbar.module.css"; type TDoUser = ThunkModuleToFunc; -type TDoDrawer = ThunkModuleToFunc; -type TDoFeed = ThunkModuleToFunc; type Props = { showToolbar: boolean; title?: ReactElement; token?: string | null; - - useUser: UseThunk; - useDrawer: UseThunk; - useFeed: UseThunk; }; export default (props: Props) => { const isSmallerScreen = useMediaQuery({ maxWidth: 1224 }); const { signUpAllowed } = useSignUpAllowed(); - const { - token, - title, - useUser: [classStateUser, doUser], - useDrawer, - useFeed, - } = props; + const { token, title } = props; const navigate = useNavigate(); const { isDarkTheme, toggleTheme } = useContext(ThemeContext); + const useUser = useThunk(DoUser); + const [classStateUser, doUser] = useUser; const user = getState(classStateUser) || DoUser.defaultState; const userID = getRootID(classStateUser); const { username, role, isStaff } = user; @@ -128,9 +116,7 @@ export default (props: Props) => { {title} {/* Center */} - {props.showToolbar && !isSmallerScreen && ( - - )} + {props.showToolbar && !isSmallerScreen && } {/* Right section */} @@ -229,7 +215,7 @@ export default (props: Props) => { aria-label="Data" variant="small" > - + ); diff --git a/src/components/Wrapper/index.tsx b/src/components/Wrapper/index.tsx index 0a0e259ba..f3224c75d 100644 --- a/src/components/Wrapper/index.tsx +++ b/src/components/Wrapper/index.tsx @@ -8,37 +8,30 @@ import { getRootID, getState, type ThunkModuleToFunc, - type UseThunk, + useThunk, } from "@chhsiao1981/use-thunk"; import { Page } from "@patternfly/react-core"; -import type * as DoDrawer from "../../reducers/drawer"; -import type * as DoFeed from "../../reducers/feed"; import * as DoUI from "../../reducers/ui"; import * as DoUser from "../../reducers/user"; import { OperationsProvider } from "../NewLibrary/context"; -type TDoDrawer = ThunkModuleToFunc; type TDoUI = ThunkModuleToFunc; type TDoUser = ThunkModuleToFunc; -type TDoFeed = ThunkModuleToFunc; type Props = { children: ReactElement[] | ReactElement; title?: ReactElement; - - useUI: UseThunk; - useUser: UseThunk; - useDrawer: UseThunk; - useFeed: UseThunk; }; export default (props: Props) => { - const { children, title, useUI, useUser, useDrawer, useFeed } = props; + const { children, title } = props; + const useUI = useThunk(DoUI); const [classStateUI, doUI] = useUI; const ui = getState(classStateUI) || DoUI.defaultState; const uiID = getRootID(classStateUI); const { isNavOpen, sidebarActiveItem } = ui; + const useUser = useThunk(DoUser); const [classStateUser, _] = useUser; const user = getState(classStateUser) || DoUser.defaultState; const { isLoggedIn } = user; @@ -77,11 +70,8 @@ export default (props: Props) => { header={
} sidebar={sidebar} diff --git a/src/main.tsx b/src/main.tsx index 88f933e19..8adcece98 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -3,19 +3,45 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import App from "./App.tsx"; import { ThemeContextProvider } from "./components/DarkTheme/useTheme.tsx"; +import * as DoDataTag from "./reducers/dataTag"; +import * as DoDrawer from "./reducers/drawer"; +import * as DoExplorer from "./reducers/explorer"; +import * as DoFeed from "./reducers/feed"; +import * as DoPacs from "./reducers/pacs"; +import * as DoUI from "./reducers/ui"; +import * as DoUser from "./reducers/user"; import { setupStore } from "./store/configureStore.ts"; import "@patternfly/react-core/dist/styles/base.css"; +import { registerThunk, ThunkContext } from "@chhsiao1981/use-thunk"; + import "./main.css"; +// @ts-expect-error registerThunk +registerThunk(DoDrawer); +// @ts-expect-error registerThunk +registerThunk(DoDataTag); +// @ts-expect-error registerThunk +registerThunk(DoExplorer); +// @ts-expect-error registerThunk +registerThunk(DoFeed); +// @ts-expect-error registerThunk +registerThunk(DoPacs); +// @ts-expect-error registerThunk +registerThunk(DoUI); +// @ts-expect-error registerThunk +registerThunk(DoUser); + enableMapSet(); const store = setupStore(); const root = createRoot(document.getElementById("root")!); root.render( - - - + + + + + , ); diff --git a/src/reducers/dataTag.ts b/src/reducers/dataTag.ts new file mode 100644 index 000000000..3914b9cfc --- /dev/null +++ b/src/reducers/dataTag.ts @@ -0,0 +1,88 @@ +import { + init as _init, + type State as rState, + setData, + type Thunk, +} from "@chhsiao1981/use-thunk"; +import { STATUS_OK } from "../api/constants"; +import { createDataTag, getDataTags } from "../api/serverApi"; +import type { DataTag } from "../api/types"; + +export const myClass = "chris-ui/data-tag"; + +const MUST_HAVE_TAGS = ["uploaded", "pacs"]; + +type TagMap = { [name: string]: DataTag }; +export interface State extends rState { + tags: string[]; + tagMap: TagMap; +} + +export const defaultState: State = { + tags: [], + tagMap: {}, +}; + +export const init = (myID: string): Thunk => { + return async (dispatch, _) => { + dispatch(_init({ myID, state: defaultState })); + }; +}; + +export const ensureTags = (myID: string, username: string): Thunk => { + return async (dispatch) => { + for (const tag of MUST_HAVE_TAGS) { + const { status, data, errmsg } = await getDataTags(username, tag); + if (status !== STATUS_OK) { + return; + } + if (errmsg) { + return; + } + if (!data) { + dispatch(fetchTags(myID, username)); + return; + } + if (data.length !== 0) { + dispatch(fetchTags(myID, username)); + return; + } + + console.info( + "dataTag.ensureTags: to create data tag: username:", + username, + "tag:", + tag, + ); + await createDataTag(username, tag); + } + + dispatch(fetchTags(myID, username)); + }; +}; + +export const fetchTags = (myID: string, username: string): Thunk => { + return async (dispatch, _) => { + const { status, data: newTags, errmsg } = await getDataTags(username); + if (status !== STATUS_OK) { + return; + } + if (errmsg) { + return; + } + + if (!newTags) { + return; + } + + const sortedNewTags = newTags.sort((a, b) => b.id - a.id); + const tagMap = sortedNewTags.reduce((r: TagMap, each: DataTag) => { + r[each.name] = each; + return r; + }, {}); + + const tags = Object.keys(tagMap); + + dispatch(setData(myID, { tags, tagMap })); + }; +}; diff --git a/src/reducers/drawer.ts b/src/reducers/drawer.ts index 4f6b5de39..ff7f7fe97 100644 --- a/src/reducers/drawer.ts +++ b/src/reducers/drawer.ts @@ -81,9 +81,8 @@ const defaultStateClinician: State = { }; export const init = (): Thunk => { - const myID = genUUID(); return (dispatch, _) => { - dispatch(_init({ myID, state: defaultState })); + dispatch(_init({ state: defaultState })); }; }; diff --git a/src/reducers/feed.ts b/src/reducers/feed.ts index 2db7c2155..d5a1419bf 100644 --- a/src/reducers/feed.ts +++ b/src/reducers/feed.ts @@ -25,9 +25,8 @@ export const defaultState: State = { }; export const init = (): Thunk => { - const myID = genUUID(); return (dispatch, _) => { - dispatch(_init({ myID, state: defaultState })); + dispatch(_init({ state: defaultState })); }; }; diff --git a/src/reducers/pacs.ts b/src/reducers/pacs.ts index 0f84f94b0..32a583d65 100644 --- a/src/reducers/pacs.ts +++ b/src/reducers/pacs.ts @@ -8,7 +8,6 @@ import { type Thunk, } from "@chhsiao1981/use-thunk"; import config from "config"; -import type { ReadonlyNonEmptyArray } from "fp-ts/lib/ReadonlyNonEmptyArray"; import type { Location } from "react-router"; import { STATUS_OK, STATUS_OK_CREATE } from "../api/constants"; import type { SeriesKey } from "../api/lonk"; @@ -56,7 +55,7 @@ export type PacsSeriesMap = { [key: string]: PacsSeriesState }; export interface State extends rState { pullRequestMap: PacsPullRequestStateMap; - services: ReadonlyNonEmptyArray; + services: string[]; service: string; isGetServices: boolean; @@ -88,7 +87,7 @@ export interface State extends rState { const studyKeyToStudyMapKey = (studyKey: StudyKey) => studyUIDToStudyMapKey(studyKey.pacs_name, studyKey.StudyInstanceUID); -const defaultServices: ReadonlyNonEmptyArray = ["(none)"]; +const defaultServices: string[] = ["(none)"]; export const defaultState: State = { pullRequestMap: {}, @@ -125,7 +124,6 @@ export const init = (myID: string): Thunk => { return async (dispatch, _) => { dispatch(_init({ myID, state: defaultState })); dispatch(getServices(myID)); - // dispatch(getWsUrl(myID)); }; }; diff --git a/src/reducers/ui.ts b/src/reducers/ui.ts index df960be9f..26a7da916 100644 --- a/src/reducers/ui.ts +++ b/src/reducers/ui.ts @@ -5,20 +5,21 @@ import { type Thunk, } from "@chhsiao1981/use-thunk"; +// This is actually the sidebar UI. export const myClass = "chris-ui/ui"; export interface State extends rState { isNavOpen?: boolean; sidebarActiveItem?: string; isTagExpanded: boolean; - isPackageTagExpanded: boolean; + isPipelineTagExpanded: boolean; } export const defaultState: State = { isNavOpen: true, sidebarActiveItem: "overview", isTagExpanded: false, - isPackageTagExpanded: false, + isPipelineTagExpanded: false, }; export const init = (myID: string): Thunk => { @@ -54,7 +55,7 @@ export const setIsTagExpanded = ( }; }; -export const setIsPackageTagExpanded = ( +export const setIsPipelineTagExpanded = ( myID: string, isPackageTagExpanded: boolean, ): Thunk => { diff --git a/src/reducers/user.ts b/src/reducers/user.ts index 18b219210..9b56b2702 100644 --- a/src/reducers/user.ts +++ b/src/reducers/user.ts @@ -1,10 +1,11 @@ import { init as _init, - genUUID, + type DispatchFuncMap, getState, type State as rState, setData, type Thunk, + type ThunkModuleToFunc, } from "@chhsiao1981/use-thunk"; import queryString from "query-string"; import { Cookies } from "react-cookie"; @@ -16,8 +17,11 @@ import { getUser, getUserID, } from "../api/serverApi"; +import type * as DoDataTag from "./dataTag"; import { Role } from "./types"; +type TDoDataTag = ThunkModuleToFunc; + export const myClass = "chris-ui/user"; export interface State extends rState { @@ -50,9 +54,10 @@ export const defaultState: State = { role: Role.Guest, }; -export const init = (): Thunk => { - const myID = genUUID(); - +export const init = ( + dataTagID: string, + doDataTag: DispatchFuncMap, +): Thunk => { return async (dispatch, _) => { const cookie = new Cookies(); const username = cookie.get("username") || ""; @@ -68,17 +73,6 @@ export const init = (): Thunk => { isLoggedIn = !!userID; } - console.info( - "user.init: username:", - username, - "token:", - token, - "isStaff:", - isStaff, - "role:", - role, - ); - const state: State = Object.assign({}, defaultState, { username, token, @@ -88,11 +82,14 @@ export const init = (): Thunk => { isLoggedIn, }); - dispatch(_init({ myID, state })); + dispatch(_init({ state })); + if (isLoggedIn) { + doDataTag.ensureTags(dataTagID, username); + } }; }; -export const login = ( +export const loginLegacy = ( myID: string, username: string, password: string, diff --git a/src/routes.tsx b/src/routes.tsx index 306ab96f3..75e0081b4 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -1,5 +1,6 @@ import { genUUID, + getRootID, getState, type ThunkModuleToFunc, useThunk, @@ -30,6 +31,7 @@ import { } from "./components/Routing/RouterContext"; import Signup from "./components/Signup"; import SinglePlugin from "./components/SinglePlugin"; +import * as DoDataTag from "./reducers/dataTag"; import * as DoDrawer from "./reducers/drawer"; import * as DoExplorer from "./reducers/explorer"; import * as DoFeed from "./reducers/feed"; @@ -41,6 +43,7 @@ type TDoUser = ThunkModuleToFunc; type TDoDrawer = ThunkModuleToFunc; type TDoExplorer = ThunkModuleToFunc; type TDoFeed = ThunkModuleToFunc; +type TDoDataTag = ThunkModuleToFunc; interface State { selectData?: Series; @@ -65,23 +68,25 @@ const _ROUTE_TO_SIDEBAR_ITEM: Record = { "library/*": "lib", "data/*": "data", "data/:id": "data", + "data/tag/*": "data-tag", + "data/tag/uploaded": "data-tag-uploaded", + "data/tag/pacs": "data-tag-pacs", "shared/*": "shared", new: "new", pacs: "pacs", login: "login", signup: "signup", - "package/*": "package", - "package/:id": "package", + "pipelines/*": "pipeline", + "pipelines/tag/:tag": "pipeline-tag", + "pipelines/tag/imported": "pipeline-tag-imported", + "pipelines/tag/composite": "pipeline-tag-composite", + "pipeline/*": "pipeline", + "pipeline/:id": "pipeline", import: "import", compose: "compose", - "niivue/:plinstId": "niivue", store: "store", "install/*": "install", "*": "notFound", - - tag: "tag0", - "tag/uploaded": "tag1", - "tag/pacs": "tag2", }; export default () => { @@ -90,23 +95,31 @@ export default () => { const [route, setRoute] = useState(""); const navigate = useNavigate(); - const [uiID, _] = useState(genUUID()); + const [uiID, _] = useState(genUUID); + const [dataTagID, _6] = useState(genUUID); const useUI = useThunk(DoUI); const [_2, doUI] = useUI; + const useUser = useThunk(DoUser); const [classStateUser, doUser] = useUser; + const userID = getRootID(classStateUser); const user = getState(classStateUser) || DoUser.defaultState; const { isLoggedIn } = user; + console.info("routes: user:", user, "userID:", userID); + const useDrawer = useThunk(DoDrawer); const [_3, doDrawer] = useDrawer; const useExplorer = useThunk(DoExplorer); const [_4, doExplorer] = useExplorer; + const useDataTag = useThunk(DoDataTag); + const [_7, doDataTag] = useDataTag; + const useFeed = useThunk(DoFeed); - const [_5, doFeed] = useExplorer; + const [_8, doFeed] = useFeed; console.info("routes: start: route:", route); @@ -145,7 +158,8 @@ export default () => { useEffect(() => { doUI.init(uiID); - doUser.init(); + doDataTag.init(dataTagID); + doUser.init(dataTagID, doDataTag); doDrawer.init(); doExplorer.init(); doFeed.init(); @@ -161,35 +175,75 @@ export default () => { return useRoutes([ { path: "/", - element: ( - - ), + element: , }, { path: "library/*", element: ( - + - + ), }, + { + path: "data/tag/uploaded", + element: ( + + + + + + ), + }, + { + path: "data/tag/public", + element: ( + + + + + + ), + }, + { + path: "data/tag/pacs", + element: ( + + + + + + ), + }, + { + path: "data/tag/:id", + element: ( + + + + + + ), + }, { path: "data/:id", element: ( @@ -198,13 +252,7 @@ export default () => { context={MainRouterContext} > - + ), @@ -217,14 +265,7 @@ export default () => { context={MainRouterContext} > - + ), @@ -237,104 +278,62 @@ export default () => { context={MainRouterContext} > - + ), }, { - path: "package/:id", - element: ( - - ), + path: "pipeline/:id", + element: , }, { path: "pacs", element: ( - - + + ), }, { path: "login", - element: , + element: , }, { path: "signup", - element: , + element: , }, { - path: "package", - element: ( - - ), + path: "pipelines", + element: , + }, + { + path: "pipelines/tag/imported", + element: , + }, + { + path: "pipelines/tag/composite", + element: , + }, + { + path: "pipelines/tag/:id", + element: , }, { path: "compute", - element: ( - - ), + element: , }, { path: "import", - element: ( - - ), + element: , }, { path: "install/*", - element: ( - - ), + element: , }, { path: "*", - element: ( - - ), + element: , }, ]); };