From b38165d2f41dc553a8612ea0acc233ea9f69d7ae Mon Sep 17 00:00:00 2001 From: Jessie Wei Date: Wed, 19 Jun 2024 08:25:56 +1000 Subject: [PATCH] fix: Fix workspace urls to allow direct navigations to the workspace relative pages (#120) * Revert "Revert "fix: Fix workspace urls to allow direct navigations to the workspace relative pages (#119)"" This reverts commit 22b3f460840af6039d8420db9021ff7f8140f745. * chore: Fix the github page redirect issue --- .projen/tasks.json | 4 + .../threat-composer-app/.projen/deps.json | 4 + .../threat-composer-app/.projen/tasks.json | 4 +- packages/threat-composer-app/package.json | 1 + .../GithubPagesNavigationHelper/index.tsx | 47 ---- .../MarkdownEditorWithPrompt/index.tsx | 35 +++ .../threat-composer-app/src/config/appMode.ts | 28 +++ .../threat-composer-app/src/config/routes.ts | 46 ++-- .../containers/App/components/Full/index.tsx | 231 +----------------- .../src/containers/App/index.tsx | 20 +- .../src/containers/AppLayout/index.tsx | 132 ++++++++++ .../src/containers/AppRoot/index.tsx | 33 +++ .../src/containers/Application/index.tsx | 3 +- .../src/containers/Architecture/index.tsx | 3 +- .../src/containers/Dataflow/index.tsx | 3 +- .../containers/ThreatModelPreview/index.tsx | 47 ++++ .../containers/ThreatModelReport/index.tsx | 62 ++--- .../ThreatStatementEditor/index.tsx | 5 +- .../containers/ThreatStatementList/index.tsx | 14 +- .../src/containers/WorkspaceHome/index.tsx | 21 +- .../src/containers/WorkspaceRoot/index.tsx | 81 ++++++ .../containers/WorkspaceSelector/index.tsx | 44 ++++ .../src/hooks/useFeatures/index.tsx | 27 ++ .../src/hooks/useNavigationView/index.tsx | 39 +++ .../src/hooks/useOnPreview/index.ts | 37 +++ packages/threat-composer-app/src/index.tsx | 7 +- .../threat-composer-app/src/routes/index.tsx | 103 +++++--- .../src/routes/initialWorkspaceLoader.tsx | 64 +++++ .../src/utils/generateUrl/index.ts | 5 +- .../src/utils/getComposerMode/index.tsx | 28 +++ .../application/ApplicationInfo/index.tsx | 3 +- .../generic/BaseDiagramInfo/index.tsx | 3 +- .../threat-composer/src/components/index.ts | 1 + .../components/report/ThreatModel/index.tsx | 27 +- .../threats/ThreatStatementEditor/index.tsx | 15 +- .../threats/ThreatStatementList/index.tsx | 5 +- .../workspaces/EditWorkspace/index.tsx | 6 +- .../workspaces/LandingPage/index.tsx | 10 +- .../workspaces/WorkspaceHome/index.tsx | 9 +- .../components/Overview/index.tsx | 7 +- .../components/STRIDEAllocation/index.tsx | 12 +- .../components/ThreatGrammar/index.tsx | 9 +- .../components/ThreatPrioritization/index.tsx | 12 +- .../hooks/useLinkClicked/index.tsx | 6 +- .../workspaces/WorkspaceInsights/index.tsx | 17 +- .../workspaces/WorkspaceInsights/types.tsx | 22 ++ .../workspaces/WorkspaceSelector/index.tsx | 11 +- .../src/contexts/ContextAggregator/index.tsx | 14 +- .../contexts/GlobalSetupContext/context.ts | 8 +- .../src/contexts/GlobalSetupContext/index.tsx | 16 +- .../LocalStateContextProvider/index.tsx | 4 - .../LocalStorageContextProvider/index.tsx | 4 - .../src/contexts/ThreatsContext/context.ts | 4 +- .../WorkspaceContextAggregator/index.tsx | 12 +- .../LocalStateContextProvider/index.tsx | 42 ++-- .../LocalStorageContextProvider/index.tsx | 50 ++-- .../src/contexts/WorkspacesContext/context.ts | 4 +- .../src/contexts/WorkspacesContext/index.tsx | 6 +- .../src/contexts/WorkspacesContext/types.ts | 2 +- .../WorkspacesContext/useWorkspaces.ts | 6 +- .../threat-composer/src/contexts/index.ts | 1 + .../src/customTypes/components.ts | 4 + .../threat-composer/src/customTypes/events.ts | 2 +- projenrc/app.ts | 1 + yarn.lock | 5 + 65 files changed, 984 insertions(+), 554 deletions(-) delete mode 100644 packages/threat-composer-app/src/components/GithubPagesNavigationHelper/index.tsx create mode 100644 packages/threat-composer-app/src/components/MarkdownEditorWithPrompt/index.tsx create mode 100644 packages/threat-composer-app/src/config/appMode.ts create mode 100644 packages/threat-composer-app/src/containers/AppLayout/index.tsx create mode 100644 packages/threat-composer-app/src/containers/AppRoot/index.tsx create mode 100644 packages/threat-composer-app/src/containers/ThreatModelPreview/index.tsx create mode 100644 packages/threat-composer-app/src/containers/WorkspaceRoot/index.tsx create mode 100644 packages/threat-composer-app/src/containers/WorkspaceSelector/index.tsx create mode 100644 packages/threat-composer-app/src/hooks/useFeatures/index.tsx create mode 100644 packages/threat-composer-app/src/hooks/useNavigationView/index.tsx create mode 100644 packages/threat-composer-app/src/hooks/useOnPreview/index.ts create mode 100644 packages/threat-composer-app/src/routes/initialWorkspaceLoader.tsx create mode 100644 packages/threat-composer-app/src/utils/getComposerMode/index.tsx create mode 100644 packages/threat-composer/src/components/workspaces/WorkspaceInsights/types.tsx diff --git a/.projen/tasks.json b/.projen/tasks.json index 5998890d..f4bbcba9 100644 --- a/.projen/tasks.json +++ b/.projen/tasks.json @@ -134,6 +134,10 @@ "steps": [ { "exec": "yarn install --check-files --frozen-lockfile" + }, + { + "exec": "yarn nx run-many --target=install:ci --output-style=stream --nx-bail", + "receiveArgs": true } ] }, diff --git a/packages/threat-composer-app/.projen/deps.json b/packages/threat-composer-app/.projen/deps.json index e769df32..972bb8dc 100644 --- a/packages/threat-composer-app/.projen/deps.json +++ b/packages/threat-composer-app/.projen/deps.json @@ -113,6 +113,10 @@ "name": "@cloudscape-design/global-styles", "type": "runtime" }, + { + "name": "@uidotdev/usehooks", + "type": "runtime" + }, { "name": "docx", "type": "runtime" diff --git a/packages/threat-composer-app/.projen/tasks.json b/packages/threat-composer-app/.projen/tasks.json index feacbf98..816e9526 100644 --- a/packages/threat-composer-app/.projen/tasks.json +++ b/packages/threat-composer-app/.projen/tasks.json @@ -145,13 +145,13 @@ }, "steps": [ { - "exec": "npx npm-check-updates@16 --upgrade --target=minor --peer --dep=dev,peer,prod,optional --filter=@cloudscape-design/jest-preset,@testing-library/jest-dom,@testing-library/react,@testing-library/user-event,@types/jest,@types/react,@types/react-dom,@types/react-router-dom,@types/uuid,eslint-import-resolver-typescript,eslint-plugin-import,merge,@aws-northstar/ui,@aws/threat-composer,@cloudscape-design/components,@cloudscape-design/design-tokens,@cloudscape-design/global-styles,docx,react,react-dom,react-router-dom,unist-util-visit,uuid,web-vitals" + "exec": "npx npm-check-updates@16 --upgrade --target=minor --peer --dep=dev,peer,prod,optional --filter=@cloudscape-design/jest-preset,@testing-library/jest-dom,@testing-library/react,@testing-library/user-event,@types/jest,@types/react,@types/react-dom,@types/react-router-dom,@types/uuid,eslint-import-resolver-typescript,eslint-plugin-import,merge,@aws-northstar/ui,@aws/threat-composer,@cloudscape-design/components,@cloudscape-design/design-tokens,@cloudscape-design/global-styles,@uidotdev/usehooks,docx,react,react-dom,react-router-dom,unist-util-visit,uuid,web-vitals" }, { "exec": "yarn install --check-files" }, { - "exec": "yarn upgrade @cloudscape-design/jest-preset @testing-library/jest-dom @testing-library/react @testing-library/user-event @types/jest @types/node @types/react @types/react-dom @types/react-router-dom @types/uuid @typescript-eslint/eslint-plugin @typescript-eslint/parser constructs eslint-import-resolver-typescript eslint-plugin-import eslint merge projen typescript @aws-northstar/ui @aws/threat-composer @cloudscape-design/components @cloudscape-design/design-tokens @cloudscape-design/global-styles docx react react-dom react-router-dom react-scripts remark-frontmatter remark-gfm remark-parse unified unist-util-visit uuid web-vitals" + "exec": "yarn upgrade @cloudscape-design/jest-preset @testing-library/jest-dom @testing-library/react @testing-library/user-event @types/jest @types/node @types/react @types/react-dom @types/react-router-dom @types/uuid @typescript-eslint/eslint-plugin @typescript-eslint/parser constructs eslint-import-resolver-typescript eslint-plugin-import eslint merge projen typescript @aws-northstar/ui @aws/threat-composer @cloudscape-design/components @cloudscape-design/design-tokens @cloudscape-design/global-styles @uidotdev/usehooks docx react react-dom react-router-dom react-scripts remark-frontmatter remark-gfm remark-parse unified unist-util-visit uuid web-vitals" }, { "exec": "npx projen" diff --git a/packages/threat-composer-app/package.json b/packages/threat-composer-app/package.json index 58b5980b..052a660d 100644 --- a/packages/threat-composer-app/package.json +++ b/packages/threat-composer-app/package.json @@ -44,6 +44,7 @@ "@cloudscape-design/components": "^3.0.517", "@cloudscape-design/design-tokens": "^3.0.34", "@cloudscape-design/global-styles": "^1.0.23", + "@uidotdev/usehooks": "^2.4.1", "docx": "^8.5.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/packages/threat-composer-app/src/components/GithubPagesNavigationHelper/index.tsx b/packages/threat-composer-app/src/components/GithubPagesNavigationHelper/index.tsx deleted file mode 100644 index 4a51a514..00000000 --- a/packages/threat-composer-app/src/components/GithubPagesNavigationHelper/index.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/** ******************************************************************************************************************* - Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - - Licensed under the Apache License, Version 2.0 (the "License"). - You may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - ******************************************************************************************************************** */ -import { useEffect, FC, PropsWithChildren } from 'react'; -import { useNavigate } from 'react-router-dom'; - -const requiredRewriteUrl = (search: string) => { - return search && (search.startsWith('?/') || search.startsWith('?%2F')); -}; - -const ROUTE_BASE_PATH = process.env.REACT_APP_ROUTE_BASE_PATH || ''; - -const GithubPagesNavigationHelper: FC> = ({ - children, -}) => { - const navigate = useNavigate(); - useEffect(() => { - const l = window.location; - if (requiredRewriteUrl(l.search)) { - let search = decodeURIComponent(l.search); - if (search.indexOf('=') === search.length - 1) { - search = search.slice(0, search.length - 1); - } - var decoded = search.slice(1).split('&').map(function (s) { - return s.replace(/~and~/g, '&'); - }).join('?'); - - navigate(ROUTE_BASE_PATH + '/' + decoded + l.hash); - } - }, [window.location.search]); - - return requiredRewriteUrl(window.location.search) ? <> : <>{children}; -}; - -export default GithubPagesNavigationHelper; \ No newline at end of file diff --git a/packages/threat-composer-app/src/components/MarkdownEditorWithPrompt/index.tsx b/packages/threat-composer-app/src/components/MarkdownEditorWithPrompt/index.tsx new file mode 100644 index 00000000..6704024f --- /dev/null +++ b/packages/threat-composer-app/src/components/MarkdownEditorWithPrompt/index.tsx @@ -0,0 +1,35 @@ +/** ******************************************************************************************************************* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ******************************************************************************************************************** */ +import { MarkdownEditor, MarkdownEditorProps } from '@aws/threat-composer'; +import { FC, useState } from 'react'; +import { unstable_usePrompt } from 'react-router-dom'; + +const MarkdownEditorWithPrompt: FC = ({ + value, ...props +}) => { + const [previousValue] = useState(value); + + unstable_usePrompt({ + message: 'You have unsaved changes, proceed anyway?', + when: ({ currentLocation, nextLocation }) => + previousValue !== value && + currentLocation.pathname !== nextLocation.pathname, + }); + + return ; +}; + +export default MarkdownEditorWithPrompt; \ No newline at end of file diff --git a/packages/threat-composer-app/src/config/appMode.ts b/packages/threat-composer-app/src/config/appMode.ts new file mode 100644 index 00000000..765b59d3 --- /dev/null +++ b/packages/threat-composer-app/src/config/appMode.ts @@ -0,0 +1,28 @@ +/** ******************************************************************************************************************* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ******************************************************************************************************************** */ + +import { + APP_MODE_BROWSER_EXTENSION, + APP_MODE_IDE_EXTENSION, +} from '@aws/threat-composer'; + +const appModeEnv = process.env.REACT_APP_APP_MODE; + +export const appMode = appModeEnv === APP_MODE_BROWSER_EXTENSION ? + APP_MODE_BROWSER_EXTENSION : + (appModeEnv === APP_MODE_IDE_EXTENSION ? + APP_MODE_IDE_EXTENSION : undefined); + diff --git a/packages/threat-composer-app/src/config/routes.ts b/packages/threat-composer-app/src/config/routes.ts index ec54da07..392a6987 100644 --- a/packages/threat-composer-app/src/config/routes.ts +++ b/packages/threat-composer-app/src/config/routes.ts @@ -13,17 +13,35 @@ See the License for the specific language governing permissions and limitations under the License. ******************************************************************************************************************** */ -export const ROUTE_WORKSPACE_HOME = '/workspaces/:workspaceId/dashboard'; -export const ROUTE_THREAT_LIST = '/workspaces/:workspaceId/threats'; -export const ROUTE_THREAT_EDITOR = '/workspaces/:workspaceId/threats/:threatId'; -export const ROUTE_MITIGATION_LIST = '/workspaces/:workspaceId/mitigations'; -export const ROUTE_ASSUMPTION_LIST = '/workspaces/:workspaceId/assumptions'; -export const ROUTE_APPLICATION_INFO = '/workspaces/:workspaceId/application'; -export const ROUTE_ARCHITECTURE_INFO = '/workspaces/:workspaceId/architecture'; -export const ROUTE_DATAFLOW_INFO = '/workspaces/:workspaceId/dataflow'; -export const ROUTE_VIEW_THREAT_MODEL = '/workspaces/:workspaceId/threatModel'; -export const ROUTE_THREAT_PACKS = '/workspaces/:workspaceId/threatPacks'; -export const ROUTE_THREAT_PACK = '/workspaces/:workspaceId/threatPacks/:threatPackId'; -export const ROUTE_THREAT_PACK_THREAT = '/workspaces/:workspaceId/threatPacks/:threatPackId/:threatId'; -export const ROUTE_MITIGATION_PACKS = '/workspaces/:workspaceId/mitigationPacks'; -export const ROUTE_MITIGATION_PACK = '/workspaces/:workspaceId/mitigationPacks/:mitigationPackId'; +export const ROUTE_WORKSPACE_PATH = 'workspaces/:workspaceId'; +export const ROUTE_WORKSPACE_HOME_PATH = 'dashboard'; +export const ROUTE_WORKSPACE_HOME = `/${ROUTE_WORKSPACE_PATH}/${ROUTE_WORKSPACE_HOME_PATH}`; +export const ROUTE_THREAT_LIST_PATH = 'threats'; +export const ROUTE_THREAT_LIST = `/${ROUTE_WORKSPACE_PATH}/${ROUTE_THREAT_LIST_PATH}`; +export const ROUTE_THREAT_EDITOR_PATH = 'threats/:threatId'; +export const ROUTE_THREAT_EDITOR = `/${ROUTE_WORKSPACE_PATH}/${ROUTE_THREAT_EDITOR_PATH}`; +export const ROUTE_MITIGATION_LIST_PATH = 'mitigations'; +export const ROUTE_MITIGATION_LIST = `/${ROUTE_WORKSPACE_PATH}/${ROUTE_MITIGATION_LIST_PATH}`; +export const ROUTE_ASSUMPTION_LIST_PATH = 'assumptions'; +export const ROUTE_ASSUMPTION_LIST = `/${ROUTE_WORKSPACE_PATH}/${ROUTE_ASSUMPTION_LIST_PATH}`; +export const ROUTE_APPLICATION_INFO_PATH = 'application'; +export const ROUTE_APPLICATION_INFO = `/${ROUTE_WORKSPACE_PATH}/${ROUTE_APPLICATION_INFO_PATH}`; +export const ROUTE_ARCHITECTURE_INFO_PATH = 'architecture'; +export const ROUTE_ARCHITECTURE_INFO = `/${ROUTE_WORKSPACE_PATH}/${ROUTE_ARCHITECTURE_INFO_PATH}`; +export const ROUTE_DATAFLOW_INFO_PATH = 'dataflow'; +export const ROUTE_DATAFLOW_INFO = `/${ROUTE_WORKSPACE_PATH}/${ROUTE_DATAFLOW_INFO_PATH}`; +export const ROUTE_VIEW_THREAT_MODEL_PATH = 'threatModel'; +export const ROUTE_VIEW_THREAT_MODEL = `/${ROUTE_WORKSPACE_PATH}/${ROUTE_VIEW_THREAT_MODEL_PATH}`; +export const ROUTE_THREAT_PACKS_PATH = 'threatPacks'; +export const ROUTE_THREAT_PACKS = `/${ROUTE_WORKSPACE_PATH}/${ROUTE_THREAT_PACKS_PATH}`; +export const ROUTE_THREAT_PACK_PATH = 'threatPacks/:threatPackId'; +export const ROUTE_THREAT_PACK = `/${ROUTE_WORKSPACE_PATH}/${ROUTE_THREAT_PACK_PATH}`; +export const ROUTE_THREAT_PACK_THREAT_PATH = 'threatPacks/:threatPackId/:threatId'; +export const ROUTE_THREAT_PACK_THREAT = `/${ROUTE_WORKSPACE_PATH}/${ROUTE_THREAT_PACK_THREAT_PATH}`; +export const ROUTE_MITIGATION_PACKS_PATH = 'mitigationPacks'; +export const ROUTE_MITIGATION_PACKS = `/${ROUTE_WORKSPACE_PATH}/${ROUTE_MITIGATION_PACKS_PATH}`; +export const ROUTE_MITIGATION_PACK_PATH = 'mitigationPacks/:mitigationPackId'; +export const ROUTE_MITIGATION_PACK = `/${ROUTE_WORKSPACE_PATH}/${ROUTE_MITIGATION_PACK_PATH}`; +export const ROUTE_PREVIEW_PATH = 'preview/:dataKey'; +export const ROUTE_PREVIEW = `/${ROUTE_PREVIEW_PATH}`; +export const ROUTE_WORKSPACE_DEFAULT = 'workspaces/default'; diff --git a/packages/threat-composer-app/src/containers/App/components/Full/index.tsx b/packages/threat-composer-app/src/containers/App/components/Full/index.tsx index 9c4fe513..895d768a 100644 --- a/packages/threat-composer-app/src/containers/App/components/Full/index.tsx +++ b/packages/threat-composer-app/src/containers/App/components/Full/index.tsx @@ -13,235 +13,18 @@ See the License for the specific language governing permissions and limitations under the License. ******************************************************************************************************************** */ -import { - ContextAggregator, - DataExchangeFormat, - ThreatStatementListFilter, - WorkspaceSelector, - useWorkspacesContext, - APP_MODE_BROWSER_EXTENSION, - APP_MODE_IDE_EXTENSION, -} from '@aws/threat-composer'; -import { SideNavigationProps } from '@cloudscape-design/components/side-navigation'; -import React, { FC, useMemo, useCallback, useState, useEffect } from 'react'; -import { Routes, Route, RouteProps, useParams, useSearchParams, useNavigate, Navigate } from 'react-router-dom'; -import AppLayout from '../../../../components/FullAppLayout'; -import { - ROUTE_APPLICATION_INFO, - ROUTE_ARCHITECTURE_INFO, - ROUTE_ASSUMPTION_LIST, - ROUTE_DATAFLOW_INFO, - ROUTE_THREAT_PACKS, - ROUTE_MITIGATION_LIST, - ROUTE_THREAT_EDITOR, - ROUTE_THREAT_LIST, - ROUTE_VIEW_THREAT_MODEL, - ROUTE_WORKSPACE_HOME, - ROUTE_MITIGATION_PACKS, -} from '../../../../config/routes'; -import { SEARCH_PARAM_FEATURES } from '../../../../config/searchParams'; -import useNotifications from '../../../../hooks/useNotifications'; -import routes from '../../../../routes'; -import generateUrl from '../../../../utils/generateUrl'; -import ThreatModelReport from '../../../ThreatModelReport'; - -const TEMP_PREVIEW_DATA_KEY = 'ThreatStatementGenerator.TempPreviewData'; - -const defaultHref = process.env.PUBLIC_URL || '/'; -const appModeEnv = process.env.REACT_APP_APP_MODE; - -const appMode = appModeEnv === APP_MODE_BROWSER_EXTENSION ? - APP_MODE_BROWSER_EXTENSION : - (appModeEnv === APP_MODE_IDE_EXTENSION ? - APP_MODE_IDE_EXTENSION : undefined); - -const AppInner: FC<{ - setWorkspaceId: React.Dispatch>; -}> = ({ setWorkspaceId }) => { - const { currentWorkspace } = useWorkspacesContext(); - const [searchParms] = useSearchParams(); - useEffect(() => { - setWorkspaceId(currentWorkspace?.id || 'default'); - }, [currentWorkspace]); - - const workspaceHome = generateUrl(ROUTE_WORKSPACE_HOME, searchParms, currentWorkspace?.id || 'default'); - - return ( - } /> - {routes.map((r: RouteProps, index: number) => )} - } /> - ); -}; +import { FC, useState } from 'react'; +import { RouterProvider } from 'react-router-dom'; +import { routerOpts, createRouter, routes } from '../../../../routes'; const Full: FC = () => { - const { workspaceId: initialWorkspaceId } = useParams(); - const [searchParms] = useSearchParams(); - const navigate = useNavigate(); - - const [isPreview] = useState(() => { - const urlParams = new URLSearchParams(window.location.search); - const previewParams = urlParams.get('preview'); - return previewParams === 'true'; + const [router] = useState(() => { + //Initialize here to ensure the default index loader only call when Full is mount without affecting Standalone mode + return createRouter(routes, routerOpts); }); - const [features] = useState(() => { - const urlParams = new URLSearchParams(window.location.search); - const featureParam = urlParams.get(SEARCH_PARAM_FEATURES); - return featureParam && featureParam.split(',') || []; - }); - - const isThreatPackFeatureOn = useMemo(() => { - return features.includes('threatPacks'); - }, [features]); - - const [workspaceId, setWorkspaceId] = useState(initialWorkspaceId || 'default'); - - const handleWorkspaceChanged = useCallback((newWorkspaceId: string) => { - navigate(generateUrl(ROUTE_WORKSPACE_HOME, searchParms, newWorkspaceId)); - }, [navigate, workspaceId, searchParms]); - - const handleNavigationView = useCallback((route: string) => { - navigate(generateUrl(route, searchParms, workspaceId)); - }, [navigate]); - - const handleThreatListView = useCallback((filter?: ThreatStatementListFilter) => { - navigate(generateUrl(ROUTE_THREAT_LIST, searchParms, workspaceId), { - state: filter ? { - filter, - } : undefined, - }); - }, [navigate, workspaceId, searchParms]); - - const handleThreatEditorView = useCallback((newThreatId: string, idToCopy?: string) => { - navigate(generateUrl(ROUTE_THREAT_EDITOR, searchParms, workspaceId, newThreatId, undefined, idToCopy ? { - idToCopy, - } : undefined), { - state: { - idToCopy, - }, - }); - }, [navigate, workspaceId, searchParms]); - - const navigationItems: SideNavigationProps.Item[] = useMemo(() => { - const navItems: SideNavigationProps.Item[] = [ - { - text: 'Dashboard', - href: generateUrl(ROUTE_WORKSPACE_HOME, searchParms, workspaceId), - type: 'link', - }, - { - text: 'Application info', - href: generateUrl(ROUTE_APPLICATION_INFO, searchParms, workspaceId), - type: 'link', - }, - { - text: 'Architecture', - href: generateUrl(ROUTE_ARCHITECTURE_INFO, searchParms, workspaceId), - type: 'link', - }, - { - text: 'Dataflow', - href: generateUrl(ROUTE_DATAFLOW_INFO, searchParms, workspaceId), - type: 'link', - }, - { - text: 'Assumptions', - href: generateUrl(ROUTE_ASSUMPTION_LIST, searchParms, workspaceId), - type: 'link', - }, - { - text: 'Threats', - href: generateUrl(ROUTE_THREAT_LIST, searchParms, workspaceId), - type: 'link', - }, - { - text: 'Mitigations', - href: generateUrl(ROUTE_MITIGATION_LIST, searchParms, workspaceId), - type: 'link', - }, - { type: 'divider' }, - { - text: 'Threat model', - href: generateUrl(ROUTE_VIEW_THREAT_MODEL, searchParms, workspaceId), - type: 'link', - }, - ]; - return isThreatPackFeatureOn ? navItems.concat([ - { type: 'divider' }, - { - type: 'section', - text: 'Reference packs', - items: [ - { - text: 'Threat packs', - href: generateUrl(ROUTE_THREAT_PACKS, searchParms, workspaceId), - type: 'link', - }, - { - text: 'Mitigation packs', - href: generateUrl(ROUTE_MITIGATION_PACKS, searchParms, workspaceId), - type: 'link', - }, - ], - }, - ]) : navItems; - - }, [searchParms, workspaceId, isThreatPackFeatureOn]); - - const handlePreview = useCallback((data: DataExchangeFormat) => { - const urlParams = new URLSearchParams(window.location.search); - urlParams.set('preview', 'true'); - window.localStorage.setItem(TEMP_PREVIEW_DATA_KEY, JSON.stringify(data)); - urlParams.set('dataKey', TEMP_PREVIEW_DATA_KEY); - window.open(`${window.location.pathname}?${urlParams.toString()}`, '_blank', 'noopener,noreferrer,resizable'); - }, []); - - const handleImported = useCallback(() => { - navigate(generateUrl(ROUTE_VIEW_THREAT_MODEL, searchParms, workspaceId)); - }, [navigate, workspaceId, searchParms]); - - const handleDefineWorkload = useCallback(() => { - navigate(generateUrl(ROUTE_APPLICATION_INFO, searchParms, workspaceId)); - }, [navigate, workspaceId, searchParms]); - - const notifications = useNotifications(); - return ( - handleNavigationView(ROUTE_APPLICATION_INFO)} - onArchitectureView={() => handleNavigationView(ROUTE_ARCHITECTURE_INFO)} - onDataflowView={() => handleNavigationView(ROUTE_DATAFLOW_INFO)} - onAssumptionListView={() => handleNavigationView(ROUTE_ASSUMPTION_LIST)} - onMitigationListView={() => handleNavigationView(ROUTE_MITIGATION_LIST)} - onThreatListView={handleThreatListView} - onThreatEditorView={handleThreatEditorView} - onPreview={handlePreview} - onImported={handleImported} - onDefineWorkload={handleDefineWorkload} - > - {isPreview ? ( - - ) : ( x.path || '')} - breadcrumbGroup={} - notifications={notifications} - > - - )} - + ); }; diff --git a/packages/threat-composer-app/src/containers/App/index.tsx b/packages/threat-composer-app/src/containers/App/index.tsx index 8bdcf29f..4a69af03 100644 --- a/packages/threat-composer-app/src/containers/App/index.tsx +++ b/packages/threat-composer-app/src/containers/App/index.tsx @@ -14,30 +14,22 @@ limitations under the License. ******************************************************************************************************************** */ import { FC } from 'react'; -import { useSearchParams } from 'react-router-dom'; import Full from './components/Full'; import Standalone from './components/Standalone'; -import GithubPagesNavigationHelper from '../../components/GithubPagesNavigationHelper'; -import { SEARCH_PARAM_MODE } from '../../config/searchParams'; +import getComposerMode from '../../utils/getComposerMode'; -const DEFAULT_MODE = process.env.REACT_APP_DEFAULT_MODE; -const isGithubPages = process.env.REACT_APP_GITHUB_PAGES === 'true'; /** * Demo app for threat-composer */ const App: FC = () => { - const [searchParams] = useSearchParams(); - const mode = searchParams.get(SEARCH_PARAM_MODE); - const composerMode = mode || DEFAULT_MODE || 'Full'; + const composerMode = getComposerMode(); - return composerMode === 'ThreatsOnly' || composerMode === 'EditorOnly' ? ( + console.log('App-ComposerMode', composerMode); + + return (composerMode === 'ThreatsOnly' || composerMode === 'EditorOnly') ? ( - ) : ( - isGithubPages ? - () : - - ); + ) : (); }; export default App; diff --git a/packages/threat-composer-app/src/containers/AppLayout/index.tsx b/packages/threat-composer-app/src/containers/AppLayout/index.tsx new file mode 100644 index 00000000..2a91f6a8 --- /dev/null +++ b/packages/threat-composer-app/src/containers/AppLayout/index.tsx @@ -0,0 +1,132 @@ +/** ******************************************************************************************************************* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ******************************************************************************************************************** */ +import { + DEFAULT_WORKSPACE_ID, + useGlobalSetupContext, +} from '@aws/threat-composer'; +import { SideNavigationProps } from '@cloudscape-design/components/side-navigation'; +import { FC, PropsWithChildren, useMemo } from 'react'; +import { useParams, useSearchParams } from 'react-router-dom'; +import AppLayoutComponent from '../../components/FullAppLayout'; +import { + ROUTE_APPLICATION_INFO_PATH, + ROUTE_ARCHITECTURE_INFO_PATH, + ROUTE_ASSUMPTION_LIST_PATH, + ROUTE_DATAFLOW_INFO_PATH, + ROUTE_THREAT_PACKS_PATH, + ROUTE_MITIGATION_LIST_PATH, + ROUTE_THREAT_LIST_PATH, + ROUTE_VIEW_THREAT_MODEL_PATH, + ROUTE_WORKSPACE_HOME_PATH, + ROUTE_MITIGATION_PACKS_PATH, +} from '../../config/routes'; +import useNotifications from '../../hooks/useNotifications'; +import generateUrl from '../../utils/generateUrl'; +import WorkspaceSelector from '../WorkspaceSelector'; + +const defaultHref = process.env.PUBLIC_URL || '/'; + +const AppLayout: FC> = ({ + children, +}) => { + const { workspaceId = DEFAULT_WORKSPACE_ID } = useParams(); + const notifications = useNotifications(); + const [searchParams] = useSearchParams(); + + const { features } = useGlobalSetupContext(); + + const isThreatPackFeatureOn = useMemo(() => { + return features.includes('threatPacks'); + }, [features]); + + const navigationItems: SideNavigationProps.Item[] = useMemo(() => { + const navItems: SideNavigationProps.Item[] = [ + { + text: 'Dashboard', + href: generateUrl(ROUTE_WORKSPACE_HOME_PATH, searchParams, workspaceId), + type: 'link', + }, + { + text: 'Application info', + href: generateUrl(ROUTE_APPLICATION_INFO_PATH, searchParams, workspaceId), + type: 'link', + }, + { + text: 'Architecture', + href: generateUrl(ROUTE_ARCHITECTURE_INFO_PATH, searchParams, workspaceId), + type: 'link', + }, + { + text: 'Dataflow', + href: generateUrl(ROUTE_DATAFLOW_INFO_PATH, searchParams, workspaceId), + type: 'link', + }, + { + text: 'Assumptions', + href: generateUrl(ROUTE_ASSUMPTION_LIST_PATH, searchParams, workspaceId), + type: 'link', + }, + { + text: 'Threats', + href: generateUrl(ROUTE_THREAT_LIST_PATH, searchParams, workspaceId), + type: 'link', + }, + { + text: 'Mitigations', + href: generateUrl(ROUTE_MITIGATION_LIST_PATH, searchParams, workspaceId), + type: 'link', + }, + { type: 'divider' }, + { + text: 'Threat model', + href: generateUrl(ROUTE_VIEW_THREAT_MODEL_PATH, searchParams, workspaceId), + type: 'link', + }, + ]; + return isThreatPackFeatureOn ? navItems.concat([ + { type: 'divider' }, + { + type: 'section', + text: 'Reference packs', + items: [ + { + text: 'Threat packs', + href: generateUrl(ROUTE_THREAT_PACKS_PATH, searchParams, workspaceId), + type: 'link', + }, + { + text: 'Mitigation packs', + href: generateUrl(ROUTE_MITIGATION_PACKS_PATH, searchParams, workspaceId), + type: 'link', + }, + ], + }, + ]) : navItems; + + }, [searchParams, workspaceId, isThreatPackFeatureOn]); + + return (} + notifications={notifications} + > + {children} + ); +}; + +export default AppLayout; diff --git a/packages/threat-composer-app/src/containers/AppRoot/index.tsx b/packages/threat-composer-app/src/containers/AppRoot/index.tsx new file mode 100644 index 00000000..e1e7d408 --- /dev/null +++ b/packages/threat-composer-app/src/containers/AppRoot/index.tsx @@ -0,0 +1,33 @@ +/** ******************************************************************************************************************* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ******************************************************************************************************************** */ + +import { GlobalSetupContext } from '@aws/threat-composer'; +import { FC } from 'react'; +import { Outlet } from 'react-router-dom'; +import { appMode } from '../../config/appMode'; +import useFeatures from '../../hooks/useFeatures'; + +const AppRoot: FC = () => { + const features = useFeatures(); + return ( + + ); +}; + +export default AppRoot; \ No newline at end of file diff --git a/packages/threat-composer-app/src/containers/Application/index.tsx b/packages/threat-composer-app/src/containers/Application/index.tsx index 6363aadd..964001e6 100644 --- a/packages/threat-composer-app/src/containers/Application/index.tsx +++ b/packages/threat-composer-app/src/containers/Application/index.tsx @@ -14,9 +14,10 @@ limitations under the License. ******************************************************************************************************************** */ import { ApplicationInfoComponent } from '@aws/threat-composer'; +import MarkdownEditorWithPrompt from '../../components/MarkdownEditorWithPrompt'; const Application = () => { - return ; + return ; }; export default Application; \ No newline at end of file diff --git a/packages/threat-composer-app/src/containers/Architecture/index.tsx b/packages/threat-composer-app/src/containers/Architecture/index.tsx index e750e2ea..61d6071d 100644 --- a/packages/threat-composer-app/src/containers/Architecture/index.tsx +++ b/packages/threat-composer-app/src/containers/Architecture/index.tsx @@ -14,9 +14,10 @@ limitations under the License. ******************************************************************************************************************** */ import { ArchitectureInfoComponent } from '@aws/threat-composer'; +import MarkdownEditorWithPrompt from '../../components/MarkdownEditorWithPrompt'; const Architecture = () => { - return ; + return ; }; export default Architecture; \ No newline at end of file diff --git a/packages/threat-composer-app/src/containers/Dataflow/index.tsx b/packages/threat-composer-app/src/containers/Dataflow/index.tsx index 123b2b08..d7f3fdeb 100644 --- a/packages/threat-composer-app/src/containers/Dataflow/index.tsx +++ b/packages/threat-composer-app/src/containers/Dataflow/index.tsx @@ -14,9 +14,10 @@ limitations under the License. ******************************************************************************************************************** */ import { DataflowInfoComponent } from '@aws/threat-composer'; +import MarkdownEditorWithPrompt from '../../components/MarkdownEditorWithPrompt'; const Dataflow = () => { - return ; + return ; }; export default Dataflow; \ No newline at end of file diff --git a/packages/threat-composer-app/src/containers/ThreatModelPreview/index.tsx b/packages/threat-composer-app/src/containers/ThreatModelPreview/index.tsx new file mode 100644 index 00000000..f4fb8e63 --- /dev/null +++ b/packages/threat-composer-app/src/containers/ThreatModelPreview/index.tsx @@ -0,0 +1,47 @@ +/** ******************************************************************************************************************* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ******************************************************************************************************************** */ +import { ThreatModelView } from '@aws/threat-composer'; +import Box from '@cloudscape-design/components/box'; +import { FC, useEffect, useMemo } from 'react'; +import { useParams } from 'react-router-dom'; + +const ThreatModelPreview: FC = () => { + const { dataKey } = useParams(); + + const data = useMemo(() => { + const dataStr = dataKey && window.localStorage.getItem(dataKey); + + if (dataStr) { + return JSON.parse(dataStr); + } + + return undefined; + }, []); + + useEffect(() => { + return () => { + dataKey && window.localStorage.removeItem(dataKey); + }; + }, [dataKey]); + + if (!data) { + return No content; + } + + return ; +}; + +export default ThreatModelPreview; \ No newline at end of file diff --git a/packages/threat-composer-app/src/containers/ThreatModelReport/index.tsx b/packages/threat-composer-app/src/containers/ThreatModelReport/index.tsx index 0989bddb..0ce80d0b 100644 --- a/packages/threat-composer-app/src/containers/ThreatModelReport/index.tsx +++ b/packages/threat-composer-app/src/containers/ThreatModelReport/index.tsx @@ -13,46 +13,34 @@ See the License for the specific language governing permissions and limitations under the License. ******************************************************************************************************************** */ -import { ThreatModel, ThreatModelView } from '@aws/threat-composer'; -import { FC, useCallback, useState } from 'react'; +import { ThreatModel } from '@aws/threat-composer'; +import { FC } from 'react'; +import { + ROUTE_APPLICATION_INFO, + ROUTE_ARCHITECTURE_INFO, + ROUTE_ASSUMPTION_LIST, + ROUTE_DATAFLOW_INFO, + ROUTE_MITIGATION_LIST, + ROUTE_THREAT_LIST, +} from '../../config/routes'; +import useNavigateView from '../../hooks/useNavigationView'; +import useOnPreview from '../../hooks/useOnPreview'; import convertToDocx from '../../utils/convertToDocx'; const ThreatModelReport: FC = () => { - const [isPreview] = useState(() => { - const urlParams = new URLSearchParams(window.location.search); - const previewParams = urlParams.get('preview'); - return previewParams === 'true'; - }); - - const [data] = useState(() => { - const urlParams = new URLSearchParams(window.location.search); - const dataKey = urlParams.get('dataKey'); - const dataStr = dataKey && window.localStorage.getItem(dataKey); - - if (dataStr) { - return JSON.parse(dataStr); - } - - if (dataKey) { - return {}; - } - - return undefined; - }); - - const handlePrintButtonClick = useCallback(() => { - const urlParams = new URLSearchParams(window.location.search); - urlParams.set('preview', 'true'); - window.open(`${window.location.pathname}?${urlParams.toString()}`, '_blank', 'noopener,noreferrer,resizable'); - }, []); - - return (data - ? () - : ()); + const handleNavigationView = useNavigateView(); + const [onPreview] = useOnPreview(); + + return ( handleNavigationView(ROUTE_APPLICATION_INFO)} + onArchitectureView={() => handleNavigationView(ROUTE_ARCHITECTURE_INFO)} + onDataflowView={() => handleNavigationView(ROUTE_DATAFLOW_INFO)} + onAssumptionListView={() => handleNavigationView(ROUTE_ASSUMPTION_LIST)} + onMitigationListView={() => handleNavigationView(ROUTE_MITIGATION_LIST)} + onThreatListView={() => handleNavigationView(ROUTE_THREAT_LIST)} + />); }; export default ThreatModelReport; \ No newline at end of file diff --git a/packages/threat-composer-app/src/containers/ThreatStatementEditor/index.tsx b/packages/threat-composer-app/src/containers/ThreatStatementEditor/index.tsx index 75ce5148..75a28156 100644 --- a/packages/threat-composer-app/src/containers/ThreatStatementEditor/index.tsx +++ b/packages/threat-composer-app/src/containers/ThreatStatementEditor/index.tsx @@ -25,11 +25,14 @@ import { } from '@aws/threat-composer'; import { useEffect, useState } from 'react'; import { useLocation, useParams } from 'react-router-dom'; +import { ROUTE_THREAT_LIST } from '../../config/routes'; +import useNavigateView from '../../hooks/useNavigationView'; import isMemoryRouterUsed from '../../utils/isMemoryRouterUsed'; const ThreatStatementEditor = () => { const { threatId } = useParams(); const location = useLocation(); + const handleNavigateView = useNavigateView(); const [idToCopy] = useState(() => { if (isMemoryRouterUsed()) { @@ -98,7 +101,7 @@ const ThreatStatementEditor = () => { setEditingStatement(editingStatement); }, [editingStatement]); - return ; + return handleNavigateView(ROUTE_THREAT_LIST)} />; }; export default ThreatStatementEditor; \ No newline at end of file diff --git a/packages/threat-composer-app/src/containers/ThreatStatementList/index.tsx b/packages/threat-composer-app/src/containers/ThreatStatementList/index.tsx index 355c1a24..5266d91d 100644 --- a/packages/threat-composer-app/src/containers/ThreatStatementList/index.tsx +++ b/packages/threat-composer-app/src/containers/ThreatStatementList/index.tsx @@ -15,10 +15,22 @@ ******************************************************************************************************************** */ import { ThreatStatementList as ThreatStatementListComponent } from '@aws/threat-composer'; import { useLocation } from 'react-router-dom'; +import { ROUTE_THREAT_EDITOR } from '../../config/routes'; +import useNavigateView from '../../hooks/useNavigationView'; const ThreatStatementList = () => { + const handleNavigateView = useNavigateView(); const { state } = useLocation(); - return ; + return handleNavigateView(ROUTE_THREAT_EDITOR, threatId, undefined, idToCopy ? { + idToCopy, + } : undefined, { + state: { + idToCopy, + }, + })} + />; }; export default ThreatStatementList; \ No newline at end of file diff --git a/packages/threat-composer-app/src/containers/WorkspaceHome/index.tsx b/packages/threat-composer-app/src/containers/WorkspaceHome/index.tsx index 0dc68879..d014f52d 100644 --- a/packages/threat-composer-app/src/containers/WorkspaceHome/index.tsx +++ b/packages/threat-composer-app/src/containers/WorkspaceHome/index.tsx @@ -13,10 +13,27 @@ See the License for the specific language governing permissions and limitations under the License. ******************************************************************************************************************** */ -import { WorkspaceHome as WorkspaceHomeComponent } from '@aws/threat-composer'; +import { WorkspaceHome as WorkspaceHomeComponent, ThreatStatementListFilter } from '@aws/threat-composer'; +import { ROUTE_APPLICATION_INFO, ROUTE_THREAT_EDITOR, ROUTE_THREAT_LIST } from '../../config/routes'; +import useNavigateView from '../../hooks/useNavigationView'; const WorkspaceHome = () => { - return ; + const handleNavigateView = useNavigateView(); + return handleNavigateView(ROUTE_APPLICATION_INFO)} + onThreatEditorView={(threatId: string, idToCopy?: string) => handleNavigateView(ROUTE_THREAT_EDITOR, threatId, undefined, idToCopy ? { + idToCopy, + } : undefined, { + state: { + idToCopy, + }, + })} + onThreatListView={(filter?: ThreatStatementListFilter) => handleNavigateView(ROUTE_THREAT_LIST, undefined, undefined, undefined, { + state: filter ? { + filter, + } : undefined, + })} + />; }; export default WorkspaceHome; \ No newline at end of file diff --git a/packages/threat-composer-app/src/containers/WorkspaceRoot/index.tsx b/packages/threat-composer-app/src/containers/WorkspaceRoot/index.tsx new file mode 100644 index 00000000..8c34f26f --- /dev/null +++ b/packages/threat-composer-app/src/containers/WorkspaceRoot/index.tsx @@ -0,0 +1,81 @@ +/** ******************************************************************************************************************* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ******************************************************************************************************************** */ +import { + DEFAULT_WORKSPACE_ID, + WorkspaceContextAggregator, + WorkspacesContext, + ThreatStatementListFilter, + WorkspaceExamplesContext, +} from '@aws/threat-composer'; +import { useCallback, FC } from 'react'; +import { useNavigate, useParams, useSearchParams, Outlet } from 'react-router-dom'; +import { + ROUTE_THREAT_EDITOR_PATH, + ROUTE_THREAT_LIST_PATH, + ROUTE_WORKSPACE_HOME, +} from '../../config/routes'; +import generateUrl from '../../utils/generateUrl'; +import AppLayout from '../AppLayout'; + +const WorkspaceRoot: FC = () => { + const { workspaceId = DEFAULT_WORKSPACE_ID } = useParams(); + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + + const handleThreatListView = useCallback((filter?: ThreatStatementListFilter) => { + navigate(generateUrl(ROUTE_THREAT_LIST_PATH, searchParams, workspaceId), { + state: filter ? { + filter, + } : undefined, + }); + }, [navigate, workspaceId, searchParams]); + + const handleThreatEditorView = useCallback((newThreatId: string, idToCopy?: string) => { + navigate(generateUrl(ROUTE_THREAT_EDITOR_PATH, searchParams, workspaceId, newThreatId, undefined, idToCopy ? { + idToCopy, + } : undefined), { + state: { + idToCopy, + }, + }); + }, [navigate, workspaceId, searchParams]); + + const handleWorkspaceChanged = useCallback((newWorkspaceId: string) => { + const url = generateUrl(ROUTE_WORKSPACE_HOME, searchParams, newWorkspaceId); + navigate(url); + }, [navigate, workspaceId, searchParams]); + + return ( + + + {(workspaceIdFromContext) => ( + + + + )} + + ); +}; + +export default WorkspaceRoot; \ No newline at end of file diff --git a/packages/threat-composer-app/src/containers/WorkspaceSelector/index.tsx b/packages/threat-composer-app/src/containers/WorkspaceSelector/index.tsx new file mode 100644 index 00000000..fc262a9e --- /dev/null +++ b/packages/threat-composer-app/src/containers/WorkspaceSelector/index.tsx @@ -0,0 +1,44 @@ +/** ******************************************************************************************************************* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ******************************************************************************************************************** */ + +import { + WorkspaceSelector as WorkspaceSelectorComponent, + APP_MODE_BROWSER_EXTENSION, + APP_MODE_IDE_EXTENSION, +} from '@aws/threat-composer'; +import { appMode } from '../../config/appMode'; +import { ROUTE_VIEW_THREAT_MODEL } from '../../config/routes'; +import useNavigateView from '../../hooks/useNavigationView'; +import useOnPreview from '../../hooks/useOnPreview'; + + +const WorkspaceSelector = () => { + const [onPreview] = useOnPreview(); + const navigate = useNavigateView(); + + return navigate(ROUTE_VIEW_THREAT_MODEL)} + singletonMode={appMode === APP_MODE_BROWSER_EXTENSION || appMode === APP_MODE_IDE_EXTENSION} + singletonPrimaryActionButtonConfig={appMode === APP_MODE_IDE_EXTENSION ? { + text: 'Save', + eventName: 'save', + } : undefined} + embededMode={false} + />; +}; + +export default WorkspaceSelector; \ No newline at end of file diff --git a/packages/threat-composer-app/src/hooks/useFeatures/index.tsx b/packages/threat-composer-app/src/hooks/useFeatures/index.tsx new file mode 100644 index 00000000..7610718e --- /dev/null +++ b/packages/threat-composer-app/src/hooks/useFeatures/index.tsx @@ -0,0 +1,27 @@ +/** ******************************************************************************************************************* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ******************************************************************************************************************** */ + +import { useSearchParams } from 'react-router-dom'; +import { SEARCH_PARAM_FEATURES } from '../../config/searchParams'; + +const useFeatures = () => { + const [searchParams] = useSearchParams(); + + const featureParam = searchParams.get(SEARCH_PARAM_FEATURES); + return featureParam && featureParam.split(',') || []; +}; + +export default useFeatures; \ No newline at end of file diff --git a/packages/threat-composer-app/src/hooks/useNavigationView/index.tsx b/packages/threat-composer-app/src/hooks/useNavigationView/index.tsx new file mode 100644 index 00000000..51589dd4 --- /dev/null +++ b/packages/threat-composer-app/src/hooks/useNavigationView/index.tsx @@ -0,0 +1,39 @@ +/** ******************************************************************************************************************* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ******************************************************************************************************************** */ + +import { DEFAULT_WORKSPACE_ID } from '@aws/threat-composer'; +import { useCallback } from 'react'; +import { NavigateOptions, useNavigate, useParams, useSearchParams } from 'react-router-dom'; +import generateUrl from '../../utils/generateUrl'; + +const useNavigateView = () => { + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const { workspaceId = DEFAULT_WORKSPACE_ID } = useParams(); + + return useCallback((route: string, threatId?: string, + additionalParams?: { + [key: string]: string; + }, additionalSearchParams?: { + [key: string]: string; + }, opts?: NavigateOptions) => + navigate(generateUrl(route, searchParams, workspaceId, threatId, additionalParams, additionalSearchParams), opts), + [ + searchParams, workspaceId, + ]); +}; + +export default useNavigateView; \ No newline at end of file diff --git a/packages/threat-composer-app/src/hooks/useOnPreview/index.ts b/packages/threat-composer-app/src/hooks/useOnPreview/index.ts new file mode 100644 index 00000000..c57a444e --- /dev/null +++ b/packages/threat-composer-app/src/hooks/useOnPreview/index.ts @@ -0,0 +1,37 @@ +/** ******************************************************************************************************************* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ******************************************************************************************************************** */ +import { DataExchangeFormat } from '@aws/threat-composer'; +import { useCallback } from 'react'; +import { generatePath } from 'react-router-dom'; +import { ROUTE_PREVIEW } from '../../config/routes'; + +const TEMP_PREVIEW_DATA_KEY = 'ThreatStatementGenerator.TempPreviewData'; +const ROUTE_BASE_PATH = process.env.REACT_APP_ROUTE_BASE_PATH; + +const useOnPreview = () => { + const handlePreview = useCallback((data: DataExchangeFormat) => { + window.localStorage.setItem(TEMP_PREVIEW_DATA_KEY, JSON.stringify(data)); + const url = `${ROUTE_BASE_PATH || ''}${generatePath(ROUTE_PREVIEW, { + dataKey: TEMP_PREVIEW_DATA_KEY, + })}`; + + window.open(url, '_blank', 'noopener,noreferrer,resizable'); + }, []); + + return [handlePreview] as [(data: DataExchangeFormat) => void]; +}; + +export default useOnPreview; \ No newline at end of file diff --git a/packages/threat-composer-app/src/index.tsx b/packages/threat-composer-app/src/index.tsx index d56b2606..ef498be9 100644 --- a/packages/threat-composer-app/src/index.tsx +++ b/packages/threat-composer-app/src/index.tsx @@ -16,14 +16,11 @@ import { ThemeProvider, Mode } from '@aws/threat-composer'; import React from 'react'; import ReactDOM from 'react-dom'; -import { BrowserRouter, MemoryRouter } from 'react-router-dom'; import App from './containers/App'; import reportWebVitals from './reportWebVitals'; import * as serviceWorkerRegistration from './serviceWorkerRegistration'; import isMemoryRouterUsed from './utils/isMemoryRouterUsed'; -const Router = isMemoryRouterUsed() ? MemoryRouter : BrowserRouter; - const initialThemeString = (document.querySelector('meta[name="dark-mode"]') as HTMLMetaElement)?.content; const initialTheme = initialThemeString ? @@ -33,9 +30,7 @@ const initialTheme = initialThemeString ? ReactDOM.render( - - - + , document.getElementById('root'), diff --git a/packages/threat-composer-app/src/routes/index.tsx b/packages/threat-composer-app/src/routes/index.tsx index 685562d8..cff6f4d6 100644 --- a/packages/threat-composer-app/src/routes/index.tsx +++ b/packages/threat-composer-app/src/routes/index.tsx @@ -13,95 +13,130 @@ See the License for the specific language governing permissions and limitations under the License. ******************************************************************************************************************** */ -import { RouteProps } from 'react-router-dom'; +import { createBrowserRouter, createMemoryRouter, redirect } from 'react-router-dom'; +import initialWorkspaceLoader from './initialWorkspaceLoader'; import { - ROUTE_WORKSPACE_HOME, - ROUTE_APPLICATION_INFO, - ROUTE_ARCHITECTURE_INFO, - ROUTE_ASSUMPTION_LIST, - ROUTE_DATAFLOW_INFO, - ROUTE_MITIGATION_LIST, - ROUTE_THREAT_EDITOR, - ROUTE_THREAT_LIST, - ROUTE_VIEW_THREAT_MODEL, - ROUTE_THREAT_PACK, - ROUTE_THREAT_PACKS, - ROUTE_MITIGATION_PACK, - ROUTE_MITIGATION_PACKS, + ROUTE_WORKSPACE_HOME_PATH, + ROUTE_APPLICATION_INFO_PATH, + ROUTE_ARCHITECTURE_INFO_PATH, + ROUTE_ASSUMPTION_LIST_PATH, + ROUTE_DATAFLOW_INFO_PATH, + ROUTE_MITIGATION_LIST_PATH, + ROUTE_THREAT_EDITOR_PATH, + ROUTE_THREAT_LIST_PATH, + ROUTE_VIEW_THREAT_MODEL_PATH, + ROUTE_THREAT_PACK_PATH, + ROUTE_THREAT_PACKS_PATH, + ROUTE_MITIGATION_PACK_PATH, + ROUTE_MITIGATION_PACKS_PATH, + ROUTE_WORKSPACE_PATH, + ROUTE_PREVIEW_PATH, } from '../config/routes'; import Application from '../containers/Application'; +import AppRoot from '../containers/AppRoot'; import Architecture from '../containers/Architecture'; import AssumptionList from '../containers/AssumptionList'; import Dataflow from '../containers/Dataflow'; import MitigationList from '../containers/MitigationList'; import MitigationPack from '../containers/MitigationPack'; import MitigationPacks from '../containers/MitigationPacks'; +import ThreatModelPreview from '../containers/ThreatModelPreview'; import ThreatModelReport from '../containers/ThreatModelReport'; import ThreatPack from '../containers/ThreatPack'; import ThreatPacks from '../containers/ThreatPacks'; import ThreatStatementEditor from '../containers/ThreatStatementEditor'; import ThreatStatementList from '../containers/ThreatStatementList'; import WorkspaceHome from '../containers/WorkspaceHome'; +import WorkspaceRoot from '../containers/WorkspaceRoot'; +import isMemoryRouterUsed from '../utils/isMemoryRouterUsed'; -const ROUTE_BASE_PATH = process.env.REACT_APP_ROUTE_BASE_PATH || ''; +const ROUTE_BASE_PATH = process.env.REACT_APP_ROUTE_BASE_PATH; -const getRouteWithBasePath = (route: string) => { - return `${ROUTE_BASE_PATH}${route}`; -}; - -const routes: RouteProps[] = [ +const workspaceRoutes = [ + { + index: true, + loader: async () => redirect(ROUTE_WORKSPACE_HOME_PATH), + }, { - path: getRouteWithBasePath(ROUTE_WORKSPACE_HOME), + path: ROUTE_WORKSPACE_HOME_PATH, element: , }, { - path: getRouteWithBasePath(ROUTE_APPLICATION_INFO), + path: ROUTE_APPLICATION_INFO_PATH, element: , }, { - path: getRouteWithBasePath(ROUTE_ARCHITECTURE_INFO), + path: ROUTE_ARCHITECTURE_INFO_PATH, element: , }, { - path: getRouteWithBasePath(ROUTE_ASSUMPTION_LIST), + path: ROUTE_ASSUMPTION_LIST_PATH, element: , }, { - path: getRouteWithBasePath(ROUTE_DATAFLOW_INFO), + path: ROUTE_DATAFLOW_INFO_PATH, element: , }, { - path: getRouteWithBasePath(ROUTE_MITIGATION_LIST), + path: ROUTE_MITIGATION_LIST_PATH, element: , }, { - path: getRouteWithBasePath(ROUTE_VIEW_THREAT_MODEL), + path: ROUTE_VIEW_THREAT_MODEL_PATH, element: , }, { - path: getRouteWithBasePath(ROUTE_THREAT_PACK), + path: ROUTE_THREAT_PACK_PATH, element: , }, { - path: getRouteWithBasePath(ROUTE_THREAT_PACKS), + path: ROUTE_THREAT_PACKS_PATH, element: , }, { - path: getRouteWithBasePath(ROUTE_MITIGATION_PACK), + path: ROUTE_MITIGATION_PACK_PATH, element: , }, { - path: getRouteWithBasePath(ROUTE_MITIGATION_PACKS), + path: ROUTE_MITIGATION_PACKS_PATH, element: , }, { - path: getRouteWithBasePath(ROUTE_THREAT_EDITOR), + path: ROUTE_THREAT_EDITOR_PATH, element: , }, { - path: getRouteWithBasePath(ROUTE_THREAT_LIST), + path: ROUTE_THREAT_LIST_PATH, element: , }, ]; -export default routes; \ No newline at end of file +export const createRouter = isMemoryRouterUsed() ? createMemoryRouter : createBrowserRouter; + +export const routerOpts = ROUTE_BASE_PATH ? { + basename: ROUTE_BASE_PATH, +} : {}; + +export const routes = [ + { + index: true, + loader: initialWorkspaceLoader, + }, + { + path: '/', + element: , + children: [ + { + path: ROUTE_WORKSPACE_PATH, + element: , + children: [ + ...workspaceRoutes, + ], + }, + { + path: ROUTE_PREVIEW_PATH, + element: , + }, + ], + }, +]; diff --git a/packages/threat-composer-app/src/routes/initialWorkspaceLoader.tsx b/packages/threat-composer-app/src/routes/initialWorkspaceLoader.tsx new file mode 100644 index 00000000..e6678187 --- /dev/null +++ b/packages/threat-composer-app/src/routes/initialWorkspaceLoader.tsx @@ -0,0 +1,64 @@ +/** ******************************************************************************************************************* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ******************************************************************************************************************** */ +import { LOCAL_STORAGE_KEY_CURRENT_WORKSPACE, Workspace } from '@aws/threat-composer'; +import { generatePath, redirect } from 'react-router-dom'; +import { ROUTE_WORKSPACE_DEFAULT, ROUTE_WORKSPACE_PATH } from '../config/routes'; + +const isGithubPages = process.env.REACT_APP_GITHUB_PAGES === 'true'; + +const requiredRewriteUrl = (search: string) => { + return search && (search.startsWith('?/') || search.startsWith('?%2F')); +}; + +const initialWorkspaceLoader = async () => { + const l = window.location; + + // For github page, the direct navigation to workspace pages (e.g., https://awslabs.github.io/threat-composer/workspaces/default/dashboard) results in 404. + // In 404.html, the url is rewrited to https://awslabs.github.io/threat-composer?/workspaces/default/dashboard so that the index.html can be loaded. + // And here is to reconstruct the original url and navigate to the right page. + if (isGithubPages && requiredRewriteUrl(l.search)) { + let search = decodeURIComponent(l.search); + if (search.indexOf('=') === search.length - 1) { + search = search.slice(0, search.length - 1); + } + var decoded = search.slice(1).split('&').map(function (s) { + return s.replace(/~and~/g, '&'); + }).join('?'); + + return redirect(decoded + l.hash); + } + + const currentWorkspaceValue = window.localStorage.getItem(LOCAL_STORAGE_KEY_CURRENT_WORKSPACE); + + if (currentWorkspaceValue) { + try { + const currentWorkspace = JSON.parse(currentWorkspaceValue) as Workspace | null | undefined; + if (currentWorkspace) { + const redirectUrl = generatePath(ROUTE_WORKSPACE_PATH, { + workspaceId: currentWorkspace.name, + }); + return redirect(redirectUrl); + } + + } catch (e) { + console.log('Error in retrieving current workspace', currentWorkspaceValue); + } + } + + return redirect(ROUTE_WORKSPACE_DEFAULT); +}; + +export default initialWorkspaceLoader; \ No newline at end of file diff --git a/packages/threat-composer-app/src/utils/generateUrl/index.ts b/packages/threat-composer-app/src/utils/generateUrl/index.ts index d6c846b2..584215dc 100644 --- a/packages/threat-composer-app/src/utils/generateUrl/index.ts +++ b/packages/threat-composer-app/src/utils/generateUrl/index.ts @@ -15,9 +15,6 @@ ******************************************************************************************************************** */ import { generatePath } from 'react-router-dom'; import { SEARCH_PARAM_FEATURES, SEARCH_PARAM_MODE } from '../../config/searchParams'; - -const ROUTE_BASE_PATH = process.env.REACT_APP_ROUTE_BASE_PATH || ''; - const SEARCH_PARAMS_KEPT = [SEARCH_PARAM_MODE, SEARCH_PARAM_FEATURES]; const generateUrl = (path: string, searchParams: URLSearchParams, workspaceId: string, threatId?: string, @@ -26,7 +23,7 @@ const generateUrl = (path: string, searchParams: URLSearchParams, workspaceId: s }, additionalSearchParams?: { [key: string]: string; }) => { - const baseUrl = `${ROUTE_BASE_PATH}${generatePath(path, { + const baseUrl = `${generatePath(path, { workspaceId, threatId, ...additionalParams, diff --git a/packages/threat-composer-app/src/utils/getComposerMode/index.tsx b/packages/threat-composer-app/src/utils/getComposerMode/index.tsx new file mode 100644 index 00000000..902cadbc --- /dev/null +++ b/packages/threat-composer-app/src/utils/getComposerMode/index.tsx @@ -0,0 +1,28 @@ +/** ******************************************************************************************************************* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ******************************************************************************************************************** */ + +import { SEARCH_PARAM_MODE } from '../../config/searchParams'; + +const DEFAULT_MODE = process.env.REACT_APP_DEFAULT_MODE; + +const getComposerMode = () => { + const searchParams = new URLSearchParams(window.location.search); + const mode = searchParams.get(SEARCH_PARAM_MODE); + const composerMode = mode || DEFAULT_MODE || 'Full'; + return composerMode; +}; + +export default getComposerMode; \ No newline at end of file diff --git a/packages/threat-composer/src/components/application/ApplicationInfo/index.tsx b/packages/threat-composer/src/components/application/ApplicationInfo/index.tsx index 4d83efc5..9e56a5d6 100644 --- a/packages/threat-composer/src/components/application/ApplicationInfo/index.tsx +++ b/packages/threat-composer/src/components/application/ApplicationInfo/index.tsx @@ -27,6 +27,7 @@ import MarkdownViewer from '../../generic/MarkdownViewer'; const ApplicationInfo: FC = ({ onEditModeChange, + MarkdownEditorComponentType = MarkdownEditor, }) => { const { applicationInfo, setApplicationInfo } = useApplicationInfoContext(); const [editMode, setEditMode] = useState(!applicationInfo.name && !applicationInfo.description ); @@ -74,7 +75,7 @@ const ApplicationInfo: FC = ({ placeholder='Enter application name' /> - = ({ onConfirm, validateData, onEditModeChange, + MarkdownEditorComponentType = MarkdownEditor, }) => { const [editMode, setEditMode] = useState(!entity.description && !entity.image); const [image, setImage] = useState(''); @@ -72,7 +73,7 @@ const BaseDiagramInfo: FC = ({ return ({headerTitle}}> {editMode ? ( - void; +export interface ThreatModelProps extends ViewNavigationEvent { + onPrintButtonClick?: (data: DataExchangeFormat) => void; isPreview?: boolean; convertToDocx?: ThreatModelViewProps['convertToDocx']; } @@ -40,28 +41,20 @@ const ThreatModel: FC = ({ return getExportFileName(composerMode, false, currentWorkspace); }, [composerMode, currentWorkspace]); - const { - onApplicationInfoView, - onArchitectureView, - onDataflowView, - onAssumptionListView, - onThreatListView, - onMitigationListView, - } = useWorkspacesContext(); return onPrintButtonClick?.(getWorkspaceData())} showPrintDownloadButtons={appMode !== APP_MODE_IDE_EXTENSION} composerMode={composerMode} data={getWorkspaceData()} downloadFileName={downloadFileName} hasContentDetails={hasContentDetails} - onApplicationInfoView={onApplicationInfoView} - onArchitectureView={onArchitectureView} - onDataflowView={onDataflowView} - onAssumptionListView={onAssumptionListView} - onThreatListView={onThreatListView} - onMitigationListView={onMitigationListView} + onApplicationInfoView={props.onApplicationInfoView} + onArchitectureView={props.onArchitectureView} + onDataflowView={props.onDataflowView} + onAssumptionListView={props.onAssumptionListView} + onThreatListView={props.onThreatListView} + onMitigationListView={props.onMitigationListView} />; }; diff --git a/packages/threat-composer/src/components/threats/ThreatStatementEditor/index.tsx b/packages/threat-composer/src/components/threats/ThreatStatementEditor/index.tsx index 0c60b46b..f6b26df6 100644 --- a/packages/threat-composer/src/components/threats/ThreatStatementEditor/index.tsx +++ b/packages/threat-composer/src/components/threats/ThreatStatementEditor/index.tsx @@ -30,7 +30,7 @@ import { useMitigationLinksContext } from '../../../contexts/MitigationLinksCont import { useMitigationsContext } from '../../../contexts/MitigationsContext/context'; import { useThreatsContext } from '../../../contexts/ThreatsContext/context'; import { useWorkspacesContext } from '../../../contexts/WorkspacesContext/context'; -import { TemplateThreatStatement } from '../../../customTypes'; +import { TemplateThreatStatement, ViewNavigationEvent } from '../../../customTypes'; import { ThreatFieldTypes } from '../../../customTypes/threatFieldTypes'; import threatFieldData from '../../../data/threatFieldData'; import threatStatementExamples from '../../../data/threatStatementExamples.json'; @@ -72,6 +72,10 @@ const styles = { const defaultThreatStatementFormat = threatStatementFormat[63]; +export interface ThreatStatementEditorProps { + onThreatListView?: ViewNavigationEvent['onThreatListView']; +} + const editorMapping: { [key in ThreatFieldTypes]: React.ComponentType }> } = { threat_source: EditorThreatSource, prerequisites: EditorPrerequisites, @@ -81,10 +85,11 @@ const editorMapping: { [key in ThreatFieldTypes]: React.ComponentType = ({ +const ThreatStatementEditorInner: FC = ({ editingStatement, + onThreatListView, }) => { - const { setEditingStatement, saveStatement, addStatement, onThreatListView } = useThreatsContext(); + const { setEditingStatement, saveStatement, addStatement } = useThreatsContext(); const inputRef = useRef<{ focus(): void }>(); const fullExamplesRef = useRef<{ collapse(): void }>(); const { currentWorkspace, workspaceList } = useWorkspacesContext(); @@ -378,10 +383,10 @@ const ThreatStatementEditorInner: FC<{ editingStatement: TemplateThreatStatement ); }; -const ThreatStatementEditor: FC = () => { +const ThreatStatementEditor: FC = (props) => { const { editingStatement } = useThreatsContext(); - return editingStatement ? : null; + return editingStatement ? : null; }; export default ThreatStatementEditor; \ No newline at end of file diff --git a/packages/threat-composer/src/components/threats/ThreatStatementList/index.tsx b/packages/threat-composer/src/components/threats/ThreatStatementList/index.tsx index 55247152..58ae2e5e 100644 --- a/packages/threat-composer/src/components/threats/ThreatStatementList/index.tsx +++ b/packages/threat-composer/src/components/threats/ThreatStatementList/index.tsx @@ -27,7 +27,7 @@ import { LEVEL_SELECTOR_OPTIONS, DEFAULT_NEW_ENTITY_ID, LEVEL_NOT_SET } from '.. import { useAssumptionLinksContext, useMitigationLinksContext } from '../../../contexts'; import { useGlobalSetupContext } from '../../../contexts/GlobalSetupContext/context'; import { useThreatsContext } from '../../../contexts/ThreatsContext/context'; -import { TemplateThreatStatement, ThreatStatementListFilter } from '../../../customTypes'; +import { TemplateThreatStatement, ThreatStatementListFilter, ViewNavigationEvent } from '../../../customTypes'; import useEditMetadata from '../../../hooks/useEditMetadata'; import { addTagToEntity, removeTagFromEntity } from '../../../utils/entityTag'; import AssetSelector from '../../generic/AssetSelector'; @@ -64,10 +64,12 @@ const styles = { export interface ThreatStatementListProps { initialFilter?: ThreatStatementListFilter; + onThreatEditorView?: ViewNavigationEvent['onThreatEditorView']; } const ThreatStatementList: FC = ({ initialFilter, + onThreatEditorView, }) => { const { statementList, @@ -75,7 +77,6 @@ const ThreatStatementList: FC = ({ addStatement, editStatement, saveStatement, - onThreatEditorView, } = useThreatsContext(); const { diff --git a/packages/threat-composer/src/components/workspaces/EditWorkspace/index.tsx b/packages/threat-composer/src/components/workspaces/EditWorkspace/index.tsx index 3a653e78..3d214df6 100644 --- a/packages/threat-composer/src/components/workspaces/EditWorkspace/index.tsx +++ b/packages/threat-composer/src/components/workspaces/EditWorkspace/index.tsx @@ -32,6 +32,7 @@ export interface EditWorkspaceProps { editMode?: boolean; currentWorkspace?: Workspace; workspaceList: Workspace[]; + exampleWorkspaceList: Workspace[]; } const EditWorkspace: FC = ({ @@ -40,6 +41,7 @@ const EditWorkspace: FC = ({ onConfirm, editMode = false, workspaceList, + exampleWorkspaceList, currentWorkspace, ...props }) => { @@ -55,11 +57,13 @@ const EditWorkspace: FC = ({ setErrorText(''); if (workspaceList.find(x => x.name === value && (!currentWorkspace || currentWorkspace.id !== x.id))) { setErrorText('A workspace already exists with that name'); + } else if (exampleWorkspaceList.find(x => x.name === value)) { + setErrorText('A workspace example already exists with that name'); } else { await onConfirm(value); setVisible(false); } - }, [onConfirm, value, workspaceList, currentWorkspace]); + }, [onConfirm, value, workspaceList, exampleWorkspaceList, currentWorkspace]); useEffect(() => { inputRef.current?.focus(); diff --git a/packages/threat-composer/src/components/workspaces/LandingPage/index.tsx b/packages/threat-composer/src/components/workspaces/LandingPage/index.tsx index 224c7bfb..0cd3e818 100644 --- a/packages/threat-composer/src/components/workspaces/LandingPage/index.tsx +++ b/packages/threat-composer/src/components/workspaces/LandingPage/index.tsx @@ -27,8 +27,14 @@ import HowItWorks from '../../../assets/how-it-works.png'; import SwitchToExample from '../../../assets/switch-to-example-workspace.gif'; import { useGlobalSetupContext } from '../../../contexts'; -const LandingPage: FC = () => { - const { setFileImportModalVisible, onDefineWorkload } = useGlobalSetupContext(); +export interface LandingPageProps { + onDefineWorkload?: () => void; +} + +const LandingPage: FC = ({ + onDefineWorkload, +}) => { + const { setFileImportModalVisible } = useGlobalSetupContext(); return ( { +const WorkspaceHome: FC = (props) => { const [hasContent] = useHasContent(); - return hasContent ? : ; + return hasContent ? : ; }; export default WorkspaceHome; \ No newline at end of file diff --git a/packages/threat-composer/src/components/workspaces/WorkspaceInsights/components/Overview/index.tsx b/packages/threat-composer/src/components/workspaces/WorkspaceInsights/components/Overview/index.tsx index a1ea65f7..962b1806 100644 --- a/packages/threat-composer/src/components/workspaces/WorkspaceInsights/components/Overview/index.tsx +++ b/packages/threat-composer/src/components/workspaces/WorkspaceInsights/components/Overview/index.tsx @@ -27,6 +27,7 @@ import { useThreatsContext } from '../../../../../contexts/ThreatsContext'; import filterThreatsByMetadata from '../../../../../utils/filterThreatsByMetadata'; import DashboardNumber from '../../../../generic/DashboardNumber'; import useLinkClicked from '../../hooks/useLinkClicked'; +import { WorkspaceInsightsProps } from '../../types'; const styles = { container: css({ @@ -59,7 +60,9 @@ const LabelValuePair: FC<{ ); }; -const Overview: FC = () => { +const Overview: FC = ({ + onThreatListView, +}) => { const { statementList } = useThreatsContext(); const { mitigationLinkList } = useMitigationLinksContext(); const { assumptionLinkList } = useAssumptionLinksContext(); @@ -72,7 +75,7 @@ const Overview: FC = () => { ).length; }, [statementList, mitigationLinkList, assumptionLinkList]); - const handleLinkClicked = useLinkClicked(); + const handleLinkClicked = useLinkClicked(onThreatListView); const missingMitigation = useMemo(() => { return statementList.filter( diff --git a/packages/threat-composer/src/components/workspaces/WorkspaceInsights/components/STRIDEAllocation/index.tsx b/packages/threat-composer/src/components/workspaces/WorkspaceInsights/components/STRIDEAllocation/index.tsx index 426d87b2..ea7f32e0 100644 --- a/packages/threat-composer/src/components/workspaces/WorkspaceInsights/components/STRIDEAllocation/index.tsx +++ b/packages/threat-composer/src/components/workspaces/WorkspaceInsights/components/STRIDEAllocation/index.tsx @@ -22,7 +22,7 @@ import { BarChart, BarChartProps, } from '@cloudscape-design/components'; -import { useState, useMemo, useCallback } from 'react'; +import { useState, useMemo, useCallback, FC } from 'react'; import { ALL_LEVELS, LEVEL_NOT_SET, @@ -30,14 +30,16 @@ import { DEFAULT_NEW_ENTITY_ID, } from '../../../../../configs'; import { useThreatsContext } from '../../../../../contexts/ThreatsContext'; -import { useWorkspacesContext } from '../../../../../contexts/WorkspacesContext'; import filterThreatsByMetadata from '../../../../../utils/filterThreatsByMetadata'; import DashboardNumber from '../../../../generic/DashboardNumber'; import useLinkClicked from '../../hooks/useLinkClicked'; +import { WorkspaceInsightsProps } from '../../types'; -const STRIDEAllocation = () => { +const STRIDEAllocation: FC = ({ + onThreatEditorView, + onThreatListView, +}) => { const { statementList, addStatement } = useThreatsContext(); - const { onThreatEditorView } = useWorkspacesContext(); const [selectedPriority, setSelectedPriority] = useState( ALL_LEVELS, @@ -81,7 +83,7 @@ const STRIDEAllocation = () => { [filteredStatementList], ); - const handleLinkClicked = useLinkClicked(); + const handleLinkClicked = useLinkClicked(onThreatListView); const barSeries: BarChartProps['series'] = [ { diff --git a/packages/threat-composer/src/components/workspaces/WorkspaceInsights/components/ThreatGrammar/index.tsx b/packages/threat-composer/src/components/workspaces/WorkspaceInsights/components/ThreatGrammar/index.tsx index 6a890c6f..f7ae110b 100644 --- a/packages/threat-composer/src/components/workspaces/WorkspaceInsights/components/ThreatGrammar/index.tsx +++ b/packages/threat-composer/src/components/workspaces/WorkspaceInsights/components/ThreatGrammar/index.tsx @@ -26,20 +26,21 @@ import { colorChartsPaletteCategorical2, colorChartsPaletteCategorical1, } from '@cloudscape-design/design-tokens'; -import { useState, useMemo, useCallback } from 'react'; +import { useState, useMemo, useCallback, FC } from 'react'; import { ALL_LEVELS, LEVEL_SELECTOR_OPTIONS_INCLUDING_ALL, DEFAULT_NEW_ENTITY_ID, } from '../../../../../configs'; import { useThreatsContext } from '../../../../../contexts/ThreatsContext'; -import { useWorkspacesContext } from '../../../../../contexts/WorkspacesContext'; import filterThreatsByMetadata from '../../../../../utils/filterThreatsByMetadata'; import DashboardNumber from '../../../../generic/DashboardNumber'; +import { WorkspaceInsightsProps } from '../../types'; -const ThreatGrammar = () => { +const ThreatGrammar: FC = ({ + onThreatEditorView, +}) => { const { statementList, addStatement } = useThreatsContext(); - const { onThreatEditorView } = useWorkspacesContext(); const [selectedPriority, setSelectedPriority] = useState( ALL_LEVELS, diff --git a/packages/threat-composer/src/components/workspaces/WorkspaceInsights/components/ThreatPrioritization/index.tsx b/packages/threat-composer/src/components/workspaces/WorkspaceInsights/components/ThreatPrioritization/index.tsx index 4507568a..d4b43b46 100644 --- a/packages/threat-composer/src/components/workspaces/WorkspaceInsights/components/ThreatPrioritization/index.tsx +++ b/packages/threat-composer/src/components/workspaces/WorkspaceInsights/components/ThreatPrioritization/index.tsx @@ -25,7 +25,7 @@ import { colorChartsStatusHigh, colorChartsStatusNeutral, } from '@cloudscape-design/design-tokens'; -import { useMemo, useCallback } from 'react'; +import { useMemo, useCallback, FC } from 'react'; import { LEVEL_HIGH, LEVEL_LOW, @@ -34,16 +34,18 @@ import { DEFAULT_NEW_ENTITY_ID, } from '../../../../../configs'; import { useThreatsContext } from '../../../../../contexts/ThreatsContext'; -import { useWorkspacesContext } from '../../../../../contexts/WorkspacesContext'; import filterThreatsByMetadata from '../../../../../utils/filterThreatsByMetadata'; import DashboardNumber from '../../../../generic/DashboardNumber'; import useLinkClicked from '../../hooks/useLinkClicked'; +import { WorkspaceInsightsProps } from '../../types'; -const ThreatPrioritization = () => { +const ThreatPrioritization: FC = ({ + onThreatEditorView, + onThreatListView, +}) => { const { statementList, addStatement } = useThreatsContext(); - const { onThreatEditorView } = useWorkspacesContext(); - const handleLinkClicked = useLinkClicked(); + const handleLinkClicked = useLinkClicked(onThreatListView); const missingPriority = useMemo( () => filterThreatsByMetadata(statementList, 'Priority').length, diff --git a/packages/threat-composer/src/components/workspaces/WorkspaceInsights/hooks/useLinkClicked/index.tsx b/packages/threat-composer/src/components/workspaces/WorkspaceInsights/hooks/useLinkClicked/index.tsx index cceec1af..6056c4c4 100644 --- a/packages/threat-composer/src/components/workspaces/WorkspaceInsights/hooks/useLinkClicked/index.tsx +++ b/packages/threat-composer/src/components/workspaces/WorkspaceInsights/hooks/useLinkClicked/index.tsx @@ -15,11 +15,9 @@ ******************************************************************************************************************** */ import { CancelableEventHandler, BaseNavigationDetail } from '@cloudscape-design/components/internal/events'; import { useCallback } from 'react'; -import { useThreatsContext } from '../../../../../contexts/ThreatsContext'; -import { ThreatStatementListFilter } from '../../../../../customTypes'; +import { ThreatStatementListFilter, ViewNavigationEvent } from '../../../../../customTypes'; -const useLinkClicked = () => { - const { onThreatListView } = useThreatsContext(); +const useLinkClicked = (onThreatListView: ViewNavigationEvent['onThreatListView']) => { return useCallback((filter?: ThreatStatementListFilter): CancelableEventHandler => (event) => { event?.preventDefault?.(); onThreatListView?.(filter); diff --git a/packages/threat-composer/src/components/workspaces/WorkspaceInsights/index.tsx b/packages/threat-composer/src/components/workspaces/WorkspaceInsights/index.tsx index 250759d6..3f2ce4a5 100644 --- a/packages/threat-composer/src/components/workspaces/WorkspaceInsights/index.tsx +++ b/packages/threat-composer/src/components/workspaces/WorkspaceInsights/index.tsx @@ -17,43 +17,46 @@ import Board, { BoardProps } from '@cloudscape-design/board-components/board'; import BoardItem from '@cloudscape-design/board-components/board-item'; import ContentLayout from '@cloudscape-design/components/content-layout'; import Header from '@cloudscape-design/components/header'; -import { useState, ReactNode, useCallback } from 'react'; +import { useState, ReactNode, useCallback, FC } from 'react'; import Overview from './components/Overview'; import STRIDEAllocation from './components/STRIDEAllocation'; import ThreatGrammar from './components/ThreatGrammar'; import ThreatPrioritization from './components/ThreatPrioritization'; +import { WorkspaceInsightsProps } from './types'; import { useApplicationInfoContext } from '../../../contexts/ApplicationContext'; +export * from './types'; + export interface ItemType { title: string; content: ReactNode; } -const WorkspaceInsights = () => { +const WorkspaceInsights: FC = (props) => { const [items, setItems] = useState[]>([ { id: 'overview', rowSpan: 2, columnSpan: 6, - data: { title: 'Threat summary', content: }, + data: { title: 'Threat summary', content: }, }, { id: 'threat-prioritization', rowSpan: 5, columnSpan: 2, - data: { title: 'Threat prioritization', content: }, + data: { title: 'Threat prioritization', content: }, }, { id: 'stride-allocation', rowSpan: 5, columnSpan: 2, - data: { title: 'Threat category distribution', content: }, + data: { title: 'Threat category distribution', content: }, }, { id: 'threat-grammer', rowSpan: 5, columnSpan: 2, - data: { title: 'Threat grammar distribution', content: }, + data: { title: 'Threat grammar distribution', content: }, }, ]); @@ -182,4 +185,4 @@ const WorkspaceInsights = () => { />); }; -export default WorkspaceInsights; \ No newline at end of file +export default WorkspaceInsights; diff --git a/packages/threat-composer/src/components/workspaces/WorkspaceInsights/types.tsx b/packages/threat-composer/src/components/workspaces/WorkspaceInsights/types.tsx new file mode 100644 index 00000000..b568569a --- /dev/null +++ b/packages/threat-composer/src/components/workspaces/WorkspaceInsights/types.tsx @@ -0,0 +1,22 @@ +/** ******************************************************************************************************************* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ******************************************************************************************************************** */ + +import { ViewNavigationEvent } from '../../../customTypes'; + +export interface WorkspaceInsightsProps { + onThreatEditorView?: ViewNavigationEvent['onThreatEditorView']; + onThreatListView?: ViewNavigationEvent['onThreatListView']; +} \ No newline at end of file diff --git a/packages/threat-composer/src/components/workspaces/WorkspaceSelector/index.tsx b/packages/threat-composer/src/components/workspaces/WorkspaceSelector/index.tsx index d4034b08..9e14c66a 100644 --- a/packages/threat-composer/src/components/workspaces/WorkspaceSelector/index.tsx +++ b/packages/threat-composer/src/components/workspaces/WorkspaceSelector/index.tsx @@ -57,6 +57,9 @@ export interface WorkspaceSelectorProps { eventName?: string; onClick?: (data: DataExchangeFormat) => void; }; + onPreview?: (data: DataExchangeFormat) => void; + onPreviewClose?: () => void; + onImported?: () => void; } const WorkspaceSelector: FC> = ({ @@ -68,6 +71,9 @@ const WorkspaceSelector: FC> = ({ filteredThreats, singletonMode = false, singletonPrimaryActionButtonConfig, + onPreview, + onPreviewClose, + onImported, }) => { const [addWorkspaceModalVisible, setAddWorkspaceModalVisible] = useState(false); @@ -86,9 +92,6 @@ const WorkspaceSelector: FC> = ({ const { appMode, composerMode, - onPreview, - onPreviewClose, - onImported, fileImportModalVisible, setFileImportModalVisible, } = useGlobalSetupContext(); @@ -376,6 +379,7 @@ const WorkspaceSelector: FC> = ({ await addWorkspace(workspaceName); }} workspaceList={workspaceList} + exampleWorkspaceList={workspaceExamples} /> )} {editWorkspaceModalVisible && currentWorkspace && ( @@ -389,6 +393,7 @@ const WorkspaceSelector: FC> = ({ } currentWorkspace={currentWorkspace} workspaceList={workspaceList} + exampleWorkspaceList={workspaceExamples} /> )} {removeDataModalVisible && ( diff --git a/packages/threat-composer/src/contexts/ContextAggregator/index.tsx b/packages/threat-composer/src/contexts/ContextAggregator/index.tsx index e9c9951f..21d50c86 100644 --- a/packages/threat-composer/src/contexts/ContextAggregator/index.tsx +++ b/packages/threat-composer/src/contexts/ContextAggregator/index.tsx @@ -14,13 +14,13 @@ limitations under the License. ******************************************************************************************************************** */ import { FC, PropsWithChildren } from 'react'; -import { AppMode, ComposerMode, DataExchangeFormat, ViewNavigationEvent } from '../../customTypes'; +import { AppMode, ComposerMode, DataExchangeFormat } from '../../customTypes'; import GlobalSetupContextProvider from '../GlobalSetupContext'; import WorkspaceContextAggregator from '../WorkspaceContextAggregator'; import WorkspaceExamplesContext from '../WorkspaceExamplesContext'; import WorkspacesContextProvider, { WorkspacesContextProviderProps } from '../WorkspacesContext'; -export interface ContextAggregatorProps extends ViewNavigationEvent { +export interface ContextAggregatorProps { composerMode?: ComposerMode; appMode?: AppMode; features?: string[]; @@ -37,19 +37,11 @@ const ContextAggregator: FC> = ({ appMode, composerMode = 'Full', features, - onPreview, - onPreviewClose, - onImported, - onDefineWorkload, ...props }) => { return ( @@ -60,8 +52,6 @@ const ContextAggregator: FC> = ({ {(workspaceId) => ( {children} )} diff --git a/packages/threat-composer/src/contexts/GlobalSetupContext/context.ts b/packages/threat-composer/src/contexts/GlobalSetupContext/context.ts index a96630fb..ad77b19a 100644 --- a/packages/threat-composer/src/contexts/GlobalSetupContext/context.ts +++ b/packages/threat-composer/src/contexts/GlobalSetupContext/context.ts @@ -14,18 +14,14 @@ limitations under the License. ******************************************************************************************************************** */ import React, { useContext, createContext } from 'react'; -import { AppMode, ComposerMode, DataExchangeFormat } from '../../customTypes'; +import { AppMode, ComposerMode } from '../../customTypes'; export interface GlobalSetupContextApi { hasVisitBefore: boolean; showInfoModal: () => void; composerMode: ComposerMode; appMode: AppMode; - features?: string[]; - onPreview?: (content: DataExchangeFormat) => void; - onPreviewClose?: () => void; - onImported?: () => void; - onDefineWorkload?: () => void; + features: string[]; fileImportModalVisible: boolean; setFileImportModalVisible: React.Dispatch>; } diff --git a/packages/threat-composer/src/contexts/GlobalSetupContext/index.tsx b/packages/threat-composer/src/contexts/GlobalSetupContext/index.tsx index 7907fda0..bae7fd21 100644 --- a/packages/threat-composer/src/contexts/GlobalSetupContext/index.tsx +++ b/packages/threat-composer/src/contexts/GlobalSetupContext/index.tsx @@ -21,17 +21,13 @@ import { GlobalSetupContext, useGlobalSetupContext } from './context'; import { useThemeContext } from '../../components/generic/ThemeProvider'; import InfoModal from '../../components/global/InfoModal'; import { LOCAL_STORAGE_KEY_NEW_VISIT_FLAG } from '../../configs/localStorageKeys'; -import { ComposerMode, DataExchangeFormat, AppMode } from '../../customTypes'; +import { ComposerMode, AppMode } from '../../customTypes'; import EventController from '../../utils/EventController'; export interface GlobalSetupContextProviderProps { composerMode?: ComposerMode; appMode?: AppMode; features?: string[]; - onPreview?: (content: DataExchangeFormat) => void; - onPreviewClose?: () => void; - onImported?: () => void; - onDefineWorkload?: () => void; } const stringifyWorkspaceData = (data: any) => { @@ -52,10 +48,6 @@ const GlobalSetupContextProvider: FC { const [fileImportModalVisible, setFileImportModalVisible] = useState(false); const { setTheme, setDensity } = useThemeContext(); @@ -88,14 +80,10 @@ const GlobalSetupContextProvider: FC setInfoModalVisible(true), - onPreview, - onPreviewClose, - onImported, fileImportModalVisible, setFileImportModalVisible, - onDefineWorkload, }}> {children} {infoModalVisible && >> = ({ children, initialValue, - onThreatListView, - onThreatEditorView, }) => { const [editingStatement, setEditingStatement] = useState(null); @@ -83,8 +81,6 @@ const ThreatsContextProvider: FC {children} ); diff --git a/packages/threat-composer/src/contexts/ThreatsContext/components/LocalStorageContextProvider/index.tsx b/packages/threat-composer/src/contexts/ThreatsContext/components/LocalStorageContextProvider/index.tsx index 880382b7..f3717138 100644 --- a/packages/threat-composer/src/contexts/ThreatsContext/components/LocalStorageContextProvider/index.tsx +++ b/packages/threat-composer/src/contexts/ThreatsContext/components/LocalStorageContextProvider/index.tsx @@ -41,8 +41,6 @@ interface ThreatsContextProviderInnerProps { const ThreatsContextProviderInner: FC> = ({ children, workspaceId: currentWorkspaceId, - onThreatListView, - onThreatEditorView, editingStatement, setEditingStatement, removeEditingStatement, @@ -117,8 +115,6 @@ const ThreatsContextProviderInner: FC {children} ); diff --git a/packages/threat-composer/src/contexts/ThreatsContext/context.ts b/packages/threat-composer/src/contexts/ThreatsContext/context.ts index 3208c5d0..6abc0f20 100644 --- a/packages/threat-composer/src/contexts/ThreatsContext/context.ts +++ b/packages/threat-composer/src/contexts/ThreatsContext/context.ts @@ -14,7 +14,7 @@ limitations under the License. ******************************************************************************************************************** */ import { useContext, createContext } from 'react'; -import { PerFieldExample, TemplateThreatStatement, ThreatStatementListFilter } from '../../customTypes'; +import { PerFieldExample, TemplateThreatStatement } from '../../customTypes'; import threatStatementExamplesData from '../../data/threatStatementExamples.json'; export type View = 'list' | 'editor'; @@ -52,8 +52,6 @@ export interface ThreatsContextApi { saveStatement: (statement: TemplateThreatStatement) => void; removeAllStatements: () => Promise; onDeleteWorkspace: (workspaceId: string) => Promise; - onThreatListView?: (filter?: ThreatStatementListFilter) => void; - onThreatEditorView?: (threatId: string, idToCopied?: string) => void; } const initialState: ThreatsContextApi = { diff --git a/packages/threat-composer/src/contexts/WorkspaceContextAggregator/index.tsx b/packages/threat-composer/src/contexts/WorkspaceContextAggregator/index.tsx index b82f671e..ce5fcda2 100644 --- a/packages/threat-composer/src/contexts/WorkspaceContextAggregator/index.tsx +++ b/packages/threat-composer/src/contexts/WorkspaceContextAggregator/index.tsx @@ -27,6 +27,7 @@ import MitigationPacksContextProvider from '../MitigationPacksContext'; import MitigationsContextProvider from '../MitigationsContext'; import ThreatPacksContextProvider from '../ThreatPacksContext'; import ThreatsContextProvider from '../ThreatsContext'; + export interface WorkspaceContextAggregatorProps extends ViewNavigationEvent { workspaceId: string | null; composerMode?: ComposerMode; @@ -39,15 +40,10 @@ export interface WorkspaceContextAggregatorProps extends ViewNavigationEvent { const WorkspaceContextInnerAggregator: FC> = ({ children, workspaceId, - onThreatEditorView, - onThreatListView, }) => { return ( - @@ -85,11 +81,7 @@ const WorkspaceContextAggregator: FC { return requiredGlobalSetupContext ? ( - + {children} diff --git a/packages/threat-composer/src/contexts/WorkspacesContext/components/LocalStateContextProvider/index.tsx b/packages/threat-composer/src/contexts/WorkspacesContext/components/LocalStateContextProvider/index.tsx index 02e5aaaf..6d7a92fa 100644 --- a/packages/threat-composer/src/contexts/WorkspacesContext/components/LocalStateContextProvider/index.tsx +++ b/packages/threat-composer/src/contexts/WorkspacesContext/components/LocalStateContextProvider/index.tsx @@ -13,23 +13,44 @@ See the License for the specific language governing permissions and limitations under the License. ******************************************************************************************************************** */ -import { FC, useEffect, useState } from 'react'; +import { FC, useState } from 'react'; import { DEFAULT_WORKSPACE_ID } from '../../../../configs/constants'; import { Workspace } from '../../../../customTypes'; +import { useWorkspaceExamplesContext } from '../../../WorkspaceExamplesContext'; import { WorkspacesContext } from '../../context'; import { WorkspacesContextProviderProps } from '../../types'; import useWorkspaces from '../../useWorkspaces'; const WorkspacesLocalStateContextProvider: FC = ({ children, - workspaceId, + workspaceName, onWorkspaceChanged, ...props }) => { - const [currentWorkspace, setCurrentWorkspace] = useState(null); + const { workspaceExamples } = useWorkspaceExamplesContext(); const [workspaceList, setWorkspaceList] = useState([]); + const [currentWorkspace, setCurrentWorkspace] = useState(() => { + if (workspaceName) { // If the workspaceName is specified by outside scope (e.g. Url), return the workspace specified by the id + if (workspaceName === DEFAULT_WORKSPACE_ID) { + return null; + } + + const foundWorkspace = workspaceList.find(x => x.name === workspaceName); + if (foundWorkspace) { + return foundWorkspace; + } + + const foundWorkspaceExample = workspaceExamples.find(x => x.name === workspaceName); + if (foundWorkspaceExample) { + return foundWorkspaceExample; + } + } + + return null; + }); + const { handleSwitchWorkspace, handleAddWorkspace, @@ -37,21 +58,6 @@ const WorkspacesLocalStateContextProvider: FC = handleRenameWorkspace, } = useWorkspaces(workspaceList, setWorkspaceList, currentWorkspace, setCurrentWorkspace, onWorkspaceChanged); - useEffect(() => { - if (workspaceId) { - if (workspaceId === DEFAULT_WORKSPACE_ID && currentWorkspace !== null) { - setCurrentWorkspace(null); - } else if (workspaceId !== currentWorkspace?.id) { - const foundWorkspace = workspaceList.find(x => x.id === workspaceId); - if (foundWorkspace) { - setCurrentWorkspace(foundWorkspace); - } else { - setCurrentWorkspace(null); - } - } - } - }, [workspaceId, workspaceList, currentWorkspace]); - return ( = ({ children, - workspaceId, + workspaceName, onWorkspaceChanged, ...props }) => { - const [currentWorkspace, setCurrentWorkspace] = useLocalStorageState(LOCAL_STORAGE_KEY_CURRENT_WORKSPACE, { - defaultValue: null, - }); - const [workspaceList, setWorkspaceList] = useLocalStorageState(LOCAL_STORAGE_KEY_WORKSPACE_LIST, { defaultValue: [], }); + const [lastWorkspace, setCurrentWorkspace] = useLocalStorageState(LOCAL_STORAGE_KEY_CURRENT_WORKSPACE, { + defaultValue: null, + }); + + const { workspaceExamples } = useWorkspaceExamplesContext(); + + const currentWorkspace = useMemo(() => { + if (workspaceName) { // If the workspaceName is specified by outside scope (e.g. Url), return the workspace specified by the id + if (workspaceName === DEFAULT_WORKSPACE_ID) { + return null; + } + + const foundWorkspace = workspaceList.find(x => x.name === workspaceName); + if (foundWorkspace) { + return foundWorkspace; + } + + const foundWorkspaceExample = workspaceExamples.find(x => x.name === workspaceName); + if (foundWorkspaceExample) { + return foundWorkspaceExample; + } + } + + return lastWorkspace; + }, [lastWorkspace, workspaceName, workspaceExamples, workspaceList]); + const { handleSwitchWorkspace, handleAddWorkspace, @@ -44,21 +67,6 @@ const WorkspacesLocalStorageContextProvider: FC handleRenameWorkspace, } = useWorkspaces(workspaceList, setWorkspaceList, currentWorkspace, setCurrentWorkspace, onWorkspaceChanged); - useEffect(() => { - if (workspaceId) { - if (workspaceId === DEFAULT_WORKSPACE_ID && currentWorkspace !== null) { - setCurrentWorkspace(null); - } else if (workspaceId !== currentWorkspace?.id) { - const foundWorkspace = workspaceList.find(x => x.id === workspaceId); - if (foundWorkspace) { - setCurrentWorkspace(foundWorkspace); - } else { - setCurrentWorkspace(null); - } - } - } - }, [workspaceId, workspaceList, currentWorkspace]); - return ( void; currentWorkspace: Workspace | null; diff --git a/packages/threat-composer/src/contexts/WorkspacesContext/index.tsx b/packages/threat-composer/src/contexts/WorkspacesContext/index.tsx index badd36a4..8823f070 100644 --- a/packages/threat-composer/src/contexts/WorkspacesContext/index.tsx +++ b/packages/threat-composer/src/contexts/WorkspacesContext/index.tsx @@ -18,7 +18,7 @@ import WorkspacesLocalStateContextProvider from './components/LocalStateContextP import WorkspacesLocalStorageContextProvider from './components/LocalStorageContextProvider'; import { useWorkspacesContext } from './context'; import { WorkspacesContextProviderProps } from './types'; -import { APP_MODE_IDE_EXTENSION } from '../../configs'; +import { APP_MODE_IDE_EXTENSION, DEFAULT_WORKSPACE_ID } from '../../configs'; import { useGlobalSetupContext } from '../GlobalSetupContext'; const WorkspacesContextProvider: FC = (props) => { @@ -26,9 +26,9 @@ const WorkspacesContextProvider: FC = (props) => return appMode === APP_MODE_IDE_EXTENSION ? () : - (); + (); }; export default WorkspacesContextProvider; diff --git a/packages/threat-composer/src/contexts/WorkspacesContext/types.ts b/packages/threat-composer/src/contexts/WorkspacesContext/types.ts index e5596ecc..634754b0 100644 --- a/packages/threat-composer/src/contexts/WorkspacesContext/types.ts +++ b/packages/threat-composer/src/contexts/WorkspacesContext/types.ts @@ -16,7 +16,7 @@ import { ReactElement } from 'react'; export interface WorkspacesContextProviderProps { - workspaceId?: string; + workspaceName?: string; // The name is used in url onWorkspaceChanged?: (workspaceId: string) => void; children: (workspace: string | null) => ReactElement<{ workspaceId: string | null }>; } diff --git a/packages/threat-composer/src/contexts/WorkspacesContext/useWorkspaces.ts b/packages/threat-composer/src/contexts/WorkspacesContext/useWorkspaces.ts index 50a8b0c3..049e3c05 100644 --- a/packages/threat-composer/src/contexts/WorkspacesContext/useWorkspaces.ts +++ b/packages/threat-composer/src/contexts/WorkspacesContext/useWorkspaces.ts @@ -26,7 +26,7 @@ const useWorkspaces = ( setWorkspaceList: React.Dispatch>, currentWorkspace: Workspace | null, setCurrentWorkspace: React.Dispatch>, - onWorkspaceChanged?: (workspaceId: string) => void, + onWorkspaceChanged?: (workspaceName: string) => void, ) => { const { workspaceExamples } = useWorkspaceExamplesContext(); @@ -49,7 +49,7 @@ const useWorkspaces = ( const handleSwitchWorkspace = useCallback((toBeSwitchedWorkspaceId: string | null) => { const workspace = getWorkspace(toBeSwitchedWorkspaceId); setCurrentWorkspace(workspace); - onWorkspaceChanged?.(workspace?.id || DEFAULT_WORKSPACE_ID); + onWorkspaceChanged?.(workspace?.name || DEFAULT_WORKSPACE_ID); }, [onWorkspaceChanged, getWorkspace]); const handleAddWorkspace = useCallback(async (workspaceName: string, @@ -63,7 +63,7 @@ const useWorkspaces = ( }; setWorkspaceList(prev => [...prev, newWorkspace]); setCurrentWorkspace(newWorkspace); - onWorkspaceChanged?.(newWorkspace.id); + onWorkspaceChanged?.(newWorkspace.name); return newWorkspace; }, [onWorkspaceChanged]); diff --git a/packages/threat-composer/src/contexts/index.ts b/packages/threat-composer/src/contexts/index.ts index c36dcadb..00d14683 100644 --- a/packages/threat-composer/src/contexts/index.ts +++ b/packages/threat-composer/src/contexts/index.ts @@ -39,3 +39,4 @@ export { default as MitigationPacksContext } from './MitigationPacksContext'; export { useMitigationPacksContext } from './MitigationPacksContext/context'; export { default as WorkspaceContextAggregator } from './WorkspaceContextAggregator'; export { default as ContextAggregator } from './ContextAggregator'; +export { default as WorkspaceExamplesContext } from './WorkspaceExamplesContext'; diff --git a/packages/threat-composer/src/customTypes/components.ts b/packages/threat-composer/src/customTypes/components.ts index 00b66cc6..23181f00 100644 --- a/packages/threat-composer/src/customTypes/components.ts +++ b/packages/threat-composer/src/customTypes/components.ts @@ -13,6 +13,10 @@ See the License for the specific language governing permissions and limitations under the License. ******************************************************************************************************************** */ +import { ComponentType } from 'react'; +import { MarkdownEditorProps } from '../components/generic/MarkdownEditor'; + export interface EditableComponentBaseProps { onEditModeChange?: (editMode: boolean) => void; + MarkdownEditorComponentType?: ComponentType; } \ No newline at end of file diff --git a/packages/threat-composer/src/customTypes/events.ts b/packages/threat-composer/src/customTypes/events.ts index da7fdba3..55b1f821 100644 --- a/packages/threat-composer/src/customTypes/events.ts +++ b/packages/threat-composer/src/customTypes/events.ts @@ -22,5 +22,5 @@ export interface ViewNavigationEvent { onAssumptionListView?: () => void; onMitigationListView?: () => void; onThreatListView?: (filter?: ThreatStatementListFilter) => void; - onThreatEditorView?: (threatId: string) => void; + onThreatEditorView?: (threatId: string, idToCopy?: string) => void; } \ No newline at end of file diff --git a/projenrc/app.ts b/projenrc/app.ts index 477590a8..1ad75e0d 100644 --- a/projenrc/app.ts +++ b/projenrc/app.ts @@ -15,6 +15,7 @@ class ThreatComposerReactAppProject extends ReactTypeScriptProject { "@cloudscape-design/global-styles", "@cloudscape-design/design-tokens", "@aws-northstar/ui", + "@uidotdev/usehooks", "react-router-dom", "uuid", "docx", diff --git a/yarn.lock b/yarn.lock index 1c0e62b6..f8e8d6dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7796,6 +7796,11 @@ "@typescript-eslint/types" "6.21.0" eslint-visitor-keys "^3.4.1" +"@uidotdev/usehooks@^2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@uidotdev/usehooks/-/usehooks-2.4.1.tgz#4b733eaeae09a7be143c6c9ca158b56cc1ea75bf" + integrity sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg== + "@vitejs/plugin-react@^4.2.1": version "4.2.1" resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-4.2.1.tgz#744d8e4fcb120fc3dbaa471dadd3483f5a304bb9"