Skip to content

Commit

Permalink
feat: Implement UI page for requests
Browse files Browse the repository at this point in the history
  • Loading branch information
vibe13 committed Nov 27, 2024
1 parent 6cbf090 commit 93233c5
Show file tree
Hide file tree
Showing 7 changed files with 325 additions and 2 deletions.
31 changes: 30 additions & 1 deletion ui/src/app/api/DefaultSbomerApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
///

import axios, { Axios, AxiosError } from 'axios';
import { SbomerApi, SbomerGeneration, SbomerManifest, SbomerStats } from '../types';
import { SbomerApi, SbomerGeneration, SbomerManifest, SbomerStats, SbomerRequest } from '../types';

type Options = {
baseUrl: string;
Expand Down Expand Up @@ -190,4 +190,33 @@ export class DefaultSbomerApi implements SbomerApi {

return request;
}

async getRequestEvents(pagination: {
pageSize: number;
pageIndex: number;
}): Promise<{ data: SbomerRequest[]; total: number }> {
const response = await fetch(
`${this.baseUrl}/api/v1beta1/requests?pageSize=${pagination.pageSize}&pageIndex=${pagination.pageIndex}`,
);

if (response.status != 200) {
const body = await response.text();

throw new Error(
'Failed fetching request events from SBOMer, got: ' + response.status + " response: '" + body + "'",
);
}

const data = await response.json();

const requests: SbomerRequest[] = [];

if (data.content) {
data.content.forEach((request: any) => {
requests.push(new SbomerRequest(request));
});
}

return { data: requests, total: data.totalHits };
}
}
26 changes: 26 additions & 0 deletions ui/src/app/components/Pages/RequestEvents/RequestsEventsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { RequestEventTable } from '@app/components/RequestEventTable/RequestEventTable';
import { useDocumentTitle } from '@app/utils/useDocumentTitle';
import { Grid, GridItem, PageSection, Title } from '@patternfly/react-core';
import * as React from 'react';
import { AppLayout } from '../AppLayout/AppLayout';

export function RequestEventsPage() {
useDocumentTitle('SBOMer | Request Events');

return (
<AppLayout>
<PageSection hasBodyWrapper={false}>
<Grid hasGutter span={12}>
<GridItem span={12}>
<Title headingLevel="h1" size="4xl">
Request Events
</Title>
</GridItem>
<GridItem span={12}>
<RequestEventTable />
</GridItem>
</Grid>
</PageSection>
</AppLayout>
);
}
116 changes: 116 additions & 0 deletions ui/src/app/components/RequestEventTable/RequestEventTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { statusToColor, statusToDescription, timestampToHumanReadable } from '@app/utils/Utils';
import {
Label,
Pagination,
PaginationVariant,
Skeleton,
Timestamp,
TimestampTooltipVariant,
Tooltip,
ExpandableSection,
CodeBlock,
CodeBlockCode,
} from '@patternfly/react-core';
import { Caption, Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table';
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { useSearchParam } from 'react-use';
import { ErrorSection } from '../Sections/ErrorSection/ErrorSection';
import { useRequestEvents } from './useRequestEvents';

const columnNames = {
id: 'ID',
receivalTime: 'Received',
eventType: 'Type',
requestConfig: 'Request Config',
event: 'Event',
};

export const RequestEventTable = () => {
const navigate = useNavigate();
const paramPage = useSearchParam('page') || 1;
const paramPageSize = useSearchParam('pageSize') || 10;

const [{ pageIndex, pageSize, value, loading, total, error }, { setPageIndex, setPageSize }] = useRequestEvents(
+paramPage - 1,
+paramPageSize,
);

const onSetPage = (_event: React.MouseEvent | React.KeyboardEvent | MouseEvent, newPage: number) => {
setPageIndex(newPage - 1);
navigate({ search: `?page=${newPage}&pageSize=${pageSize}` });
};

const onPerPageSelect = (_event: React.MouseEvent | React.KeyboardEvent | MouseEvent, newPerPage: number) => {
setPageSize(newPerPage);
setPageIndex(0);
navigate({ search: `?page=1&pageSize=${newPerPage}` });
};

if (error) {
return <ErrorSection />;
}

if (loading) {
return <Skeleton screenreaderText="Loading data..." />;
}

if (!value) {
return null;
}

return (
<>
<Table aria-label="Request events table" variant="compact">
<Caption>Latest request events</Caption>
<Thead>
<Tr>
<Th>{columnNames.id}</Th>
<Th>{columnNames.receivalTime}</Th>
<Th>{columnNames.eventType}</Th>
<Th>{columnNames.requestConfig}</Th>
<Th>{columnNames.event}</Th>
</Tr>
</Thead>
<Tbody>
{value.map((requestEvent) => (
<Tr>
<Td dataLabel={columnNames.id}>
<pre>{requestEvent.id}</pre>
</Td>
<Td dataLabel={columnNames.receivalTime}>
<Timestamp date={requestEvent.receivalTime} tooltip={{ variant: TimestampTooltipVariant.default }}>
{timestampToHumanReadable(Date.now() - requestEvent.receivalTime.getTime(), false, 'ago')}
</Timestamp>
</Td>
<Td dataLabel={columnNames.event}>
<span className="pf-v5-c-timestamp pf-m-help-text">{requestEvent.eventType}</span>
</Td>
<Td dataLabel={columnNames.requestConfig}>
<Tooltip isContentLeftAligned={true} content={<code>{requestEvent.requestConfig}</code>}>
<span className="pf-v5-c-timestamp pf-m-help-text">{requestEvent.requestConfigTypeName}={requestEvent.requestConfigTypeValue}</span>
</Tooltip>
</Td>
<Td dataLabel={columnNames.eventType}>
<CodeBlock>
<ExpandableSection toggleTextExpanded="Hide" toggleTextCollapsed="Show">
<CodeBlockCode>{JSON.stringify(requestEvent.event, null, 2)}</CodeBlockCode>
</ExpandableSection>
</CodeBlock>
</Td>
</Tr>
))}
</Tbody>
</Table>
<Pagination
itemCount={total}
widgetId="request-table-pagination"
perPage={pageSize}
page={pageIndex + 1}
variant={PaginationVariant.bottom}
onSetPage={onSetPage}
onPerPageSelect={onPerPageSelect}
/>
</>
);
};
67 changes: 67 additions & 0 deletions ui/src/app/components/RequestEventTable/useRequestEvents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
///
/// JBoss, Home of Professional Open Source.
/// Copyright 2023 Red Hat, Inc., and individual contributors
/// as indicated by the @author tags.
///
/// 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 { DefaultSbomerApi } from '@app/api/DefaultSbomerApi';
import { useCallback, useState } from 'react';
import useAsyncRetry from 'react-use/lib/useAsyncRetry';

export function useRequestEvents(initialPage: number, intialPageSize: number) {
const sbomerApi = DefaultSbomerApi.getInstance();
const [total, setTotal] = useState(0);
const [pageIndex, setPageIndex] = useState(initialPage || 0);
const [pageSize, setPageSize] = useState(intialPageSize || 10);

const getRequestEvents = useCallback(
async ({ pageSize, pageIndex }: { pageSize: number; pageIndex: number }) => {
try {
return await sbomerApi.getRequestEvents({ pageSize, pageIndex });
} catch (e) {
return Promise.reject(e);
}
},
[pageIndex, pageSize],
);

const { loading, value, error, retry } = useAsyncRetry(
() =>
getRequestEvents({
pageSize: pageSize,
pageIndex: pageIndex,
}).then((data) => {
setTotal(data.total);
return data.data;
}),
[pageIndex, pageSize],
);

return [
{
pageIndex,
pageSize,
total,
value,
loading,
error,
},
{
setPageIndex,
setPageSize,
retry,
},
] as const;
}
1 change: 0 additions & 1 deletion ui/src/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import '@patternfly/react-core/dist/styles/base.css';
import * as React from 'react';
import { createBrowserRouter, RouteObject, RouterProvider } from 'react-router-dom';


import { IAppRoute, routes } from './routes';

const App = () => {
Expand Down
6 changes: 6 additions & 0 deletions ui/src/app/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { GenerationRequestPage } from './components/Pages/GenerationRequests/Gen
import { GenerationRequestsPage } from './components/Pages/GenerationRequests/GenerationRequestsPage';
import { ManifestPage } from './components/Pages/Manifests/ManifestPage';
import { ManifestsPage } from './components/Pages/Manifests/ManifestsPage';
import { RequestEventsPage } from './components/Pages/RequestEvents/RequestsEventsPage';
import { NotFoundPage } from './components/Pages/NotFound/NotFoundPage';

let routeFocusTimer: number;
Expand Down Expand Up @@ -53,6 +54,11 @@ const routes: AppRouteConfig[] = [
element: <ManifestPage />,
path: '/manifests/:id',
},
{
element: <RequestEventsPage />,
label: 'Request Events',
path: '/requestevents',
},
{
element: <NotFoundPage />,
path: '*',
Expand Down
80 changes: 80 additions & 0 deletions ui/src/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,81 @@ export class SbomerManifest {
}
}

export class SbomerRequest {
public id: string;
public receivalTime: Date;
public eventType: string;
public requestConfig: string;
public requestConfigTypeName: string;
public requestConfigTypeValue: string;
public event: any;
public eventDestination: string;

constructor(payload: any) {
this.id = payload.id;
this.receivalTime = new Date(payload.receival_time);
this.eventType = payload.eventType;

// Parse `request_config` if it exists
if (payload.requestConfig) {
try {

this.requestConfig = JSON.stringify(payload.requestConfig);
var rConfig = JSON.parse(this.requestConfig); // Convert to JSON object
this.requestConfigTypeName = rConfig.type;

// Match the type and extract the value
switch (this.requestConfigTypeName) {
case 'errata-advisory':
this.requestConfigTypeValue = rConfig.advisoryId;
break;
case 'image':
this.requestConfigTypeValue = rConfig.image;
break;
case 'pnc-build':
this.requestConfigTypeValue = rConfig.buildId;
break;
case 'pnc-operation':
this.requestConfigTypeValue = rConfig.operationId;
break;
case 'pnc-analysis':
this.requestConfigTypeValue = rConfig.milestoneId;
break;
default:
this.requestConfigTypeName = '';
this.requestConfigTypeValue = '';
}
} catch (error) {
console.error('Failed to parse requestConfig:', error);
this.requestConfig = ''; // Set to null if parsing fails
this.requestConfigTypeName = '';
this.requestConfigTypeValue = '';
}
} else {
this.requestConfig = '';
this.requestConfigTypeName = '';
this.requestConfigTypeValue = '';
}

// Parse `request_config` if it exists
if (payload.event) {
try {
this.event = JSON.parse(JSON.stringify(payload.event)); // Convert to JSON object
this.eventDestination = this.event.destination;
} catch (error) {
console.error('Failed to parse event:', error);
this.event = null; // Set to null if parsing fails
this.eventDestination = '';
}
} else {
this.event = null; // Set to null if parsing fails
this.eventDestination = '';
}

this.event = payload.event;
}
}

export type GenerateParams = {
config: string;
};
Expand All @@ -118,4 +193,9 @@ export type SbomerApi = {
getGeneration(id: string): Promise<SbomerGeneration>;

getManifest(id: string): Promise<SbomerManifest>;

getRequestEvents(pagination: {
pageSize: number;
pageIndex: number;
}): Promise<{ data: SbomerRequest[]; total: number }>;
};

0 comments on commit 93233c5

Please sign in to comment.