Skip to content

Commit d8f7acf

Browse files
Anushree BondiaAnushree Bondia
authored andcommitted
feat:added new tab called all obligation
1 parent f489072 commit d8f7acf

3 files changed

Lines changed: 259 additions & 2 deletions

File tree

messages/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -944,6 +944,7 @@
944944
"Obligation updated successfully": "Obligation updated successfully",
945945
"Obligations": "Obligations",
946946
"Obligations View": "Obligations View",
947+
"All Obligations": "All Obligations",
947948
"On Hold": "On Hold",
948949
"Only Approved": "Only Approved",
949950
"Open": "Open",
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
// Copyright (C) Siemens AG, 2025. Part of the SW360 Frontend Project.
2+
3+
// This program and the accompanying materials are made
4+
// available under the terms of the Eclipse Public License 2.0
5+
// which is available at https://www.eclipse.org/legal/epl-2.0/
6+
7+
// SPDX-License-Identifier: EPL-2.0
8+
// License-Filename: LICENSE
9+
10+
'use client'
11+
12+
import { ColumnDef, getCoreRowModel, getPaginationRowModel, useReactTable } from '@tanstack/react-table'
13+
import { StatusCodes } from 'http-status-codes'
14+
import { signOut, useSession } from 'next-auth/react'
15+
import { useTranslations } from 'next-intl'
16+
import { type JSX, useEffect, useMemo, useState } from 'react'
17+
import { Spinner } from 'react-bootstrap'
18+
import { ClientSidePageSizeSelector, ClientSideTableFooter, SW360Table } from '@/components/sw360'
19+
import {
20+
Embedded,
21+
ErrorDetails,
22+
ObligationData,
23+
ObligationResponse,
24+
Project,
25+
} from '@/object-types'
26+
import MessageService from '@/services/message.service'
27+
import { ApiUtils, CommonUtils } from '@/utils'
28+
29+
interface AggregatedObligation extends ObligationData {
30+
projectName: string
31+
projectVersion?: string
32+
projectId: string
33+
obligationTitle: string
34+
}
35+
36+
type LinkedProjects = Embedded<Project, 'sw360:projects'>
37+
38+
interface Props {
39+
projectId: string
40+
}
41+
42+
const Capitalize = (text: string) =>
43+
text.split('_')
44+
.map((c) => c.charAt(0).toUpperCase() + c.substring(1).toLowerCase())
45+
.join(' ')
46+
47+
export default function AllObligationsView({ projectId }: Props): JSX.Element {
48+
const t = useTranslations('default')
49+
const session = useSession()
50+
const [showProcessing, setShowProcessing] = useState(false)
51+
const [aggregatedObligations, setAggregatedObligations] = useState<AggregatedObligation[]>([])
52+
53+
useEffect(() => {
54+
if (session.status === 'unauthenticated') {
55+
void signOut()
56+
}
57+
}, [session])
58+
59+
const columns = useMemo<ColumnDef<AggregatedObligation>[]>(
60+
() => [
61+
{
62+
id: 'project',
63+
header: t('Project'),
64+
cell: ({ row }) => (
65+
<span>
66+
{row.original.projectName}{' '}
67+
{row.original.projectVersion ? `(${row.original.projectVersion})` : ''}
68+
</span>
69+
),
70+
meta: { width: '20%' },
71+
},
72+
{
73+
id: 'title',
74+
header: t('Obligation'),
75+
accessorKey: 'obligationTitle',
76+
meta: { width: '25%' },
77+
},
78+
{
79+
id: 'status',
80+
header: t('Status'),
81+
cell: ({ row }) => <>{Capitalize(row.original.status ?? '')}</>,
82+
meta: { width: '15%' },
83+
},
84+
{
85+
id: 'type',
86+
header: t('Type'),
87+
cell: ({ row }) => <>{Capitalize(row.original.obligationType ?? '')}</>,
88+
meta: { width: '10%' },
89+
},
90+
{
91+
id: 'level',
92+
header: t('Level'),
93+
cell: ({ row }) => <>{Capitalize(row.original.obligationLevel ?? '')}</>,
94+
meta: { width: '10%' },
95+
},
96+
{
97+
id: 'comment',
98+
header: t('Comment'),
99+
accessorKey: 'comment',
100+
meta: { width: '30%' },
101+
},
102+
],
103+
[t]
104+
)
105+
106+
const table = useReactTable({
107+
data: aggregatedObligations,
108+
columns,
109+
getCoreRowModel: getCoreRowModel(),
110+
getPaginationRowModel: getPaginationRowModel(),
111+
meta: {
112+
rowHeightConstant: true,
113+
},
114+
})
115+
116+
useEffect(() => {
117+
if (session.status !== 'authenticated') return
118+
const controller = new AbortController()
119+
const signal = controller.signal
120+
121+
void (async () => {
122+
setShowProcessing(true)
123+
try {
124+
// 1. Fetch current project to get its name/version
125+
const rootProjectResponse = await ApiUtils.GET(`projects/${projectId}`, session.data.user.access_token, signal)
126+
if (rootProjectResponse.status !== StatusCodes.OK) throw new Error('Failed to fetch root project')
127+
const rootProject = (await rootProjectResponse.json()) as Project
128+
129+
// 2. Fetch all linked projects (transitive)
130+
const linkedProjectsResponse = await ApiUtils.GET(
131+
`projects/${projectId}/linkedProjects?transitive=true`,
132+
session.data.user.access_token,
133+
signal
134+
)
135+
if (linkedProjectsResponse.status !== StatusCodes.OK) throw new Error('Failed to fetch linked projects')
136+
const linkedProjectsData = (await linkedProjectsResponse.json()) as LinkedProjects
137+
const allProjects = [rootProject, ...(linkedProjectsData._embedded?.['sw360:projects'] ?? [])]
138+
139+
// 3. Fetch obligations for each project
140+
const allAggregated: AggregatedObligation[] = []
141+
142+
// Map to store project obligations for "parent check" if needed
143+
const projectObligationsMap: Record<string, Record<string, ObligationData>> = {}
144+
145+
for (const p of allProjects) {
146+
const pId = p.id || p._links?.self.href.split('/').at(-1) || ''
147+
if (!pId) continue
148+
149+
// Fetch different types of obligations
150+
const endpoints = [
151+
`projects/${pId}/licenseObligations`,
152+
`projects/${pId}/obligation?obligationLevel=project`,
153+
`projects/${pId}/obligation?obligationLevel=component`,
154+
`projects/${pId}/obligation?obligationLevel=organization`
155+
]
156+
157+
projectObligationsMap[pId] = {}
158+
159+
// Fetch all endpoints for this project
160+
const responses = await Promise.all(
161+
endpoints.map(ep => ApiUtils.GET(ep, session.data.user.access_token, signal))
162+
)
163+
164+
for (const res of responses) {
165+
try {
166+
if (res.status === StatusCodes.OK) {
167+
const data = (await res.json()) as ObligationResponse
168+
if (data.obligations) {
169+
Object.entries(data.obligations).forEach(([title, detail]) => {
170+
let extractedLevel = 'License'
171+
if (detail.obligationLevel) {
172+
extractedLevel = detail.obligationLevel
173+
} else if (res.url.includes('obligationLevel=')) {
174+
const match = res.url.match(/obligationLevel=([^&]+)/)
175+
if (match) extractedLevel = match[1]
176+
}
177+
projectObligationsMap[pId][title] = {
178+
...detail,
179+
obligationLevel: extractedLevel
180+
}
181+
})
182+
}
183+
}
184+
} catch (e) {
185+
console.error(`Failed to parse response for project ${pId}`, e)
186+
}
187+
}
188+
}
189+
190+
// 4. Filter and build final list
191+
const fulfilledStatuses = ['ACKNOWLEDGED_OR_FULFILLED', 'FULFILLED_AND_PARENT_MUST_ALSO_FULFILL']
192+
const subProjects = linkedProjectsData._embedded?.['sw360:projects'] ?? []
193+
194+
for (const p of subProjects) {
195+
const pId = p.id || p._links?.self.href.split('/').at(-1) || ''
196+
const obs = projectObligationsMap[pId] || {}
197+
198+
Object.entries(obs).forEach(([title, detail]) => {
199+
if (detail.status && fulfilledStatuses.includes(detail.status)) {
200+
let parentFulfillmentOk = true;
201+
if (detail.status === 'FULFILLED_AND_PARENT_MUST_ALSO_FULFILL') {
202+
const rootObs = projectObligationsMap[projectId] || {}
203+
const rootOb = rootObs[title]
204+
if (!rootOb || !fulfilledStatuses.includes(rootOb.status || '')) {
205+
parentFulfillmentOk = false
206+
}
207+
}
208+
209+
if (parentFulfillmentOk) {
210+
allAggregated.push({
211+
...detail,
212+
projectName: p.name,
213+
projectVersion: p.version,
214+
projectId: pId,
215+
obligationTitle: title
216+
})
217+
}
218+
}
219+
})
220+
}
221+
222+
setAggregatedObligations(allAggregated)
223+
} catch (error) {
224+
if (error instanceof DOMException && error.name === 'AbortError') return
225+
const message = error instanceof Error ? error.message : String(error)
226+
MessageService.error(message)
227+
} finally {
228+
setShowProcessing(false)
229+
}
230+
})()
231+
232+
return () => controller.abort()
233+
}, [projectId, session])
234+
235+
return (
236+
<div className='mb-3'>
237+
{showProcessing ? (
238+
<div className='col-12 mt-1 text-center'>
239+
<Spinner className='spinner' />
240+
</div>
241+
) : (
242+
<>
243+
<ClientSidePageSizeSelector table={table} />
244+
<SW360Table table={table} showProcessing={showProcessing} />
245+
<ClientSideTableFooter table={table} />
246+
</>
247+
)}
248+
</div>
249+
)
250+
}

src/app/[locale]/projects/components/Obligations/Obligations.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { Dispatch, type JSX, SetStateAction, useEffect, useState } from 'react'
1616
import { Dropdown, Nav, Tab } from 'react-bootstrap'
1717
import { AccessControl } from '@/components/AccessControl/AccessControl'
1818
import { ActionType, ObligationEntry, UserGroupType } from '@/object-types'
19+
import AllObligationsView from './AllObligationsView'
1920
import ObligationView from './ObligationsView/ObligationsView'
2021
import ReleaseView from './ReleaseView'
2122

@@ -69,6 +70,11 @@ function Obligations({ projectId, actionType, payload, setPayload }: Props): JSX
6970
<span className='fw-medium'>{t('Release View')}</span>
7071
</Nav.Link>
7172
</Nav.Item>
73+
<Nav.Item>
74+
<Nav.Link eventKey='all-obligations'>
75+
<span className='fw-medium'>{t('All Obligations')}</span>
76+
</Nav.Link>
77+
</Nav.Item>
7278
</Nav>
7379
</div>
7480
{actionType === ActionType.DETAIL && (
@@ -94,8 +100,8 @@ function Obligations({ projectId, actionType, payload, setPayload }: Props): JSX
94100
setPayload={setPayload}
95101
/>
96102
</Tab.Pane>
97-
<Tab.Pane eventKey='release-view'>
98-
<ReleaseView projectId={projectId} />
103+
<Tab.Pane eventKey='all-obligations'>
104+
<AllObligationsView projectId={projectId} />
99105
</Tab.Pane>
100106
</Tab.Content>
101107
</Tab.Container>

0 commit comments

Comments
 (0)