diff --git a/application/frontend/src/pages/Explorer/explorer.tsx b/application/frontend/src/pages/Explorer/explorer.tsx index c85fc3b3e..c3e78e3af 100644 --- a/application/frontend/src/pages/Explorer/explorer.tsx +++ b/application/frontend/src/pages/Explorer/explorer.tsx @@ -1,181 +1,181 @@ -import './explorer.scss'; - -import React, { useEffect, useState } from 'react'; -import { Link } from 'react-router-dom'; -import { List } from 'semantic-ui-react'; - -import { LoadingAndErrorIndicator } from '../../components/LoadingAndErrorIndicator'; -import { TYPE_CONTAINS, TYPE_LINKED_TO } from '../../const'; -import { useDataStore } from '../../providers/DataProvider'; -import { LinkedTreeDocument, TreeDocument } from '../../types'; -import { getDocumentDisplayName } from '../../utils'; -import { getInternalUrl } from '../../utils/document'; -import { LinkedStandards } from './LinkedStandards'; - -export const Explorer = () => { - const { dataLoading, dataTree } = useDataStore(); - const [loading, setLoading] = useState(false); - const [filter, setFilter] = useState(''); - const [filteredTree, setFilteredTree] = useState(); - const applyHighlight = (text, term) => { - if (!term) return text; - let index = text.toLowerCase().indexOf(term); - if (index >= 0) { - return ( - <> - {text.substring(0, index)} - {text.substring(index, index + term.length)} - {text.substring(index + term.length)} - - ); - } - return text; - }; - - const filterFunc = (doc: TreeDocument, term: string) => - doc?.displayName?.toLowerCase().includes(term) || doc?.name?.toLowerCase().includes(term); - - const recursiveFilter = (doc: TreeDocument, term: string) => { - if (doc.links) { - const filteredLinks: LinkedTreeDocument[] = []; - doc.links.forEach((x) => { - const filteredDoc = recursiveFilter(x.document, term); - if (filterFunc(x.document, term) || filteredDoc) { - filteredLinks.push({ ltype: x.ltype, document: filteredDoc || x.document }); - } - }); - doc.links = filteredLinks; - } - - if (filterFunc(doc, term) || doc.links?.length) { - return doc; // Return the document if it or any of its children (links or standards) matches the term - } - return null; // Return null if the document and its descendants do not match the term - }; - - //accordion - const [collapsedItems, setCollapsedItems] = useState([]); - const isCollapsed = (id: string) => collapsedItems.includes(id); - const toggleItem = (id: string) => { - if (collapsedItems.includes(id)) { - setCollapsedItems(collapsedItems.filter((itemId) => itemId !== id)); - } else { - setCollapsedItems([...collapsedItems, id]); - } - }; - - useEffect(() => { - if (dataTree.length) { - const treeCopy = structuredClone(dataTree); - const filTree: TreeDocument[] = []; - treeCopy - .map((x) => recursiveFilter(x, filter)) - .forEach((x) => { - if (x) { - filTree.push(x); - } - }); - setFilteredTree(filTree); - } - }, [filter, dataTree, setFilteredTree]); - - useEffect(() => { - setLoading(dataLoading); - }, [dataLoading]); - - function processNode(item) { - if (!item) { - return <>; - } - item.displayName = item.displayName ?? getDocumentDisplayName(item); - item.url = item.url ?? getInternalUrl(item); - item.links = item.links ?? []; - - const contains = item.links.filter((x) => x.ltype === TYPE_CONTAINS); - const linkedTo = item.links.filter((x) => x.ltype === TYPE_LINKED_TO); - - const creCode = item.id; - const creName = item.displayName.split(' : ').pop(); - return ( - - - - {contains.length > 0 && ( -
toggleItem(item.id)} - > - -
- )} - - {applyHighlight(creCode, filter)}: - {applyHighlight(creName, filter)} - -
- - {contains.length > 0 && !isCollapsed(item.id) && ( - {contains.map((child) => processNode(child.document))} - )} -
-
- ); - } - - function update(event) { - setFilter(event.target.value.toLowerCase()); - } - - return ( - <> -
-

Open CRE Explorer

-

- A visual explorer of Open Common Requirement Enumerations (CREs). Originally created by:{' '} - - Zeljko Obrenovic - - . -

- - - - - {filteredTree?.map((item) => { - return processNode(item); - })} - -
- - ); -}; +import './explorer.scss'; + +import React, { useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; +import { List } from 'semantic-ui-react'; + +import { LoadingAndErrorIndicator } from '../../components/LoadingAndErrorIndicator'; +import { TYPE_CONTAINS, TYPE_LINKED_TO } from '../../const'; +import { useDataStore } from '../../providers/DataProvider'; +import { LinkedTreeDocument, TreeDocument } from '../../types'; +import { getDocumentDisplayName } from '../../utils'; +import { getInternalUrl } from '../../utils/document'; +import { LinkedStandards } from './LinkedStandards'; + +export const Explorer = () => { + const { dataLoading, dataTree } = useDataStore(); + const [loading, setLoading] = useState(false); + const [filter, setFilter] = useState(''); + const [filteredTree, setFilteredTree] = useState(); + const applyHighlight = (text, term) => { + if (!term) return text; + let index = text.toLowerCase().indexOf(term); + if (index >= 0) { + return ( + <> + {text.substring(0, index)} + {text.substring(index, index + term.length)} + {text.substring(index + term.length)} + + ); + } + return text; + }; + + const filterFunc = (doc: TreeDocument, term: string) => + doc?.displayName?.toLowerCase().includes(term) || doc?.name?.toLowerCase().includes(term); + + const recursiveFilter = (doc: TreeDocument, term: string) => { + if (doc.links) { + const filteredLinks: LinkedTreeDocument[] = []; + doc.links.forEach((x) => { + const filteredDoc = recursiveFilter(x.document, term); + if (filterFunc(x.document, term) || filteredDoc) { + filteredLinks.push({ ltype: x.ltype, document: filteredDoc || x.document }); + } + }); + doc.links = filteredLinks; + } + + if (filterFunc(doc, term) || doc.links?.length) { + return doc; // Return the document if it or any of its children (links or standards) matches the term + } + return null; // Return null if the document and its descendants do not match the term + }; + + //accordion + const [collapsedItems, setCollapsedItems] = useState([]); + const isCollapsed = (id: string) => collapsedItems.includes(id); + const toggleItem = (id: string) => { + if (collapsedItems.includes(id)) { + setCollapsedItems(collapsedItems.filter((itemId) => itemId !== id)); + } else { + setCollapsedItems([...collapsedItems, id]); + } + }; + + useEffect(() => { + if (dataTree.length) { + const treeCopy = structuredClone(dataTree); + const filTree: TreeDocument[] = []; + treeCopy + .map((x) => recursiveFilter(x, filter)) + .forEach((x) => { + if (x) { + filTree.push(x); + } + }); + setFilteredTree(filTree); + } + }, [filter, dataTree, setFilteredTree]); + + useEffect(() => { + setLoading(dataLoading); + }, [dataLoading]); + + function processNode(item) { + if (!item) { + return <>; + } + item.displayName = item.displayName ?? getDocumentDisplayName(item); + item.url = item.url ?? getInternalUrl(item); + item.links = item.links ?? []; + + const contains = item.links.filter((x) => x.ltype === TYPE_CONTAINS); + const linkedTo = item.links.filter((x) => x.ltype === TYPE_LINKED_TO); + + const creCode = item.id; + const creName = item.displayName.split(' : ').pop(); + return ( + + + + {contains.length > 0 && ( +
toggleItem(item.id)} + > + +
+ )} + + {applyHighlight(creCode, filter)}: + {applyHighlight(creName, filter)} + +
+ + {contains.length > 0 && !isCollapsed(item.id) && ( + {contains.map((child) => processNode(child.document))} + )} +
+
+ ); + } + + function update(event) { + setFilter(event.target.value.toLowerCase()); + } + + return ( + <> +
+

Open CRE Explorer

+

+ A visual explorer of Open Common Requirement Enumerations (CREs). Originally created by:{' '} + + Zeljko Obrenovic + + . +

+ + + + + {filteredTree?.map((item) => { + return processNode(item); + })} + +
+ + ); +}; diff --git a/application/frontend/src/pages/Graph/Graph.tsx b/application/frontend/src/pages/Graph/Graph.tsx index 8c1e2e351..2a6e281e9 100644 --- a/application/frontend/src/pages/Graph/Graph.tsx +++ b/application/frontend/src/pages/Graph/Graph.tsx @@ -91,7 +91,7 @@ const onLoad = (reactFlowInstance) => { }; export const Graph = () => { - const { id } = useParams<{id : string}>(); + const { id } = useParams<{ id: string }>(); const { apiUrl } = useEnvironment(); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); diff --git a/application/frontend/src/pages/Search/Search.tsx b/application/frontend/src/pages/Search/Search.tsx index 0a7e33bf6..bc0c8be0f 100644 --- a/application/frontend/src/pages/Search/Search.tsx +++ b/application/frontend/src/pages/Search/Search.tsx @@ -207,8 +207,7 @@ export const SearchPage = () => { Use the{' '} {/* Scroll to hero on purpose, the searchbar is covered by the header so we need to scroll to the hero section first */} - search bar - + search bar ,{' '} @@ -264,21 +263,23 @@ export const SearchPage = () => {

OPENCRE CHAT

- Use - - OpenCRE Chat + Use{' '} + + OpenCRE Chat - to ask any security question. In collaboration with{' '} - Google, we injected all the standards in OpenCRE into an AI model to create - the most comprehensive security chatbot. This ensures you get a more{' '} - reliable answer, and also a reference to a reputable source. + to ask any security question. In collaboration with Google, we injected all + the standards in OpenCRE into an AI model to create the most comprehensive security chatbot. + This ensures you get a more reliable answer, and also a reference to a{' '} + reputable source.

-

MAP ANALYSIS

+

+ MAP ANALYSIS +

Utilize Map Analysis as a tool to explore and understand the connections between two standards. diff --git a/application/frontend/src/pages/Search/components/ui/button.tsx b/application/frontend/src/pages/Search/components/ui/button.tsx index b3cc1a1ac..fd8b8acda 100644 --- a/application/frontend/src/pages/Search/components/ui/button.tsx +++ b/application/frontend/src/pages/Search/components/ui/button.tsx @@ -1,32 +1,32 @@ -import './button.scss'; // Import the new SCSS file - -import React from 'react'; - -// Define the types for button props -type ButtonVariant = 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'; -type ButtonSize = 'default' | 'sm' | 'lg' | 'icon'; - -export interface ButtonProps extends React.ButtonHTMLAttributes { - variant?: ButtonVariant; - size?: ButtonSize; -} - -const Button = React.forwardRef( - ({ className, variant = 'default', size = 'default', ...props }, ref) => { - // Build the className string based on props - const classNames = [ - 'btn', - `btn--${variant}`, - `btn--${size}`, - className, // Allow passing additional custom classes - ] - .filter(Boolean) - .join(' '); - - return - -

- ) : ( - '' - )} - - -
- {/* Left these divs so that we can add auth functionality directly here. */} - {/*
- - Log In - - - - Sign Up - -
*/} - - -
- - - - -
- -
-
- -
- {showFilter && currentUrlParams.has('showButtons') ? ( -
- - -
- ) : ( - '' - )} - - - -
- {/*
- - Log In - - - - Sign Up - -
*/} -
-
- - ); -}; +import './header.scss'; + +import { Menu, Search } from 'lucide-react'; +import React, { useState } from 'react'; +import { Link, useHistory } from 'react-router-dom'; +import { Button } from 'semantic-ui-react'; + +import { ClearFilterButton } from '../../components/FilterButton/FilterButton'; +import { useLocationFromOutsideRoute } from '../../hooks/useLocationFromOutsideRoute'; +import { SearchBar } from '../../pages/Search/components/SearchBar'; + +export const Header = () => { + let currentUrlParams = new URLSearchParams(window.location.search); + const history = useHistory(); + const HandleDoFilter = () => { + currentUrlParams.set('applyFilters', 'true'); + history.push(window.location.pathname + '?' + currentUrlParams.toString()); + }; + const { showFilter } = useLocationFromOutsideRoute(); + + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); + + const closeMobileMenu = () => { + setIsMobileMenuOpen(false); + }; + + return ( + <> + + +
+ +
+
+ +
+ {showFilter && currentUrlParams.has('showButtons') ? ( +
+ + +
+ ) : ( + '' + )} + + + +
+ {/*
+ + Log In + + + + Sign Up + +
*/} +
+
+ + ); +}; diff --git a/application/frontend/src/utils/document.ts b/application/frontend/src/utils/document.ts index 9b353f081..1fe3a8e02 100644 --- a/application/frontend/src/utils/document.ts +++ b/application/frontend/src/utils/document.ts @@ -51,12 +51,15 @@ export const groupBy = (list: T[], getKey: (item: T) => export const getInternalUrl = (doc: Document): string => { if (doc.doctype.toLowerCase() != 'cre') { - var standardAPIPath = `/node/${doc.doctype.toLowerCase()}/${doc.name}/`; + var standardAPIPath = `/node/${doc.doctype.toLowerCase()}/${doc.name}`; if (doc) { if (doc.section) { - standardAPIPath += `section/${encodeURIComponent(doc.section)}`; + standardAPIPath += `/section/${encodeURIComponent(doc.section)}`; } else if (doc.sectionID) { - standardAPIPath += `sectionid/${encodeURIComponent(doc.sectionID)}`; + standardAPIPath += `/sectionid/${encodeURIComponent(doc.sectionID)}`; + } + if (doc.subsection) { + standardAPIPath += `/subsection/${encodeURIComponent(doc.subsection)}`; } if (doc.subsection) { standardAPIPath += `/subsection/${encodeURIComponent(doc.subsection)}`; @@ -72,9 +75,12 @@ export const getApiEndpoint = (doc: Document, apiUrl: string): string => { var standardAPIPath = `${apiUrl}/${doc.doctype.toLowerCase()}/${doc.name}`; if (doc) { if (doc.section) { - standardAPIPath += `section/${encodeURIComponent(doc.section)}`; + standardAPIPath += `/section/${encodeURIComponent(doc.section)}`; } else if (doc.sectionID) { - standardAPIPath += `sectionid/${encodeURIComponent(doc.sectionID)}`; + standardAPIPath += `/sectionid/${encodeURIComponent(doc.sectionID)}`; + } + if (doc.subsection) { + standardAPIPath += `/subsection/${encodeURIComponent(doc.subsection)}`; } return standardAPIPath; } diff --git a/application/web/web_main.py b/application/web/web_main.py index bb60695d9..5a8d42b41 100644 --- a/application/web/web_main.py +++ b/application/web/web_main.py @@ -144,13 +144,28 @@ def find_cre(creid: str = None, crename: str = None) -> Any: # refer @app.route("/rest/v1//", methods=["GET"]) @app.route("/rest/v1/standard/", methods=["GET"]) -def find_node_by_name(name: str, ntype: str = defs.Credoctypes.Standard.value) -> Any: +@app.route("/rest/v1///sectionid/", methods=["GET"]) +@app.route("/rest/v1///section/
", methods=["GET"]) +@app.route( + "/rest/v1///section/
/subsection/", methods=["GET"] +) +@app.route( + "/rest/v1///sectionid//subsection/", + methods=["GET"], +) +def find_node_by_name( + name: str, + ntype: str = defs.Credoctypes.Standard.value, + section: str = "", + subsection: str = "", + sectionID: str = "", +) -> Any: if posthog: - posthog.capture(f"find_node_by_name", f"hame:{name};nodeType{ntype}") + posthog.capture(f"find_node_by_name", f"name:{name};nodeType{ntype}") database = db.Node_collection() - opt_section = request.args.get("section") - opt_sectionID = request.args.get("sectionID") + opt_section = section or request.args.get("section") + opt_sectionID = sectionID or request.args.get("sectionID") # opt_osib = request.args.get("osib") opt_version = request.args.get("version") opt_format = request.args.get("format") @@ -158,7 +173,7 @@ def find_node_by_name(name: str, ntype: str = defs.Credoctypes.Standard.value) - opt_section = urllib.parse.unquote(opt_section) if opt_sectionID: opt_sectionID = urllib.parse.unquote(opt_sectionID) - opt_subsection = request.args.get("subsection") + opt_subsection = subsection or request.args.get("subsection") opt_hyperlink = request.args.get("hyperlink") # match ntype to the credoctypes case-insensitive @@ -218,7 +233,6 @@ def find_node_by_name(name: str, ntype: str = defs.Credoctypes.Standard.value) - res = [node.todict() for node in nodes] result["standards"] = res - return jsonify(result) else: abort(404, "Node does not exist")