diff --git a/.changeset/olive-deers-smash.md b/.changeset/olive-deers-smash.md new file mode 100644 index 000000000..2bf34a1aa --- /dev/null +++ b/.changeset/olive-deers-smash.md @@ -0,0 +1,6 @@ +--- +'@roadiehq/backstage-plugin-wiz': patch +'@roadiehq/plugin-wiz-backend': patch +--- + +Added WIZ logo to components, improved error response diff --git a/plugins/backend/wiz-backend/README.md b/plugins/backend/wiz-backend/README.md index 6ae115206..66f3222f0 100644 --- a/plugins/backend/wiz-backend/README.md +++ b/plugins/backend/wiz-backend/README.md @@ -1,6 +1,6 @@ # Roadie WIZ Backend plugin for Backstage -This plugin is the backend for WIZ Backstage plugin. You can see the corresponding frontend plugin in [here](/plugins/frontend/backstage-plugin-wiz/README.md). +This plugin is the backend for WIZ Backstage plugin. You can see the corresponding frontend plugin in [here](../../frontend/backstage-plugin-wiz/README.md). This plugin provides functionality to retrieve corresponding access token, needed for API calls and retriving data, based on clientId, clientSecret and tokenURL. diff --git a/plugins/backend/wiz-backend/src/service/WizClient.ts b/plugins/backend/wiz-backend/src/service/WizClient.ts index 5978565d9..98eda52d6 100644 --- a/plugins/backend/wiz-backend/src/service/WizClient.ts +++ b/plugins/backend/wiz-backend/src/service/WizClient.ts @@ -48,7 +48,7 @@ export class WizClient { }); if (!response.ok) { - throw new Error(`Failed to fetch access token: ${response.statusText}`); + throw new Error(`${response.status}:${response.statusText}`); } const data = await response.json(); @@ -97,9 +97,10 @@ export class WizClient { } async getIssuesForProject(projectId: string) { - await this.ensureValidToken(); + try { + await this.ensureValidToken(); - const query = ` + const query = ` query IssuesTable($filterBy: IssueFilters, $first: Int, $after: String, $orderBy: IssueOrder) { issues: issuesV2(filterBy: $filterBy, first: $first, after: $after, orderBy: $orderBy) { nodes { @@ -110,7 +111,6 @@ export class WizClient { id name controlDescription: description - resolutionRecommendation securitySubCategories { title category { @@ -127,15 +127,6 @@ export class WizClient { updatedAt type resolvedAt - projects { - id - name - slug - businessUnit - riskProfile { - businessImpact - } - } status severity entitySnapshot { @@ -144,12 +135,6 @@ export class WizClient { name status createdAt - externalId - } - serviceTickets { - externalId - name - url } } pageInfo { @@ -160,32 +145,42 @@ export class WizClient { } `; - const options = { - method: 'POST', - headers: { - Authorization: `Bearer ${this.accessToken}`, - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - query, - variables: { - first: 100, - filterBy: { - project: [projectId], - }, - orderBy: { direction: 'DESC', field: 'SEVERITY' }, + const options = { + method: 'POST', + headers: { + Authorization: `Bearer ${this.accessToken}`, + Accept: 'application/json', + 'Content-Type': 'application/json', }, - }), - }; + body: JSON.stringify({ + query, + variables: { + first: 100, + filterBy: { + project: [projectId], + }, + orderBy: { direction: 'DESC', field: 'SEVERITY' }, + }, + }), + }; - const response = await fetch(this.wizAPIUrl, options); + const response = await fetch(this.wizAPIUrl, options); - if (!response.ok) { + if (!response.ok) { + const errorMessage = `${response.status} ${response.statusText}`; + const errorBody = await response.json().catch(() => null); + + throw new Error( + errorBody?.errors[0].message + ? `${errorBody.errors[0].message}` + : errorMessage, + ); + } + return response.json(); + } catch (error: any) { throw new Error( - `Request to retrieve wiz issues failed: ${response.statusText}`, + error.message || 'An error occurred while fetching project issues.', ); } - return response.json(); } } diff --git a/plugins/backend/wiz-backend/src/service/router.ts b/plugins/backend/wiz-backend/src/service/router.ts index 8184b9a6e..222d053e3 100644 --- a/plugins/backend/wiz-backend/src/service/router.ts +++ b/plugins/backend/wiz-backend/src/service/router.ts @@ -41,9 +41,21 @@ export async function createRouter( const data = await wizAuthClient.getIssuesForProject( req.params.projectId, ); - return res.send(data); + if (!data || data.errors) { + return res.status(200).send({ + error: data.errors[0].message, + }); + } + return res.status(200).json(data); } catch (error: any) { - return res.status(500).send({ error: error.message }); + if (error.message.includes('401')) { + return res.status(401).send({ + error: error.message, + }); + } + return res.status(500).send({ + error: 'Failed to fetch issues for project', + }); } }); diff --git a/plugins/frontend/backstage-plugin-wiz/README.md b/plugins/frontend/backstage-plugin-wiz/README.md index 8d9ac7547..7574662ca 100644 --- a/plugins/frontend/backstage-plugin-wiz/README.md +++ b/plugins/frontend/backstage-plugin-wiz/README.md @@ -1,6 +1,6 @@ # Wiz Plugin for Backstage -This plugin is the frontend for WIZ Backend Backstage plugin. You can see the corresponding backend plugin in [here](/plugins/backend/wiz-backend/README.md). +This plugin is the frontend for WIZ Backend Backstage plugin. You can see the corresponding backend plugin in [here](../../backend/wiz-backend/README.md). ![a Wiz plugin for Backstage](./docs/wiz-issues.png). ![a Wiz issues expanded](./docs/wiz-expanded-issues.png) @@ -28,7 +28,7 @@ Severity chart Component: ## Getting started -Make sure you have installed [WIZ backend plugin](/plugins/backend/wiz-backend/README.md). This will generate access token needed for retriving and displaying issues in components. +Make sure you have installed [WIZ backend plugin](../../backend/wiz-backend/README.md). This will generate access token needed for retriving and displaying issues in components. ### Add plugin component to your Backstage instance: diff --git a/plugins/frontend/backstage-plugin-wiz/docs/issues-chart.png b/plugins/frontend/backstage-plugin-wiz/docs/issues-chart.png index 72fb09001..8d4f505ec 100644 Binary files a/plugins/frontend/backstage-plugin-wiz/docs/issues-chart.png and b/plugins/frontend/backstage-plugin-wiz/docs/issues-chart.png differ diff --git a/plugins/frontend/backstage-plugin-wiz/docs/issues-widget.png b/plugins/frontend/backstage-plugin-wiz/docs/issues-widget.png index 648282943..76af6ebd4 100644 Binary files a/plugins/frontend/backstage-plugin-wiz/docs/issues-widget.png and b/plugins/frontend/backstage-plugin-wiz/docs/issues-widget.png differ diff --git a/plugins/frontend/backstage-plugin-wiz/docs/severity-graph.png b/plugins/frontend/backstage-plugin-wiz/docs/severity-graph.png index cdf18ef5f..7647b301b 100644 Binary files a/plugins/frontend/backstage-plugin-wiz/docs/severity-graph.png and b/plugins/frontend/backstage-plugin-wiz/docs/severity-graph.png differ diff --git a/plugins/frontend/backstage-plugin-wiz/docs/wiz-expanded-issues.png b/plugins/frontend/backstage-plugin-wiz/docs/wiz-expanded-issues.png index ae0c25492..ac1549a37 100644 Binary files a/plugins/frontend/backstage-plugin-wiz/docs/wiz-expanded-issues.png and b/plugins/frontend/backstage-plugin-wiz/docs/wiz-expanded-issues.png differ diff --git a/plugins/frontend/backstage-plugin-wiz/docs/wiz-issues.png b/plugins/frontend/backstage-plugin-wiz/docs/wiz-issues.png index 815f0957e..cc2aa1d37 100644 Binary files a/plugins/frontend/backstage-plugin-wiz/docs/wiz-issues.png and b/plugins/frontend/backstage-plugin-wiz/docs/wiz-issues.png differ diff --git a/plugins/frontend/backstage-plugin-wiz/src/api/WizClient.ts b/plugins/frontend/backstage-plugin-wiz/src/api/WizClient.ts index 3e6c60283..b29e8d0bb 100644 --- a/plugins/frontend/backstage-plugin-wiz/src/api/WizClient.ts +++ b/plugins/frontend/backstage-plugin-wiz/src/api/WizClient.ts @@ -32,16 +32,25 @@ export class WizClient implements WizAPI { async fetchIssuesForProject(projectId: string): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('wiz-backend'); - const response = await this.fetchApi.fetch( - `${baseUrl}/wiz-issues/${projectId}`, - ); - const payload = await response.json(); - - if (!response.ok) { - throw new Error( - 'There was an error retrieving issues for specific project', + + try { + const response = await this.fetchApi.fetch( + `${baseUrl}/wiz-issues/${projectId}`, ); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(`${errorData.error}`); + } + + const payload = await response.json(); + + if (payload.error) { + throw new Error(`${payload.error}`); + } + return payload.data.issues.nodes; + } catch (error: any) { + throw new Error(`${error.message}`); } - return payload.data.issues.nodes; } } diff --git a/plugins/frontend/backstage-plugin-wiz/src/assets/wiz-logo.png b/plugins/frontend/backstage-plugin-wiz/src/assets/wiz-logo.png new file mode 100644 index 000000000..8b433486d Binary files /dev/null and b/plugins/frontend/backstage-plugin-wiz/src/assets/wiz-logo.png differ diff --git a/plugins/frontend/backstage-plugin-wiz/src/components/EntityIssuesChart/IssuesChart.tsx b/plugins/frontend/backstage-plugin-wiz/src/components/EntityIssuesChart/IssuesChart.tsx index 660bdcf1a..4e114adaa 100644 --- a/plugins/frontend/backstage-plugin-wiz/src/components/EntityIssuesChart/IssuesChart.tsx +++ b/plugins/frontend/backstage-plugin-wiz/src/components/EntityIssuesChart/IssuesChart.tsx @@ -26,13 +26,26 @@ import { } from '@backstage/core-components'; import { LineChart } from './LineChart'; import { WIZ_PROJECT_ANNOTATION } from '../constants'; +import { useStyles } from '../../style'; +import { Typography } from '@material-ui/core'; export const IssuesChart = () => { const api = useApi(wizApiRef); const { entity } = useEntity(); + const classes = useStyles(); const wizAnnotation = entity?.metadata.annotations?.[WIZ_PROJECT_ANNOTATION] ?? ''; + const WizIcon = () => { + return ( + WIZ Logo + ); + }; + const { value, loading, error } = useAsync(async () => { return await api.fetchIssuesForProject(wizAnnotation); }, []); @@ -44,8 +57,20 @@ export const IssuesChart = () => { } return ( - - + , + classes: { + root: classes.card, + }, + }} + > + {value.length > 0 ? ( + + ) : ( + There are no issues for this project + )} ); }; diff --git a/plugins/frontend/backstage-plugin-wiz/src/components/EntitySeverityChart/SeverityChart.tsx b/plugins/frontend/backstage-plugin-wiz/src/components/EntitySeverityChart/SeverityChart.tsx index 7819d2f64..0fc5db865 100644 --- a/plugins/frontend/backstage-plugin-wiz/src/components/EntitySeverityChart/SeverityChart.tsx +++ b/plugins/frontend/backstage-plugin-wiz/src/components/EntitySeverityChart/SeverityChart.tsx @@ -26,13 +26,26 @@ import { } from '@backstage/core-components'; import { BarChart } from '.'; import { WIZ_PROJECT_ANNOTATION } from '../constants'; +import { useStyles } from '../../style'; +import { Typography } from '@material-ui/core'; export const SeverityChart = () => { const api = useApi(wizApiRef); + const classes = useStyles(); const { entity } = useEntity(); const wizAnnotation = entity?.metadata.annotations?.[WIZ_PROJECT_ANNOTATION] ?? ''; + const WizIcon = () => { + return ( + WIZ Logo + ); + }; + const { value, loading, error } = useAsync(async () => { return await api.fetchIssuesForProject(wizAnnotation); }, []); @@ -44,8 +57,20 @@ export const SeverityChart = () => { } return ( - - + , + classes: { + root: classes.card, + }, + }} + > + {value.length > 0 ? ( + + ) : ( + There are no issues for this project + )} ); }; diff --git a/plugins/frontend/backstage-plugin-wiz/src/components/Issues/Issues.tsx b/plugins/frontend/backstage-plugin-wiz/src/components/Issues/Issues.tsx index 4be809e78..c3c4b1906 100644 --- a/plugins/frontend/backstage-plugin-wiz/src/components/Issues/Issues.tsx +++ b/plugins/frontend/backstage-plugin-wiz/src/components/Issues/Issues.tsx @@ -44,6 +44,7 @@ import MaterialReactTable, { MRT_ColumnDef } from 'material-react-table'; import FilterAltOutlinedIcon from '@mui/icons-material/FilterAltOutlined'; import { createTheme, ThemeProvider, ThemeOptions } from '@mui/material/styles'; import { WIZ_PROJECT_ANNOTATION } from '../constants'; +import { useStyles } from '../../style'; const getCorrectChip = (theme: Theme, severity: string) => { switch (severity) { @@ -111,6 +112,7 @@ export const Issues = () => { const theme = useTheme(); const api = useApi(wizApiRef); const { entity } = useEntity(); + const classes = useStyles(); const wizAnnotation = entity?.metadata.annotations?.[WIZ_PROJECT_ANNOTATION] ?? ''; const { value, loading, error } = useAsync(async () => { @@ -240,49 +242,43 @@ export const Issues = () => { return ; } + const WizIcon = () => { + return ( + + WIZ Logo + + ); + }; + + const title = ( + + + Overview of WIZ issues + + ); + return ( - - - {Object.keys(groupedIssues).map(ruleId => ( - - - }> - - - - Issue type (Control) - - - {groupedIssues[ruleId][0]?.sourceRule?.name} - - - - + + {value.length > 0 ? ( + Object.keys(groupedIssues).map(ruleId => ( + + + }> + + { fontSize: 'smaller', }} > - Open + Issue type (Control) - { - groupedIssues[ruleId].filter( - issue => issue.status === 'OPEN', - ).length - } + {groupedIssues[ruleId][0]?.sourceRule?.name} - - - Resolved - - + - { - groupedIssues[ruleId].filter( - issue => issue.status === 'RESOLVED', - ).length - } - - - - + Open + + + { + groupedIssues[ruleId].filter( + issue => issue.status === 'OPEN', + ).length + } + + + - Total - - + Resolved + + + { + groupedIssues[ruleId].filter( + issue => issue.status === 'RESOLVED', + ).length + } + + + - {groupedIssues[ruleId].length} - + + Total + + + {groupedIssues[ruleId].length} + + - - + - - - - - - - ), - }, - }} - muiTableBodyCellProps={{ - sx: () => ({ - whiteSpace: 'nowrap', - }), - }} - defaultColumn={{ - minSize: 10, - }} - state={{ - isLoading: loading, - }} - /> - - - - - - ))} + + + + + + + ), + }, + }} + muiTableBodyCellProps={{ + sx: () => ({ + whiteSpace: 'nowrap', + }), + }} + defaultColumn={{ + minSize: 10, + }} + state={{ + isLoading: loading, + }} + /> + + + + + + )) + ) : ( + There are no issues for this project + )} ); diff --git a/plugins/frontend/backstage-plugin-wiz/src/components/IssuesWidget/IssuesWidget.tsx b/plugins/frontend/backstage-plugin-wiz/src/components/IssuesWidget/IssuesWidget.tsx index 6b10f5c01..9f10068ce 100644 --- a/plugins/frontend/backstage-plugin-wiz/src/components/IssuesWidget/IssuesWidget.tsx +++ b/plugins/frontend/backstage-plugin-wiz/src/components/IssuesWidget/IssuesWidget.tsx @@ -26,6 +26,7 @@ import { } from '@backstage/core-components'; import { useEntity } from '@backstage/plugin-catalog-react'; import { WIZ_PROJECT_ANNOTATION } from '../constants'; +import { useStyles } from '../../style'; const SeverityIndicator = ({ theme, @@ -69,6 +70,7 @@ export const IssuesWidget = () => { const api = useApi(wizApiRef); const { entity } = useEntity(); const configApi = useApi(configApiRef); + const classes = useStyles(); const dashboardLink = configApi.getOptionalString('wiz.dashboardLink') ?? ''; @@ -86,6 +88,16 @@ export const IssuesWidget = () => { (item: { status: string }) => item.status === 'RESOLVED', ); + const WizIcon = () => { + return ( + WIZ Logo + ); + }; + if (loading) { return ; } else if (error) { @@ -95,6 +107,12 @@ export const IssuesWidget = () => { return ( , + classes: { + root: classes.card, + }, + }} deepLink={{ link: `https://${dashboardLink}`, title: 'Go to WIZ', @@ -104,117 +122,122 @@ export const IssuesWidget = () => { }, }} > - - - Open + {' '} + {value.length > 0 ? ( + + + Open - - it.severity === 'CRITICAL', - ).length - } - severity="high" - content="Critical severity" - /> - it.severity === 'HIGH', - ).length - } - severity="high" - content="High severity" - /> - it.severity === 'MEDIUM', - ).length - } - severity="medium" - content="Medium severity" - /> - it.severity === 'LOW', - ).length - } - severity="low" - content="Low severity" - /> + + it.severity === 'CRITICAL', + ).length + } + severity="high" + content="Critical severity" + /> + it.severity === 'HIGH', + ).length + } + severity="high" + content="High severity" + /> + it.severity === 'MEDIUM', + ).length + } + severity="medium" + content="Medium severity" + /> + it.severity === 'LOW', + ).length + } + severity="low" + content="Low severity" + /> + - - - Resolved + + Resolved - - it.severity === 'CRITICAL', - ).length - } - severity="high" - content="Critical severity" - /> - it.severity === 'HIGH', - ).length - } - severity="high" - content="High severity" - /> - it.severity === 'MEDIUM', - ).length - } - severity="medium" - content="Medium severity" - /> - it.severity === 'LOW', - ).length - } - severity="low" - content="Low severity" - /> + + it.severity === 'CRITICAL', + ).length + } + severity="high" + content="Critical severity" + /> + it.severity === 'HIGH', + ).length + } + severity="high" + content="High severity" + /> + it.severity === 'MEDIUM', + ).length + } + severity="medium" + content="Medium severity" + /> + it.severity === 'LOW', + ).length + } + severity="low" + content="Low severity" + /> + - + ) : ( + There are no issues for this project + )} ); }; diff --git a/plugins/frontend/backstage-plugin-wiz/src/style.ts b/plugins/frontend/backstage-plugin-wiz/src/style.ts new file mode 100644 index 000000000..e43e79e3f --- /dev/null +++ b/plugins/frontend/backstage-plugin-wiz/src/style.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2024 Larder Software Limited + * + * 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 { makeStyles } from '@material-ui/core'; + +export const useStyles = makeStyles(() => ({ + commonLogo: { + height: 'auto', + }, + logo: { + extend: 'commonLogo', + width: '2rem', + }, + contentLogo: { + extend: 'commonLogo', + width: '3rem', + }, + card: { + display: 'flex', + }, +}));