From 1b14a6c13d0d98b0705c2b5c876f95855a8ad612 Mon Sep 17 00:00:00 2001 From: Jessie Wei Date: Tue, 29 Aug 2023 17:58:38 +1000 Subject: [PATCH] feat(ThreatModelView): Allow users to download the markdown file (#37) --- .../components/ThreatModelView/index.tsx | 27 +++++++++++++------ .../components/report/ThreatModel/index.tsx | 10 ++++++- .../src/hooks/useExportImport/index.ts | 9 ++----- .../src/utils/downloadObjectAsJson/index.ts | 1 + .../utils/downloadObjectAsMarkdown/index.ts | 26 ++++++++++++++++++ .../src/utils/getExportFileName/index.ts | 24 +++++++++++++++++ 6 files changed, 81 insertions(+), 16 deletions(-) create mode 100644 packages/threat-composer/src/utils/downloadObjectAsMarkdown/index.ts create mode 100644 packages/threat-composer/src/utils/getExportFileName/index.ts diff --git a/packages/threat-composer/src/components/report/ThreatModel/components/ThreatModelView/index.tsx b/packages/threat-composer/src/components/report/ThreatModel/components/ThreatModelView/index.tsx index 855a9dab..677d33ef 100644 --- a/packages/threat-composer/src/components/report/ThreatModel/components/ThreatModelView/index.tsx +++ b/packages/threat-composer/src/components/report/ThreatModel/components/ThreatModelView/index.tsx @@ -26,6 +26,7 @@ import { css } from '@emotion/react'; import { FC, useEffect, useCallback, useState, ReactNode } from 'react'; import { DataExchangeFormat, HasContentDetails, ViewNavigationEvent } from '../../../../../customTypes'; import printStyles from '../../../../../styles/print'; +import downloadContentAsMarkdown from '../../../../../utils/downloadObjectAsMarkdown'; import sanitizeHtml from '../../../../../utils/sanitizeHtml'; import MarkdownViewer from '../../../../generic/MarkdownViewer'; import { getApplicationInfoContent } from '../../utils/getApplicationInfo'; @@ -56,6 +57,7 @@ const styles = { export interface ThreatModelViewProps extends ViewNavigationEvent { composerMode: string; data: DataExchangeFormat; + downloadFileName?: string; onPrintButtonClick?: () => void; hasContentDetails?: HasContentDetails; } @@ -63,6 +65,7 @@ export interface ThreatModelViewProps extends ViewNavigationEvent { const ThreatModelView: FC = ({ data, composerMode, + downloadFileName, onPrintButtonClick, hasContentDetails, ...props @@ -75,14 +78,14 @@ const ThreatModelView: FC = ({ setLoading(true); const sanitizedData = sanitizeHtml(data); const processedContent = (composerMode === 'Full' ? [ - hasContentDetails?.applicationName && await getApplicationName(sanitizedData), - hasContentDetails?.applicationInfo && await getApplicationInfoContent(sanitizedData), - hasContentDetails?.architecture && await getArchitectureContent(sanitizedData), - hasContentDetails?.dataflow && await getDataflowContent(sanitizedData), - hasContentDetails?.assumptions && await getAssumptionsContent(sanitizedData), - hasContentDetails?.threats && await getThreatsContent(sanitizedData), - hasContentDetails?.mitigations && await getMitigationsContent(sanitizedData), - hasContentDetails?.threats && await getAssetsContent(sanitizedData), + (!hasContentDetails || hasContentDetails.applicationName) && await getApplicationName(sanitizedData), + (!hasContentDetails || hasContentDetails.applicationInfo) && await getApplicationInfoContent(sanitizedData), + (!hasContentDetails || hasContentDetails.architecture) && await getArchitectureContent(sanitizedData), + (!hasContentDetails || hasContentDetails.dataflow) && await getDataflowContent(sanitizedData), + (!hasContentDetails || hasContentDetails.assumptions) && await getAssumptionsContent(sanitizedData), + (!hasContentDetails || hasContentDetails.threats) && await getThreatsContent(sanitizedData), + (!hasContentDetails || hasContentDetails.mitigations) && await getMitigationsContent(sanitizedData), + (!hasContentDetails || hasContentDetails.threats) && await getAssetsContent(sanitizedData), ] : [await getThreatsContent(sanitizedData, true)]).filter(x => !!x).join('\n'); setContent(processedContent); @@ -96,6 +99,10 @@ const ThreatModelView: FC = ({ await navigator.clipboard.writeText(content); }, [content]); + const handleDownloadMarkdown = useCallback(() => { + downloadFileName && downloadContentAsMarkdown(content, downloadFileName); + }, [content, downloadFileName]); + const getNextStepButtons = useCallback(() => { const buttons: ReactNode[] = []; if (!hasContentDetails?.applicationInfo) { @@ -141,6 +148,10 @@ const ThreatModelView: FC = ({ Copy as Markdown + {downloadFileName && } } diff --git a/packages/threat-composer/src/components/report/ThreatModel/index.tsx b/packages/threat-composer/src/components/report/ThreatModel/index.tsx index 5684c34e..a5b66efa 100644 --- a/packages/threat-composer/src/components/report/ThreatModel/index.tsx +++ b/packages/threat-composer/src/components/report/ThreatModel/index.tsx @@ -13,11 +13,12 @@ See the License for the specific language governing permissions and limitations under the License. ******************************************************************************************************************** */ -import { FC } from 'react'; +import { FC, useMemo } from 'react'; import ThreatModelView from './components/ThreatModelView'; import { useGlobalSetupContext, useWorkspacesContext } from '../../../contexts'; import useImportExport from '../../../hooks/useExportImport'; import useHasContent from '../../../hooks/useHasContent'; +import getExportFileName from '../../../utils/getExportFileName'; export interface ThreatModelProps { onPrintButtonClick?: () => void; @@ -29,6 +30,12 @@ const ThreatModel: FC = ({ const { getWorkspaceData } = useImportExport(); const { composerMode } = useGlobalSetupContext(); const [_, hasContentDetails] = useHasContent(); + const { currentWorkspace } = useWorkspacesContext(); + + const downloadFileName = useMemo(() => { + return getExportFileName(composerMode, false, currentWorkspace); + }, [composerMode, currentWorkspace]); + const { onApplicationInfoView, onArchitectureView, @@ -41,6 +48,7 @@ const ThreatModel: FC = ({ onPrintButtonClick={onPrintButtonClick} composerMode={composerMode} data={getWorkspaceData()} + downloadFileName={downloadFileName} hasContentDetails={hasContentDetails} onApplicationInfoView={onApplicationInfoView} onArchitectureView={onArchitectureView} diff --git a/packages/threat-composer/src/hooks/useExportImport/index.ts b/packages/threat-composer/src/hooks/useExportImport/index.ts index e6c8503f..f17034b0 100644 --- a/packages/threat-composer/src/hooks/useExportImport/index.ts +++ b/packages/threat-composer/src/hooks/useExportImport/index.ts @@ -14,7 +14,6 @@ limitations under the License. ******************************************************************************************************************** */ import { useCallback } from 'react'; -import { EXPORT_FILE_NAME } from '../../configs/export'; import { useWorkspacesContext } from '../../contexts'; import { useApplicationInfoContext } from '../../contexts/ApplicationContext/context'; import { useArchitectureInfoContext } from '../../contexts/ArchitectureContext/context'; @@ -25,18 +24,14 @@ import { useGlobalSetupContext } from '../../contexts/GlobalSetupContext/context import { useMitigationLinksContext } from '../../contexts/MitigationLinksContext/context'; import { useMitigationsContext } from '../../contexts/MitigationsContext/context'; import { useThreatsContext } from '../../contexts/ThreatsContext/context'; -import { ComposerMode, DataExchangeFormat, TemplateThreatStatement, Workspace } from '../../customTypes'; +import { DataExchangeFormat, TemplateThreatStatement } from '../../customTypes'; import downloadObjectAsJson from '../../utils/downloadObjectAsJson'; +import getExportFileName from '../../utils/getExportFileName'; import sanitizeHtml from '../../utils/sanitizeHtml'; import validateData from '../../utils/validateData'; const SCHEMA_VERSION = 1.0; -const getExportFileName = (composerMode: ComposerMode, filtered: boolean, currentWorkspace: Workspace | null) => { - const exportFileName = `${EXPORT_FILE_NAME}_Workspace_${currentWorkspace ? currentWorkspace.name.replace(' ', '-') : 'Default'}${composerMode !== 'Full' ? '_ThreatsOnly' : ''}${filtered ? '_Filtered' : ''}`; - return exportFileName; -}; - const useImportExport = () => { const { composerMode } = useGlobalSetupContext(); const { currentWorkspace } = useWorkspacesContext(); diff --git a/packages/threat-composer/src/utils/downloadObjectAsJson/index.ts b/packages/threat-composer/src/utils/downloadObjectAsJson/index.ts index 1e77278b..f4ad4887 100644 --- a/packages/threat-composer/src/utils/downloadObjectAsJson/index.ts +++ b/packages/threat-composer/src/utils/downloadObjectAsJson/index.ts @@ -23,4 +23,5 @@ const downloadObjectAsJson = (exportObj: any, exportName: string) => { downloadAnchorNode.remove(); }; + export default downloadObjectAsJson; \ No newline at end of file diff --git a/packages/threat-composer/src/utils/downloadObjectAsMarkdown/index.ts b/packages/threat-composer/src/utils/downloadObjectAsMarkdown/index.ts new file mode 100644 index 00000000..9080395b --- /dev/null +++ b/packages/threat-composer/src/utils/downloadObjectAsMarkdown/index.ts @@ -0,0 +1,26 @@ +/** ******************************************************************************************************************* + 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. + ******************************************************************************************************************** */ +const downloadContentAsMarkdown = (content: any, exportName: string) => { + var dataStr = 'data:text/markdown;charset=utf-8,' + encodeURIComponent(content); + var downloadAnchorNode = document.createElement('a'); + downloadAnchorNode.setAttribute('href', dataStr); + downloadAnchorNode.setAttribute('download', exportName + '.md'); + document.body.appendChild(downloadAnchorNode); + downloadAnchorNode.click(); + downloadAnchorNode.remove(); +}; + +export default downloadContentAsMarkdown; \ No newline at end of file diff --git a/packages/threat-composer/src/utils/getExportFileName/index.ts b/packages/threat-composer/src/utils/getExportFileName/index.ts new file mode 100644 index 00000000..9dc4371a --- /dev/null +++ b/packages/threat-composer/src/utils/getExportFileName/index.ts @@ -0,0 +1,24 @@ +/** ******************************************************************************************************************* + 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 { EXPORT_FILE_NAME } from '../../configs/export'; +import { ComposerMode, Workspace } from '../../customTypes'; + +const getExportFileName = (composerMode: ComposerMode, filtered: boolean, currentWorkspace: Workspace | null) => { + const exportFileName = `${EXPORT_FILE_NAME}_Workspace_${currentWorkspace ? currentWorkspace.name.replace(' ', '-') : 'Default'}${composerMode !== 'Full' ? '_ThreatsOnly' : ''}${filtered ? '_Filtered' : ''}`; + return exportFileName; +}; + +export default getExportFileName; \ No newline at end of file