diff --git a/messages/en.json b/messages/en.json index daf81691f..8c5da5ed3 100644 --- a/messages/en.json +++ b/messages/en.json @@ -134,6 +134,7 @@ "Cloud Backend": "Cloud Backend", "Cloud Service": "Cloud Service", "Code Snippet": "Code Snippet", + "Collapse all": "Collapse all", "Comment": "Comment", "Comments": "Comments", "Commercial Details": "Commercial Details", @@ -321,6 +322,7 @@ "External URL": "External URL", "External URLs": "External URLs", "External ids": "External ids", + "Expand Next Level": "Expand Next Level", "Field Name": "Field Name", "File name": "File name", "Finalized License Scan Report": "Finalized License Scan Report", @@ -419,6 +421,7 @@ "Link to Projects": "Link to Projects", "Linked Releases": "Linked Releases", "Linked Releases and Projects": "Linked Releases and Projects", + "List View": "List View", "Logout": "Sign Out", "MAINLINE": "Mainline", "MEDIUM": "MEDIUM", @@ -516,9 +519,11 @@ "Programming Languages": "Programming Languages", "Project": "Project", "Project Clearing State": "Project Clearing State", + "Project Mainline State": "Project Mainline State", "Project Manager": "Project Manager", "Project Name": "Project Name", "Project Owner": "Project Owner", + "Project Path": "Project Path", "Project Relationship": "Project Relationship", "Project Responsible": "Project Responsible", "Project Responsible (Email)": "Project Responsible (Email)", @@ -552,12 +557,15 @@ "Reference Doc Changes": "Reference Doc Changes", "References": "References", "Related": "Related", + "Relation": "Relation", "Release": "Release", "Release Aggregate Data": "Release Aggregate Data", + "Release Clearing State": "Release Clearing State", "Release Create failed": "Release Create failed", "Release Date of this Release": "Release Date of this Release", "Release Mainline State": "Release Mainline State", "Release Overview": "Release Overview", + "Release Path": "Release Path", "Release Repository": "Release Repository", "Release Summary": "Release Summary", "Release Vendor": "Release Vendor", @@ -700,6 +708,7 @@ "Token Name": "Token Name", "Total Number Of Files": "Total Number Of Files", "Total vulnerabilities": "Total vulnerabilities", + "Tree View": "Tree View", "Type": "Type", "UNDER_CLEARING": "Under Clearing", "UNKNOWN": "Unknown", diff --git a/src/app/[locale]/projects/detail/[id]/components/LicenseClearing.tsx b/src/app/[locale]/projects/detail/[id]/components/LicenseClearing.tsx new file mode 100644 index 000000000..996f23aff --- /dev/null +++ b/src/app/[locale]/projects/detail/[id]/components/LicenseClearing.tsx @@ -0,0 +1,57 @@ +// Copyright (C) Siemens AG, 2023. Part of the SW360 Frontend Project. + +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ + +// SPDX-License-Identifier: EPL-2.0 +// License-Filename: LICENSE + +'use client' + +import { useTranslations } from 'next-intl' +import { useState } from 'react' +import { Nav, Tab } from 'react-bootstrap' +import ListView from './ListView' + +export default function LicenseClearing({ + projectId, + projectName, + projectVersion, +}: { + projectId: string + projectName: string + projectVersion: string +}) { + const t = useTranslations('default') + const [key, setKey] = useState('tree-view') + + return ( + <> + setKey(k)}> +
+
+ +
+
+ + + + + + +
+ + ) +} diff --git a/src/app/[locale]/projects/detail/[id]/components/ListView.tsx b/src/app/[locale]/projects/detail/[id]/components/ListView.tsx new file mode 100644 index 000000000..dff6dd9ff --- /dev/null +++ b/src/app/[locale]/projects/detail/[id]/components/ListView.tsx @@ -0,0 +1,444 @@ +// Copyright (C) Siemens AG, 2023. Part of the SW360 Frontend Project. + +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ + +// SPDX-License-Identifier: EPL-2.0 +// License-Filename: LICENSE + +'use client' + +import { HttpStatus } from '@/object-types' +import { ApiUtils } from '@/utils' +import { signOut, useSession } from 'next-auth/react' +import { useTranslations } from 'next-intl' +import { Table, _ } from 'next-sw360' +import Link from 'next/link' +import { notFound } from 'next/navigation' +import { useEffect, useState } from 'react' +import { OverlayTrigger, Spinner, Tooltip } from 'react-bootstrap' +import { FaPencilAlt } from 'react-icons/fa' + +const Capitalize = (text: string) => + text.split('_').reduce((s, c) => s + ' ' + (c.charAt(0) + c.substring(1).toLocaleLowerCase()), '') + +interface ProjectState { + state: string + clearingState: string +} + +interface ReleaseState { + clearingState: string +} + +enum ElementType { + LINKED_PROJECT, + LINKED_RELEASE, +} + +interface ListViewData { + elementType: ElementType + elem: { + name: string + version: string + id: string + } + type: string + projectPath: string[] + releasePath: string[] + relation: string + mainLicenses: string[] + state: ProjectState | ReleaseState + releaseMainlineState: string + projectMainlineState: string + comment: string + actions: string +} + +const extractLinkedProjectsAndTheirLinkedReleases = ( + licenseClearingData: any, + linkedProjectsData: any, + finalData: ListViewData[], + path: string[] +) => { + if (!linkedProjectsData) return + for (const p of linkedProjectsData) { + path.push(`${p.name} (${p.version})`) + + finalData.push({ + elementType: ElementType.LINKED_PROJECT, + elem: { + name: p.name ?? '', + version: p.version ?? '', + id: p['_links']['self']['href'].substring(p['_links']['self']['href'].lastIndexOf('/') + 1), + }, + type: p.projectType ?? '', + projectPath: path.slice(), + releasePath: [], + relation: '', + mainLicenses: [], + state: { + clearingState: p.clearingState, + state: p.state, + }, + releaseMainlineState: '', + projectMainlineState: '', + comment: '', + actions: p['_links']['self']['href'].substring(p['_links']['self']['href'].lastIndexOf('/') + 1), + }) + + if (!licenseClearingData['linkedReleases']) { + continue + } + + for (const l of p['linkedReleases']) { + const id = l.release.substring(l.release.lastIndexOf('/') + 1) + const res = licenseClearingData['_embedded']['sw360:release'].filter( + (e: any) => + e['_links']['self']['href'].substring(e['_links']['self']['href'].lastIndexOf('/') + 1) === id + ) + finalData.push({ + elementType: ElementType.LINKED_RELEASE, + elem: { + name: res[0].name ?? '', + version: res[0].version ?? '', + id: id, + }, + type: res[0].componentType ?? '', + projectPath: path.slice(), + releasePath: [], + relation: l.relation ?? '', + mainLicenses: res[0].mainLicenseIds ?? '', + state: { + clearingState: res[0].clearingState ?? '', + }, + releaseMainlineState: l.mainlineState ?? '', + projectMainlineState: l.mainlineState ?? '', + comment: l.comment ?? '', + actions: id, + }) + } + extractLinkedProjectsAndTheirLinkedReleases( + licenseClearingData, + p?.['_embedded']?.['sw360:linkedProjects'], + finalData, + path + ) + path.pop() + } +} + +const extractLinkedReleases = ( + projectName: string, + projectVersion: string, + licenseClearingData: any, + finalData: ListViewData[], + path: string[] +) => { + if (!licenseClearingData && !licenseClearingData?.['linkedReleases']) return + path.push(`${projectName} (${projectVersion})`) + for (const l of licenseClearingData['linkedReleases']) { + const id = l.release.substring(l.release.lastIndexOf('/') + 1) + const res = licenseClearingData['_embedded']['sw360:release'].filter( + (e: any) => e['_links']['self']['href'].substring(e['_links']['self']['href'].lastIndexOf('/') + 1) === id + ) + finalData.push({ + elementType: ElementType.LINKED_RELEASE, + elem: { + name: res[0].name, + version: res[0].version, + id: id, + }, + type: res[0].componentType, + projectPath: path.slice(), + releasePath: [], + relation: l.relation, + mainLicenses: res[0].mainLicenseIds, + state: { + clearingState: res[0].clearingState, + }, + releaseMainlineState: l.mainlineState, + projectMainlineState: l.mainlineState, + comment: l.comment, + actions: id, + }) + } +} + +export default function ListView({ + projectId, + projectName, + projectVersion, +}: { + projectId: string + projectName: string + projectVersion: string +}) { + const t = useTranslations('default') + const [data, setData] = useState() + const { data: session, status } = useSession() + + const columns = [ + { + id: 'licenseClearing.name', + name: t('Name'), + width: '9%', + formatter: ({ + name, + version, + id, + type, + }: { + name: string + version: string + id: string + type: ElementType + }) => + _( + + {`${name} (${version})`} + + ), + sort: true, + }, + { + id: 'licenseClearing.type', + name: t('Type'), + width: '7%', + formatter: (type: string) => _(<>{Capitalize(type)}), + sort: true, + }, + { + id: 'licenseClearing.projectPath', + name: t('Project Path'), + width: '12%', + formatter: (path: string[]) => _(<>{path.join(' -> ')}), + sort: true, + }, + { + id: 'licenseClearing.releasePath', + name: t('Release Path'), + width: '9%', + formatter: (path: string[]) => _(<>{path.join(' -> ')}), + sort: true, + }, + { + id: 'licenseClearing.relation', + name: t('Relation'), + width: '9%', + formatter: (type: string) => _(<>{Capitalize(type)}), + sort: true, + }, + { + id: 'licenseClearing.mainLicenses', + name: t('Main Licenses'), + width: '10%', + formatter: (licenses: string[]) => + _( + <> + {licenses.map((e, i) => ( +
  • + + {e} + + {i === licenses.length - 1 ? '' : ','}{' '} +
  • + ))} + + ), + sort: true, + }, + { + id: 'licenseClearing.state', + name: t('State'), + width: '8%', + formatter: ({ state, elementType }: { state: ProjectState | ReleaseState; elementType: ElementType }) => { + if (elementType === ElementType.LINKED_PROJECT) { + return _( + <> +
    + {`${t('Project State')}: ${Capitalize( + (state as ProjectState).state + )}`} + } + > + {(state as ProjectState).state === 'ACTIVE' ? ( + {'PS'} + ) : ( + {'PS'} + )} + + {`${t('Project Clearing State')}: ${Capitalize( + (state as ProjectState).clearingState + )}`} + } + > + {(state as ProjectState).clearingState === 'OPEN' ? ( + {'CS'} + ) : (state as ProjectState).clearingState === 'IN_PROGRESS' ? ( + {'CS'} + ) : ( + {'CS'} + )} + +
    + + ) + } + return _( + <> +
    + {`${t('Release Clearing State')}: ${Capitalize( + (state as ReleaseState).clearingState + )}`} + } + > + {(state as ReleaseState).clearingState === 'NEW_CLEARING' ? ( + {'CS'} + ) : (state as ReleaseState).clearingState === 'REPORT_AVAILABLE' ? ( + {'CS'} + ) : ( + {'CS'} + )} + +
    + + ) + }, + sort: true, + }, + { + id: 'licenseClearing.releaseMainlineState', + name: t('Release Mainline State'), + width: '8%', + formatter: (type: string) => _(<>{Capitalize(type)}), + sort: true, + }, + { + id: 'licenseClearing.projectMainlineState', + name: t('Project Mainline State'), + width: '8%', + formatter: (type: string) => _(<>{Capitalize(type)}), + sort: true, + }, + { + id: 'licenseClearing.comment', + name: t('Comment'), + width: '8%', + sort: true, + }, + { + id: 'licenseClearing.actions', + name: t('Actions'), + sort: true, + width: '6%', + formatter: ({ id, type }: { id: string; type: ElementType }) => + _( + <> + {t('Edit')}}> + + + + + + ), + }, + ] + + useEffect(() => { + if (status !== 'authenticated') return + const controller = new AbortController() + const signal = controller.signal + + ;(async () => { + try { + const res_licenseClearing = await ApiUtils.GET( + `projects/${projectId}/licenseClearing?transitive=true`, + session.user.access_token, + signal + ) + + const res_linkedProjects = ApiUtils.GET( + `projects/${projectId}/linkedProjects?transitive=true`, + session.user.access_token, + signal + ) + + const responses = await Promise.all([res_licenseClearing, res_linkedProjects]) + if ( + responses[0].status === HttpStatus.UNAUTHORIZED || + responses[1].status === HttpStatus.UNAUTHORIZED + ) { + return signOut() + } else if (responses[0].status !== HttpStatus.OK || responses[1].status !== HttpStatus.OK) { + return notFound() + } + + const licenseClearingData = await responses[0].json() + const linkedProjectsData = await responses[1].json() + + const finalData: ListViewData[] = [] + const path: string[] = [] + extractLinkedReleases(projectName, projectVersion, licenseClearingData, finalData, path) + extractLinkedProjectsAndTheirLinkedReleases( + licenseClearingData, + linkedProjectsData?.['_embedded']?.['sw360:projects'], + finalData, + path + ) + + const d = finalData.map((e) => [ + { ...e.elem, type: e.elementType }, + e.type, + e.projectPath, + e.releasePath, + e.relation, + e.mainLicenses, + { state: e.state, elementType: e.elementType }, + e.releaseMainlineState, + e.projectMainlineState, + e.comment, + { id: e.elem.id, type: e.elementType }, + ]) + setData(d) + } catch (e) { + console.error(e) + } + })() + + return () => controller.abort() + }, [status]) + + return ( + <> + {data ? ( + + ) : ( +
    + +
    + )} + + ) +} diff --git a/src/app/[locale]/projects/detail/[id]/components/ProjectDetailTab.tsx b/src/app/[locale]/projects/detail/[id]/components/ProjectDetailTab.tsx index a5324ada7..a3297720a 100644 --- a/src/app/[locale]/projects/detail/[id]/components/ProjectDetailTab.tsx +++ b/src/app/[locale]/projects/detail/[id]/components/ProjectDetailTab.tsx @@ -9,18 +9,18 @@ 'use client' +import { AdministrationDataType, HttpStatus, SummaryDataType } from '@/object-types' +import { ApiUtils } from '@/utils' import { signOut, useSession } from 'next-auth/react' import { useTranslations } from 'next-intl' import { notFound } from 'next/navigation' import { useEffect, useState } from 'react' import { Button, Col, Dropdown, ListGroup, Row, Spinner, Tab } from 'react-bootstrap' - -import { AdministrationDataType, HttpStatus, SummaryDataType } from '@/object-types' -import { ApiUtils } from '@/utils' import LinkProjects from '../../../components/LinkProjects' import Administration from './Administration' import ChangeLog from './Changelog' import EccDetails from './Ecc' +import LicenseClearing from './LicenseClearing' import Summary from './Summary' export default function ViewProjects({ projectId }: { projectId: string }) { @@ -145,7 +145,15 @@ export default function ViewProjects({ projectId }: { projectId: string }) { )} - + + {summaryData && ( + + )} + diff --git a/src/styles/globals.css b/src/styles/globals.css index a969dd011..5b6b9597c 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -684,7 +684,8 @@ th { background-color: gray; } -.form-check-input[disabled] ~ .form-check-label, .form-check-input:disabled ~ .form-check-label { +.form-check-input[disabled] ~ .form-check-label, +.form-check-input:disabled ~ .form-check-label { cursor: not-allowed !important; } @@ -720,3 +721,18 @@ th { .toast-container { position: fixed !important; } + +.nav-pills .nav-link { + background-color: #f2f2f2 !important; + color: #f2a922 !important; +} + +.nav-pills .nav-link:hover { + background-color: #f2f2f2 !important; + color: #21719c !important; +} + +.nav-pills .nav-link.active { + background-color: #0c70f2 !important; + color: white !important; +} diff --git a/src/styles/gridjs/sw360.css b/src/styles/gridjs/sw360.css index bb6397b16..b2b18ba2c 100644 --- a/src/styles/gridjs/sw360.css +++ b/src/styles/gridjs/sw360.css @@ -232,6 +232,7 @@ th.gridjs-th { white-space: nowrap; outline: none; vertical-align: bottom; + white-space: break-spaces; } th.gridjs-th .gridjs-th-content { text-overflow: ellipsis; @@ -362,7 +363,6 @@ th.gridjs-th:last-child { background-color: #9bc2f7; } - .gridjs a:link { text-decoration: none; color: var(--sw360-primary-color); @@ -373,8 +373,7 @@ th.gridjs-th:last-child { color: var(--sw360-primary-color); } - .gridjs a:hover { text-decoration: underline; color: var(--sw360-primary-background-color); -} \ No newline at end of file +}