Skip to content
This repository was archived by the owner on Jun 18, 2025. It is now read-only.

Commit 610dfcc

Browse files
feat: add dashboard to landing page (#211)
This change adds a dashboard to the Landing Page to surface quick information to the user.
1 parent a47be85 commit 610dfcc

File tree

4 files changed

+332
-52
lines changed

4 files changed

+332
-52
lines changed

plugins/cad/src/components/PackageManagementPage/PackageManagementPage.tsx

Lines changed: 19 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -16,30 +16,22 @@
1616

1717
import {
1818
Breadcrumbs,
19-
Button,
2019
ContentHeader,
2120
Progress,
21+
Tabs,
2222
} from '@backstage/core-components';
23-
import { useApi, useRouteRef } from '@backstage/core-plugin-api';
23+
import { useApi } from '@backstage/core-plugin-api';
2424
import { makeStyles, Typography } from '@material-ui/core';
2525
import Alert from '@material-ui/lab/Alert';
26-
import { groupBy } from 'lodash';
2726
import React from 'react';
28-
import { Link as RouterLink } from 'react-router-dom';
2927
import useAsync from 'react-use/lib/useAsync';
3028
import { configAsDataApiRef } from '../../apis';
31-
import { registerRepositoryRouteRef } from '../../routes';
32-
import { showRegisteredFunctionRepositories } from '../../utils/featureFlags';
33-
import {
34-
ContentSummary,
35-
getPackageDescriptor,
36-
PackageContentSummaryOrder,
37-
} from '../../utils/repository';
3829
import {
3930
getRepositorySummaries,
4031
populatePackageSummaries,
4132
} from '../../utils/repositorySummary';
42-
import { RepositoriesTable } from './components/RepositoriesTable';
33+
import { DashboardTabContent } from './components/DashboardTabContent';
34+
import { RepositoriesTabContent } from './components/RepositoriesTabContent';
4335

4436
export const useStyles = makeStyles({
4537
repositoriesTablesSection: {
@@ -50,13 +42,10 @@ export const useStyles = makeStyles({
5042
});
5143

5244
export const PackageManagementPage = () => {
53-
const classes = useStyles();
5445
const api = useApi(configAsDataApiRef);
5546

56-
const registerRepository = useRouteRef(registerRepositoryRouteRef);
57-
5847
const {
59-
value: allRepositorySummaries,
48+
value: allSummaries,
6049
loading,
6150
error,
6251
} = useAsync(async () => {
@@ -81,52 +70,30 @@ export const PackageManagementPage = () => {
8170
return <Alert severity="error">{error.message}</Alert>;
8271
}
8372

84-
if (!allRepositorySummaries) {
73+
if (!allSummaries) {
8574
throw new Error('Repository summaries is not defined');
8675
}
8776

88-
const repositoriesByContentType = groupBy(
89-
allRepositorySummaries,
90-
({ repository }) => getPackageDescriptor(repository),
91-
);
92-
9377
return (
9478
<div>
9579
<Breadcrumbs>
9680
<Typography>Package Management</Typography>
9781
</Breadcrumbs>
9882

99-
<ContentHeader title="Package Management">
100-
<Button
101-
component={RouterLink}
102-
to={registerRepository()}
103-
color="primary"
104-
variant="contained"
105-
>
106-
Register Repository
107-
</Button>
108-
</ContentHeader>
109-
110-
<div className={classes.repositoriesTablesSection}>
111-
{PackageContentSummaryOrder.map(contentType => (
112-
<RepositoriesTable
113-
key={contentType}
114-
title={`${contentType} Repositories`}
115-
contentType={contentType}
116-
repositories={repositoriesByContentType[contentType] ?? []}
117-
/>
118-
))}
83+
<ContentHeader title="Package Management" />
11984

120-
{showRegisteredFunctionRepositories() && (
121-
<RepositoriesTable
122-
title="Function Repositories"
123-
contentType={ContentSummary.FUNCTION}
124-
repositories={
125-
repositoriesByContentType[ContentSummary.FUNCTION] ?? []
126-
}
127-
/>
128-
)}
129-
</div>
85+
<Tabs
86+
tabs={[
87+
{
88+
label: 'Dashboard',
89+
content: <DashboardTabContent summaries={allSummaries} />,
90+
},
91+
{
92+
label: 'Repositories',
93+
content: <RepositoriesTabContent summaries={allSummaries} />,
94+
},
95+
]}
96+
/>
13097
</div>
13198
);
13299
};
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/**
2+
* Copyright 2022 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { InfoCard } from '@backstage/core-components';
18+
import { makeStyles } from '@material-ui/core';
19+
import { Alert } from '@material-ui/lab';
20+
import React, { Fragment } from 'react';
21+
import { PackageRevisionLifecycle } from '../../../types/PackageRevision';
22+
import { Repository } from '../../../types/Repository';
23+
import { PackageSummary } from '../../../utils/packageSummary';
24+
import { toLowerCase } from '../../../utils/string';
25+
import { RepositoryLink } from '../../Links';
26+
27+
type ContentInfoCardProps = {
28+
contentType: string;
29+
repositories: Repository[];
30+
packages: PackageSummary[];
31+
};
32+
33+
export const useStyles = makeStyles({
34+
summary: {
35+
marginTop: '8px',
36+
'& > *:not(:first-child)': {
37+
marginTop: '8px',
38+
},
39+
},
40+
actions: {
41+
justifyContent: 'flex-start',
42+
margin: '12px',
43+
marginRight: 'auto',
44+
},
45+
});
46+
47+
const getActions = (
48+
repositories: Repository[],
49+
className: string,
50+
): JSX.Element => {
51+
if (repositories.length === 0) {
52+
return <Fragment />;
53+
}
54+
55+
return (
56+
<div className={className}>
57+
<Fragment>Repositories:</Fragment>&nbsp;
58+
{repositories.map((r, idx) => (
59+
<Fragment>
60+
<RepositoryLink repository={r} />
61+
{idx !== repositories.length - 1 && <Fragment>, </Fragment>}
62+
</Fragment>
63+
))}
64+
</div>
65+
);
66+
};
67+
68+
const getPublishedPackages = (packages: PackageSummary[]): PackageSummary[] => {
69+
return packages.filter(summary => !!summary.latestPublishedRevision);
70+
};
71+
72+
const getProposedPackages = (packages: PackageSummary[]): PackageSummary[] => {
73+
return packages.filter(
74+
summary =>
75+
summary.latestRevision.spec.lifecycle ===
76+
PackageRevisionLifecycle.PROPOSED,
77+
);
78+
};
79+
80+
const getDraftPackages = (packages: PackageSummary[]): PackageSummary[] => {
81+
return packages.filter(
82+
summary =>
83+
summary.latestRevision.spec.lifecycle === PackageRevisionLifecycle.DRAFT,
84+
);
85+
};
86+
87+
const getUpgradePackages = (packages: PackageSummary[]): PackageSummary[] => {
88+
return packages.filter(summary => summary.isUpgradeAvailable);
89+
};
90+
91+
export const ContentInfoCard = ({
92+
contentType,
93+
repositories,
94+
packages,
95+
}: ContentInfoCardProps) => {
96+
const classes = useStyles();
97+
98+
const title = `${contentType}s`;
99+
const contentTypeLowerCase = toLowerCase(contentType);
100+
101+
const published = getPublishedPackages(packages).length;
102+
const upgradesAvailable = getUpgradePackages(packages).length;
103+
const pendingReview = getDraftPackages(packages).length;
104+
const drafts = getProposedPackages(packages).length;
105+
106+
const subheader = `${repositories.length} repositories registered`;
107+
108+
return (
109+
<InfoCard
110+
title={title}
111+
subheader={subheader}
112+
actions={getActions(repositories, classes.actions)}
113+
>
114+
<div className={classes.summary}>
115+
{repositories.length === 0 && (
116+
<Alert severity="info">
117+
no {contentTypeLowerCase} repositories registered
118+
</Alert>
119+
)}
120+
121+
{repositories.length > 0 && (
122+
<Alert severity="success">
123+
{published} {contentTypeLowerCase}s published
124+
</Alert>
125+
)}
126+
127+
{upgradesAvailable > 0 && (
128+
<Alert severity="info">
129+
{upgradesAvailable} {contentTypeLowerCase}s with upgrades available
130+
</Alert>
131+
)}
132+
133+
{pendingReview > 0 && (
134+
<Alert severity="info">
135+
{pendingReview} {contentTypeLowerCase} revisions pending review
136+
</Alert>
137+
)}
138+
139+
{drafts > 0 && <Alert severity="info">{drafts} draft revisions</Alert>}
140+
</div>
141+
</InfoCard>
142+
);
143+
};
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/**
2+
* Copyright 2022 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { makeStyles } from '@material-ui/core';
18+
import { flatten, groupBy } from 'lodash';
19+
import React from 'react';
20+
import { RepositorySummary } from '../../../types/RepositorySummary';
21+
import {
22+
getPackageDescriptor,
23+
PackageContentSummaryOrder,
24+
} from '../../../utils/repository';
25+
import { ContentInfoCard } from './ContentInfoCard';
26+
27+
type DashboardTabContentProps = {
28+
summaries: RepositorySummary[];
29+
};
30+
31+
export const useStyles = makeStyles({
32+
summaryList: {
33+
'& > *:not(:first-child)': {
34+
marginTop: '40px',
35+
},
36+
},
37+
38+
cards: {
39+
display: 'flex',
40+
flexFlow: 'wrap',
41+
'& > *': {
42+
minWidth: '500px',
43+
maxWidth: '800px',
44+
flex: 1,
45+
margin: '0 16px 16px 0',
46+
},
47+
},
48+
});
49+
50+
const getDescriptor = (summary: RepositorySummary): string =>
51+
getPackageDescriptor(summary.repository);
52+
53+
export const DashboardTabContent = ({
54+
summaries,
55+
}: DashboardTabContentProps) => {
56+
const classes = useStyles();
57+
58+
const repositoriesByContentType = groupBy(summaries, getDescriptor);
59+
60+
return (
61+
<div className={classes.cards}>
62+
{PackageContentSummaryOrder.map(contentType => {
63+
const contentRepositories =
64+
repositoriesByContentType[contentType] || [];
65+
const packageSummaries = flatten(
66+
contentRepositories.map(r => r.packageSummaries || []),
67+
);
68+
const repositories = contentRepositories.map(
69+
repository => repository.repository,
70+
);
71+
72+
return (
73+
<ContentInfoCard
74+
key={contentType}
75+
contentType={contentType}
76+
repositories={repositories}
77+
packages={packageSummaries}
78+
/>
79+
);
80+
})}
81+
</div>
82+
);
83+
};

0 commit comments

Comments
 (0)