Skip to content

Commit

Permalink
RHINENG-10603 first page table functionality - Pagination + Sorting (#3)
Browse files Browse the repository at this point in the history
* RHINENG-10603 first page table functionality

* minor fixes

* code refactoring for landing page table

* handling offset and sorting query params and code cleanup

* disable third sort on table component

* added unit tests and code cleanup

* improved landing page layout

* fixed unit test
  • Loading branch information
PreetiW authored and jkilzi committed Aug 5, 2024
1 parent 601c232 commit 5cc5fa3
Show file tree
Hide file tree
Showing 13 changed files with 1,808 additions and 71 deletions.
16 changes: 15 additions & 1 deletion workspaces/resource-optimization/app-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
app:
title: Resource Optimization
baseUrl: http://localhost:3000
support:
url: https://github.com/backstage/backstage/issues # Used by common ErrorPage
items: # Used by common SupportButton component
- title: Issues
icon: github
links:
- url: https://github.com/backstage/backstage/issues
title: GitHub Issues
- title: Discord Chatroom
icon: chat
links:
- url: https://discord.gg/backstage-687207715902193673
title: '#backstage'

organization:
name: Red Hat
Expand Down Expand Up @@ -73,7 +86,8 @@ auth:
# See https://backstage.io/docs/auth/guest/provider
guest: {}

scaffolder: {}
scaffolder:
{}
# see https://backstage.io/docs/features/software-templates/configuration for software template options

catalog:
Expand Down
7 changes: 7 additions & 0 deletions workspaces/resource-optimization/packages/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { CatalogGraphPage } from '@backstage/plugin-catalog-graph';
import { RequirePermission } from '@backstage/plugin-permission-react';
import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha';
import { ResourceOptimizationPage } from '@backstage-community/plugin-resource-optimization';
import { ResourceOptimizationDetailPage } from '@backstage-community/plugin-resource-optimization';

const app = createApp({
apis,
Expand Down Expand Up @@ -102,6 +103,12 @@ const routes = (
<Route path="/settings" element={<UserSettingsPage />} />
<Route path="/catalog-graph" element={<CatalogGraphPage />} />
<Route path="/resource-optimization" element={<ResourceOptimizationPage />} />
<Route
path="/resource-optimization/:id"
element={<ResourceOptimizationDetailPage />}
>
{entityPage}
</Route>
</FlatRoutes>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,45 @@ import { screen } from '@testing-library/react';
import {
setupRequestMockHandlers,
renderInTestApp,
TestApiRegistry,
} from '@backstage/test-utils';
import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
import { ApiProvider } from '@backstage/core-app-api';
import { optimizationsApiRef } from '../../api/refs';
import { searchApiRef } from '@backstage/plugin-search-react';
import { getRecommendationMockResponse } from './mockResponses';
import { RecommendationList } from '@backstage-community/plugin-resource-optimization-common';
import { TypedResponse } from '@backstage-community/plugin-resource-optimization-common/src/generated/apis/OptimizationsApi.client';

const emptySearchResults = Promise.resolve({
results: [],
});

const recommendationsListResult: Promise<TypedResponse<RecommendationList>> =
new Promise((resolve, reject) => {
return {
json: async () => getRecommendationMockResponse,
};
});

const query = () => emptySearchResults;
const querySpy = jest.fn(query);
const searchApi = { query: querySpy };

const catalogApi: jest.Mocked<CatalogApi> = {
getEntitiesByRefs: jest.fn(),
} as any;

const getRecommendationList = () => recommendationsListResult;
const getRecommendationListSpy = jest.fn(getRecommendationList);
const optimizationApi = { getRecommendationList: getRecommendationListSpy };

// create apiRegistry for mocking apis
const apiRegistry = TestApiRegistry.from(
[searchApiRef, searchApi],
[optimizationsApiRef, optimizationApi],
[catalogApiRef, catalogApi],
);

describe('ExampleComponent', () => {
const server = setupServer();
Expand All @@ -21,9 +59,12 @@ describe('ExampleComponent', () => {
});

it('should render', async () => {
await renderInTestApp(<ExampleComponent />);
expect(
screen.getByText('Welcome to resource-optimization!'),
).toBeInTheDocument();
expect(1).toBeTruthy();
await renderInTestApp(
<ApiProvider apis={apiRegistry}>
<ExampleComponent />
</ApiProvider>,
);
expect(screen.getByText('Resource Optimization')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -1,29 +1,32 @@
import React from 'react';
import { Typography, Grid } from '@material-ui/core';
import React, { useState } from 'react';
import { Grid, Chip, Typography } from '@material-ui/core';
import {
Header,
Page,
Content,
Link,
TableColumn,
Table,
Select,
Progress,
ResponseErrorPanel,
ContentHeader,
SupportButton,
} from '@backstage/core-components';
import { SearchBar } from '@backstage/plugin-search-react';
import useAsync from 'react-use/lib/useAsync';
import { useApi } from '@backstage/core-plugin-api';
import { optimizationsApiRef } from '../../api/refs';
import { Recommendations } from '@backstage-community/plugin-resource-optimization-common';
import { columns } from '../Tables/columns';
import {
CatalogFilterLayout,
EntityListProvider,
} from '@backstage/plugin-catalog-react';

export default {
title: 'Plugins/Examples',
component: Page,
};

// const VALUE_NOT_AVAILABLE = 'N/A';

const SELECT_ITEMS = [
{
label: 'Cluster 1',
Expand All @@ -39,40 +42,10 @@ const SELECT_ITEMS = [
},
];

const columns: TableColumn<Recommendations>[] = [
{
title: 'Containers',
highlight: true,
render: row => <Link to="#message-source">{row.container}</Link>,
},
{
title: 'Projects',
render: row => <Typography variant="body2">{row.project}</Typography>,
},
{
title: 'Workloads',
render: row => <Typography variant="body2">{row.workload}</Typography>,
},
{
title: 'Workload types',
render: row => <Typography variant="body2">{row.workloadType}</Typography>,
},
{
title: 'Clusters',
render: row => <Typography variant="body2">{row.clusterAlias}</Typography>,
},
{
title: 'Last reported',
render: row => (
<Typography variant="body2">{row.lastReported?.toString()}</Typography>
),
},
];

const ClusterFilter = () => (
<Select
placeholder="All results"
label="CLUSTER"
label="Cluster"
items={SELECT_ITEMS}
multiple
onChange={() => {}}
Expand All @@ -82,7 +55,7 @@ const ClusterFilter = () => (
const ProjectFilter = () => (
<Select
placeholder="All results"
label="PROJECT"
label="Project"
items={SELECT_ITEMS}
multiple
onChange={() => {}}
Expand All @@ -92,7 +65,7 @@ const ProjectFilter = () => (
const WorkloadFilter = () => (
<Select
placeholder="All results"
label="WORKLOAD"
label="Workload"
items={SELECT_ITEMS}
multiple
onChange={() => {}}
Expand All @@ -102,52 +75,107 @@ const WorkloadFilter = () => (
const TypeFilter = () => (
<Select
placeholder="All results"
label="TYPE"
label="WorkLoad Type"
items={SELECT_ITEMS}
multiple
onChange={() => {}}
/>
);

type SortOrder = 'asc' | 'desc';

export const ExampleComponent = () => {
const api = useApi(optimizationsApiRef);

const [page, setPage] = useState(0); // first page starts at 0
const [rowsPerPage, setRowsPerPage] = useState(5);

const [orderBy, setOrderBy] = useState('last_reported');
const [orderDirection, setOrderDirection] = useState<SortOrder>('desc');

const { value, loading, error } = useAsync(async () => {
const response = await api.getRecommendationList({ query: {} });
const offsetValue = page * rowsPerPage;

const apiQuery: Parameters<typeof api.getRecommendationList>[0]['query'] = {
limit: rowsPerPage,
offset: offsetValue,
orderBy: orderBy,
orderHow: orderDirection,
};

const response = await api.getRecommendationList({
query: apiQuery,
});
const payload = await response.json();
return payload;
}, []);

if (loading) {
return <Progress />;
}
}, [rowsPerPage, page, orderBy, orderDirection]);

if (error) {
return <ResponseErrorPanel error={error} />;
}

const handleChangeRowsPerPage = (pageSize: number) => {
setRowsPerPage(pageSize);
};

const handleChangePage = (page: number, pageSize: number) => {
setPage(page);
};

const handleOnOrderChange = (orderBy: number, orderDirection: SortOrder) => {
if (orderBy >= 0) {
setOrderBy(`${columns[orderBy].field}`);
setOrderDirection(orderDirection);
}
};

const handleOnSearchChange = (searchText: string) => {
console.log(searchText);
};

const tableTitle = `Optimizable containers (${value?.meta?.count || 0})`;

return (
<Page themeId="tool">
<Header title="Resource Optimization" />
<Content>
<ContentHeader title="Filters" />
<Grid container direction="row">
<Grid item xs={3}>
<ClusterFilter />
<ProjectFilter />
<WorkloadFilter />
<TypeFilter />
</Grid>

<Grid item xs={9}>
<Table<Recommendations>
options={{ paging: true, padding: 'dense' }}
data={value!.data ?? []}
columns={columns}
title="Optimizable containers"
/>
</Grid>
</Grid>
<ContentHeader title="">
<SupportButton>All your optimizations</SupportButton>
</ContentHeader>

<EntityListProvider>
<CatalogFilterLayout>
<CatalogFilterLayout.Filters>
<Typography variant="h6">Filters</Typography>
<hr></hr>
<ClusterFilter />
<ProjectFilter />
<WorkloadFilter />
<TypeFilter />
</CatalogFilterLayout.Filters>
<CatalogFilterLayout.Content>
<Table<Recommendations>
title={tableTitle}
options={{
debounceInterval: 700,
paging: true,
search: true,
padding: 'dense',
thirdSortClick: false,
}}
data={value?.data || []}
isLoading={loading}
columns={columns}
totalCount={value?.meta?.count || 0}
page={page}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
onOrderChange={handleOnOrderChange}
onSearchChange={handleOnSearchChange}
/>
</CatalogFilterLayout.Content>
</CatalogFilterLayout>
</EntityListProvider>
</Content>
</Page>
);
Expand Down
Loading

0 comments on commit 5cc5fa3

Please sign in to comment.