Introduce Stats Feature
Create two new API calls, getAllTags & getAllReleases Rename "components" prop to more suitable "features" Signed-off-by: Erik Engervall <erik.engervall@gmail.com>
This commit is contained in:
@@ -54,6 +54,6 @@ The plugin exports a single full-page extension `GitHubReleaseManagerPage`, whic
|
||||
|
||||
The plugin is configurable either via props or the select elements on the page.
|
||||
|
||||
If project configuration is provided via props, the select elements are disabled. It is also possible to omit components from the page via props, as well as attaching callbacks for successful executions.
|
||||
If project configuration is provided via props, the select elements are disabled. It is also possible to omit features from the page via props, as well as attaching callbacks for successful executions.
|
||||
|
||||
See the plugin's dev folder (`dev/index.tsx`) to see some examples.
|
||||
|
||||
@@ -38,8 +38,9 @@ import { useStyles } from './styles/styles';
|
||||
|
||||
export interface GitHubReleaseManagerProps {
|
||||
project?: Omit<Project, 'isProvidedViaProps'>;
|
||||
components?: {
|
||||
features?: {
|
||||
info?: Pick<ComponentConfig<void>, 'omit'>;
|
||||
stats?: Pick<ComponentConfig<void>, 'omit'>;
|
||||
createRc?: ComponentConfigCreateRc;
|
||||
promoteRc?: ComponentConfigPromoteRc;
|
||||
patch?: ComponentConfigPatch;
|
||||
@@ -90,9 +91,7 @@ export function GitHubReleaseManager(props: GitHubReleaseManagerProps) {
|
||||
<RepoDetailsForm username={usernameResponse.value.username} />
|
||||
</InfoCardPlus>
|
||||
|
||||
{isProjectValid(project) && (
|
||||
<Features components={props.components} />
|
||||
)}
|
||||
{isProjectValid(project) && <Features features={props.features} />}
|
||||
</div>
|
||||
</ProjectContext.Provider>
|
||||
</PluginApiClientContext.Provider>
|
||||
|
||||
@@ -57,6 +57,10 @@ describe('PluginApiClient', () => {
|
||||
"promoteRc": Object {
|
||||
"promoteRelease": [Function],
|
||||
},
|
||||
"stats": Object {
|
||||
"getAllReleases": [Function],
|
||||
"getAllTags": [Function],
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -562,6 +562,43 @@ ${selectedPatchCommit.commit.message}`,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
stats = {
|
||||
getAllTags: async ({ owner, repo }: OwnerRepo) => {
|
||||
const { octokit } = await this.getOctokit();
|
||||
|
||||
const tags = await octokit.paginate(octokit.repos.listTags, {
|
||||
owner,
|
||||
repo,
|
||||
per_page: 100,
|
||||
...DISABLE_CACHE,
|
||||
});
|
||||
|
||||
return tags.map(tag => ({
|
||||
tagName: tag.name,
|
||||
}));
|
||||
},
|
||||
|
||||
getAllReleases: async ({ owner, repo }: OwnerRepo) => {
|
||||
const { octokit } = await this.getOctokit();
|
||||
|
||||
const releases = await octokit.paginate(octokit.repos.listReleases, {
|
||||
owner,
|
||||
repo,
|
||||
per_page: 100,
|
||||
...DISABLE_CACHE,
|
||||
});
|
||||
|
||||
return releases.map(release => ({
|
||||
release,
|
||||
id: release.id,
|
||||
name: release.name,
|
||||
tagName: release.tag_name,
|
||||
createdAt: release.published_at,
|
||||
htmlUrl: release.html_url,
|
||||
}));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
type UnboxPromise<T extends Promise<any>> = T extends Promise<infer U>
|
||||
@@ -623,6 +660,28 @@ type GetRecentCommits = (
|
||||
export type GetRecentCommitsResult = UnboxReturnedPromise<GetRecentCommits>;
|
||||
export type GetRecentCommitsResultSingle = UnboxArray<GetRecentCommitsResult>;
|
||||
|
||||
type GetAllTags = (
|
||||
args: OwnerRepo,
|
||||
) => Promise<
|
||||
Array<{
|
||||
tagName: string;
|
||||
}>
|
||||
>;
|
||||
export type GetAllTagsResult = UnboxReturnedPromise<GetAllTags>;
|
||||
|
||||
type GetAllReleases = (
|
||||
args: OwnerRepo,
|
||||
) => Promise<
|
||||
Array<{
|
||||
id: number;
|
||||
name: string | null;
|
||||
tagName: string;
|
||||
createdAt: string | null;
|
||||
htmlUrl: string;
|
||||
}>
|
||||
>;
|
||||
export type GetAllReleasesResult = UnboxReturnedPromise<GetAllReleases>;
|
||||
|
||||
type GetLatestRelease = (
|
||||
args: OwnerRepo,
|
||||
) => Promise<{
|
||||
@@ -858,4 +917,8 @@ export interface IPluginApiClient {
|
||||
promoteRc: {
|
||||
promoteRelease: PromoteRelease;
|
||||
};
|
||||
stats: {
|
||||
getAllTags: GetAllTags;
|
||||
getAllReleases: GetAllReleases;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -52,8 +52,8 @@ describe('Features', () => {
|
||||
|
||||
expect(getByTestId(TEST_IDS.info.info)).toMatchInlineSnapshot(`
|
||||
<div
|
||||
class="MuiBox-root MuiBox-root-11"
|
||||
data-testid="grm--info"
|
||||
style="margin-bottom: 1em;"
|
||||
>
|
||||
<h6
|
||||
class="MuiTypography-root MuiTypography-h6"
|
||||
@@ -98,20 +98,6 @@ describe('Features', () => {
|
||||
</strong>
|
||||
: A GitHub release intended for end users
|
||||
</p>
|
||||
<button
|
||||
class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedSecondary"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="MuiButton-label"
|
||||
>
|
||||
Show stats
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -32,9 +32,9 @@ import { useVersioningStrategyMatchesRepoTags } from '../hooks/useVersioningStra
|
||||
import { validateTagName } from '../helpers/tagParts/validateTagName';
|
||||
|
||||
export function Features({
|
||||
components,
|
||||
features,
|
||||
}: {
|
||||
components: GitHubReleaseManagerProps['components'];
|
||||
features: GitHubReleaseManagerProps['features'];
|
||||
}) {
|
||||
const { pluginApiClient } = usePluginApiClientContext();
|
||||
const { project } = useProjectContext();
|
||||
@@ -114,34 +114,35 @@ export function Features({
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{!components?.info?.omit && (
|
||||
{!features?.info?.omit && (
|
||||
<Info
|
||||
latestRelease={gitHubBatchInfo.value.latestRelease}
|
||||
releaseBranch={gitHubBatchInfo.value.releaseBranch}
|
||||
statsEnabled={features?.stats?.omit !== true}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!components?.createRc?.omit && (
|
||||
{!features?.createRc?.omit && (
|
||||
<CreateRc
|
||||
latestRelease={gitHubBatchInfo.value.latestRelease}
|
||||
releaseBranch={gitHubBatchInfo.value.releaseBranch}
|
||||
defaultBranch={gitHubBatchInfo.value.repository.defaultBranch}
|
||||
successCb={components?.createRc?.successCb}
|
||||
successCb={features?.createRc?.successCb}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!components?.promoteRc?.omit && (
|
||||
{!features?.promoteRc?.omit && (
|
||||
<PromoteRc
|
||||
latestRelease={gitHubBatchInfo.value.latestRelease}
|
||||
successCb={components?.promoteRc?.successCb}
|
||||
successCb={features?.promoteRc?.successCb}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!components?.patch?.omit && (
|
||||
{!features?.patch?.omit && (
|
||||
<Patch
|
||||
latestRelease={gitHubBatchInfo.value.latestRelease}
|
||||
releaseBranch={gitHubBatchInfo.value.releaseBranch}
|
||||
successCb={components?.patch?.successCb}
|
||||
successCb={features?.patch?.successCb}
|
||||
/>
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Link, Typography, Button } from '@material-ui/core';
|
||||
import { Link, Typography, Button, Box } from '@material-ui/core';
|
||||
import BarChartIcon from '@material-ui/icons/BarChart';
|
||||
|
||||
import {
|
||||
GetBranchResult,
|
||||
@@ -23,7 +24,7 @@ import {
|
||||
} from '../../api/PluginApiClient';
|
||||
import { Differ } from '../../components/Differ';
|
||||
import { InfoCardPlus } from '../../components/InfoCardPlus';
|
||||
import { Stats } from '../../components/Stats/Stats';
|
||||
import { Stats } from '../Stats/Stats';
|
||||
import { TEST_IDS } from '../../test-helpers/test-ids';
|
||||
import { useProjectContext } from '../../contexts/ProjectContext';
|
||||
import { useStyles } from '../../styles/styles';
|
||||
@@ -32,16 +33,21 @@ import flowImage from './flow.png';
|
||||
interface InfoCardProps {
|
||||
releaseBranch: GetBranchResult | null;
|
||||
latestRelease: GetLatestReleaseResult;
|
||||
statsEnabled: boolean;
|
||||
}
|
||||
|
||||
export const Info = ({ releaseBranch, latestRelease }: InfoCardProps) => {
|
||||
export const Info = ({
|
||||
releaseBranch,
|
||||
latestRelease,
|
||||
statsEnabled,
|
||||
}: InfoCardProps) => {
|
||||
const { project } = useProjectContext();
|
||||
const classes = useStyles();
|
||||
const [showStats, setShowStats] = useState(false);
|
||||
|
||||
return (
|
||||
<InfoCardPlus>
|
||||
<div style={{ marginBottom: '1em' }} data-testid={TEST_IDS.info.info}>
|
||||
<Box marginBottom={1} data-testid={TEST_IDS.info.info}>
|
||||
<Typography variant="h6">Terminology</Typography>
|
||||
|
||||
<Typography>
|
||||
@@ -65,18 +71,9 @@ export const Info = ({ releaseBranch, latestRelease }: InfoCardProps) => {
|
||||
<strong>Release Version</strong>: A GitHub release intended for end
|
||||
users
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={() => setShowStats(true)}
|
||||
>
|
||||
Show stats
|
||||
</Button>
|
||||
{showStats && <Stats setShowStats={setShowStats} />}
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '1em' }}>
|
||||
<Box marginBottom={1}>
|
||||
<Typography variant="h6">Flow</Typography>
|
||||
|
||||
<Typography className={classes.paragraph}>
|
||||
@@ -90,9 +87,9 @@ export const Info = ({ releaseBranch, latestRelease }: InfoCardProps) => {
|
||||
</Typography>
|
||||
|
||||
<img alt="flow" src={flowImage} style={{ width: '100%' }} />
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
<div style={{ marginBottom: '1em' }}>
|
||||
<Box marginBottom={1}>
|
||||
<Typography variant="h6">Details</Typography>
|
||||
|
||||
<Typography>
|
||||
@@ -113,7 +110,23 @@ export const Info = ({ releaseBranch, latestRelease }: InfoCardProps) => {
|
||||
<Typography>
|
||||
Latest release: <Differ icon="tag" next={latestRelease?.tagName} />
|
||||
</Typography>
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
{statsEnabled && (
|
||||
<Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={() => setShowStats(true)}
|
||||
startIcon={<BarChartIcon />}
|
||||
size="small"
|
||||
>
|
||||
Show stats
|
||||
</Button>
|
||||
|
||||
{showStats && <Stats setShowStats={setShowStats} />}
|
||||
</Box>
|
||||
)}
|
||||
</InfoCardPlus>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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 React from 'react';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import {
|
||||
Box,
|
||||
makeStyles,
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
|
||||
import { CenteredCircularProgress } from '../../components/CenteredCircularProgress';
|
||||
import { getMappedReleases } from './getMappedReleases';
|
||||
import { getSummary } from './getSummary';
|
||||
import { getTags } from './getTags';
|
||||
import { Row } from './Row';
|
||||
import { useGetStats } from './hooks/useGetStats';
|
||||
import { useProjectContext } from '../../contexts/ProjectContext';
|
||||
import { Warn } from './Warn';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
table: {
|
||||
minWidth: 650,
|
||||
},
|
||||
});
|
||||
|
||||
export function DialogBody() {
|
||||
const classes = useStyles();
|
||||
const { stats } = useGetStats();
|
||||
const { project } = useProjectContext();
|
||||
|
||||
if (stats.error) {
|
||||
return (
|
||||
<Alert severity="error">Unexpected error: {stats.error.message}</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (stats.loading) {
|
||||
return <CenteredCircularProgress />;
|
||||
}
|
||||
|
||||
if (!stats.value) {
|
||||
return <Alert severity="error">Couldn't find any stats :(</Alert>;
|
||||
}
|
||||
|
||||
const { allReleases, allTags } = stats.value;
|
||||
const mappedReleases = getMappedReleases({ allReleases, project });
|
||||
const tags = getTags({ allTags, project, mappedReleases });
|
||||
const summary = getSummary({ mappedReleases });
|
||||
const shouldWarn =
|
||||
tags.unmappable.length > 0 ||
|
||||
tags.unmatched.length > 0 ||
|
||||
mappedReleases.unmatched.length > 0;
|
||||
|
||||
if (shouldWarn) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("⚠️ Here's a summary of unmapped/unmatched tags/releases", {
|
||||
unmappableTags: tags.unmappable,
|
||||
unmatchableTags: tags.unmatched,
|
||||
unmatchableReleases: mappedReleases.unmatched,
|
||||
});
|
||||
}
|
||||
|
||||
const getDecimalNumber = (n: number) => {
|
||||
if (isNaN(n)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (n.toString().includes('.')) {
|
||||
return n.toFixed(2);
|
||||
}
|
||||
|
||||
return n;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Paper
|
||||
variant="outlined"
|
||||
style={{
|
||||
padding: 20,
|
||||
marginLeft: 25,
|
||||
marginRight: 25,
|
||||
marginBottom: 25,
|
||||
}}
|
||||
>
|
||||
<Box margin={1}>
|
||||
<Typography variant="h4">Summary</Typography>
|
||||
<Typography variant="body2">
|
||||
Total releases: {summary.totalReleases}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box margin={1}>
|
||||
<Typography variant="h6">Release Candidate</Typography>
|
||||
<Typography variant="body2">
|
||||
Release Candidate patches: {summary.totalCandidatePatches}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2">
|
||||
Release Candidate patches per release:{' '}
|
||||
{getDecimalNumber(
|
||||
summary.totalCandidatePatches / summary.totalReleases,
|
||||
)}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box margin={1}>
|
||||
<Typography variant="h6">Release Version</Typography>
|
||||
<Typography variant="body2">
|
||||
Release Version patches: {summary.totalVersionPatches}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2">
|
||||
Release Version patches per release:{' '}
|
||||
{getDecimalNumber(
|
||||
summary.totalVersionPatches / summary.totalReleases,
|
||||
)}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box margin={1}>
|
||||
<Typography variant="h6">Total</Typography>
|
||||
<Typography variant="body2">
|
||||
Patches:{' '}
|
||||
{summary.totalCandidatePatches + summary.totalVersionPatches}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2">
|
||||
Patches per release:{' '}
|
||||
{getDecimalNumber(
|
||||
(summary.totalCandidatePatches + summary.totalVersionPatches) /
|
||||
summary.totalReleases,
|
||||
)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
<TableContainer component={Paper}>
|
||||
<Table className={classes.table} size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell />
|
||||
<TableCell>Release</TableCell>
|
||||
<TableCell>Created at</TableCell>
|
||||
<TableCell># candidate patches</TableCell>
|
||||
<TableCell># release patches</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
||||
<TableBody>
|
||||
{Object.entries(mappedReleases.releases).map(
|
||||
([baseVersion, mappedRelease], index) => {
|
||||
return (
|
||||
<Row
|
||||
key={`row-${index}`}
|
||||
baseVersion={baseVersion}
|
||||
mappedRelease={mappedRelease}
|
||||
/>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
<Box marginTop={2}>
|
||||
{shouldWarn && (
|
||||
<Warn tags={tags} mappedReleases={mappedReleases} project={project} />
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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 React from 'react';
|
||||
import {
|
||||
createStyles,
|
||||
IconButton,
|
||||
Theme,
|
||||
Typography,
|
||||
withStyles,
|
||||
WithStyles,
|
||||
} from '@material-ui/core';
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
import MuiDialogTitle from '@material-ui/core/DialogTitle';
|
||||
|
||||
import { Stats } from './Stats';
|
||||
|
||||
interface DialogTitleProps extends WithStyles<typeof styles> {
|
||||
children: React.ReactNode;
|
||||
setShowStats: React.ComponentProps<typeof Stats>['setShowStats'];
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
margin: 0,
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
closeButton: {
|
||||
position: 'absolute',
|
||||
right: theme.spacing(1),
|
||||
top: theme.spacing(1),
|
||||
color: theme.palette.grey[500],
|
||||
},
|
||||
});
|
||||
|
||||
export const DialogTitle = withStyles(styles)((props: DialogTitleProps) => {
|
||||
const { children, classes, setShowStats, ...other } = props;
|
||||
|
||||
return (
|
||||
<MuiDialogTitle disableTypography className={classes.root} {...other}>
|
||||
<Typography variant="h6">{children}</Typography>
|
||||
<IconButton
|
||||
aria-label="close"
|
||||
className={classes.closeButton}
|
||||
onClick={() => setShowStats(false)}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</MuiDialogTitle>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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 React, { useState } from 'react';
|
||||
import { DateTime } from 'luxon';
|
||||
import {
|
||||
Box,
|
||||
Collapse,
|
||||
IconButton,
|
||||
Link,
|
||||
makeStyles,
|
||||
TableCell,
|
||||
TableRow,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
|
||||
import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp';
|
||||
|
||||
import { getMappedReleases } from './getMappedReleases';
|
||||
|
||||
const useRowStyles = makeStyles({
|
||||
root: {
|
||||
'& > *': {
|
||||
borderBottom: 'unset',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
interface RowProps {
|
||||
baseVersion: string;
|
||||
mappedRelease: ReturnType<typeof getMappedReleases>['releases']['0'];
|
||||
}
|
||||
|
||||
export function Row({ baseVersion, mappedRelease }: RowProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const classes = useRowStyles();
|
||||
const versions = mappedRelease.versions.reverse();
|
||||
const candidates = mappedRelease.candidates.reverse();
|
||||
const isPrerelease = versions.length === 0;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<TableRow className={classes.root}>
|
||||
<TableCell>
|
||||
<IconButton
|
||||
aria-label="expand row"
|
||||
size="small"
|
||||
onClick={() => setOpen(!open)}
|
||||
>
|
||||
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
|
||||
<TableCell component="th" scope="row">
|
||||
<Link href={mappedRelease.htmlUrl} target="_blank">
|
||||
{baseVersion}
|
||||
{isPrerelease ? ' (prerelease)' : ''}
|
||||
</Link>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
{mappedRelease.createdAt
|
||||
? DateTime.fromISO(mappedRelease.createdAt)
|
||||
.setLocale('sv-SE')
|
||||
.toFormat('yyyy-MM-dd')
|
||||
: '-'}
|
||||
</TableCell>
|
||||
|
||||
<TableCell>{candidates.length}</TableCell>
|
||||
|
||||
<TableCell>{Math.max(0, versions.length - 1)}</TableCell>
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={6}>
|
||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||
<Box margin={1}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
{!isPrerelease && (
|
||||
<Box margin={1}>
|
||||
{versions.map(version => (
|
||||
<Typography key={version} variant="body1">
|
||||
{version}
|
||||
</Typography>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{!isPrerelease && (
|
||||
<Box
|
||||
margin={1}
|
||||
style={{ transform: 'rotate(-45deg)', fontSize: 30 }}
|
||||
>
|
||||
{' 🚀 '}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box margin={1}>
|
||||
{candidates.map(candidate => (
|
||||
<Typography key={candidate} variant="body1">
|
||||
{candidate}
|
||||
</Typography>
|
||||
))}
|
||||
</Box>
|
||||
</div>
|
||||
</Box>
|
||||
</Collapse>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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 React from 'react';
|
||||
import { Button, Dialog, Theme, withStyles } from '@material-ui/core';
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
import MuiDialogActions from '@material-ui/core/DialogActions';
|
||||
import MuiDialogContent from '@material-ui/core/DialogContent';
|
||||
|
||||
import { DialogBody } from './DialogBody';
|
||||
import { DialogTitle } from './DialogTitle';
|
||||
import { Transition } from '../../components/Transition';
|
||||
|
||||
const DialogContent = withStyles((theme: Theme) => ({
|
||||
root: {
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
}))(MuiDialogContent);
|
||||
|
||||
const DialogActions = withStyles((theme: Theme) => ({
|
||||
root: {
|
||||
margin: 0,
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
}))(MuiDialogActions);
|
||||
|
||||
interface StatsProps {
|
||||
setShowStats: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
export function Stats({ setShowStats }: StatsProps) {
|
||||
return (
|
||||
<Dialog open maxWidth="md" fullWidth TransitionComponent={Transition}>
|
||||
<DialogTitle setShowStats={setShowStats}>Stats</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<DialogBody />
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={() => setShowStats(false)}
|
||||
variant="contained"
|
||||
size="large"
|
||||
color="primary"
|
||||
startIcon={<CloseIcon />}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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 React from 'react';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
|
||||
import { getMappedReleases } from './getMappedReleases';
|
||||
import { getTags } from './getTags';
|
||||
import { Project } from '../../contexts/ProjectContext';
|
||||
|
||||
interface WarnProps {
|
||||
tags: ReturnType<typeof getTags>;
|
||||
mappedReleases: ReturnType<typeof getMappedReleases>;
|
||||
project: Project;
|
||||
}
|
||||
|
||||
export const Warn = ({ tags, mappedReleases, project }: WarnProps) => {
|
||||
return (
|
||||
<Alert severity="warning" style={{ marginBottom: 10 }}>
|
||||
{tags.unmappable.length > 0 && (
|
||||
<div>
|
||||
Failed to map <strong>{tags.unmappable.length}</strong> tags to
|
||||
releases
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tags.unmatched.length > 0 && (
|
||||
<div>
|
||||
Failed to match <strong>{tags.unmatched.length}</strong> tags to{' '}
|
||||
{project.versioningStrategy}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mappedReleases.unmatched.length > 0 && (
|
||||
<div>
|
||||
Failed to match <strong>{mappedReleases.unmatched.length}</strong>{' '}
|
||||
releases to {project.versioningStrategy}
|
||||
</div>
|
||||
)}
|
||||
<div>See full output in the console</div>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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 { calverRegexp } from '../../helpers/tagParts/getCalverTagParts';
|
||||
import { GetAllReleasesResult } from '../../api/PluginApiClient';
|
||||
import { Project } from '../../contexts/ProjectContext';
|
||||
import { semverRegexp } from '../../helpers/tagParts/getSemverTagParts';
|
||||
|
||||
export function getMappedReleases({
|
||||
allReleases,
|
||||
project,
|
||||
}: {
|
||||
allReleases: GetAllReleasesResult;
|
||||
project: Project;
|
||||
}) {
|
||||
return allReleases.reduce(
|
||||
(
|
||||
acc: {
|
||||
unmatched: string[];
|
||||
releases: {
|
||||
[baseVersion: string]: {
|
||||
createdAt: string | null;
|
||||
candidates: string[];
|
||||
versions: string[];
|
||||
htmlUrl: string;
|
||||
};
|
||||
};
|
||||
},
|
||||
release,
|
||||
) => {
|
||||
const match =
|
||||
project.versioningStrategy === 'semver'
|
||||
? release.tagName.match(semverRegexp)
|
||||
: release.tagName.match(calverRegexp);
|
||||
|
||||
if (!match) {
|
||||
acc.unmatched.push(release.tagName);
|
||||
return acc;
|
||||
}
|
||||
|
||||
const prefix = match[1];
|
||||
const baseVersion =
|
||||
project.versioningStrategy === 'semver'
|
||||
? `${match[2]}.${match[3]}`
|
||||
: match[2];
|
||||
|
||||
if (!acc.releases[baseVersion]) {
|
||||
acc.releases[baseVersion] = {
|
||||
createdAt: release.createdAt,
|
||||
candidates: prefix === 'rc' ? [release.tagName] : [],
|
||||
versions: prefix === 'version' ? [release.tagName] : [],
|
||||
htmlUrl: release.htmlUrl,
|
||||
};
|
||||
return acc;
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{ unmatched: [], releases: {} },
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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 { getMappedReleases } from './getMappedReleases';
|
||||
|
||||
export function getSummary({
|
||||
mappedReleases,
|
||||
}: {
|
||||
mappedReleases: ReturnType<typeof getMappedReleases>;
|
||||
}) {
|
||||
return Object.entries(mappedReleases.releases).reduce(
|
||||
(
|
||||
acc: {
|
||||
totalReleases: number;
|
||||
totalCandidatePatches: number;
|
||||
totalVersionPatches: number;
|
||||
},
|
||||
[_baseVersion, mappedRelease],
|
||||
) => {
|
||||
acc.totalReleases += 1;
|
||||
acc.totalCandidatePatches += mappedRelease.candidates.length - 1;
|
||||
acc.totalVersionPatches += mappedRelease.versions.length - 1;
|
||||
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
totalReleases: 0,
|
||||
totalCandidatePatches: 0,
|
||||
totalVersionPatches: 0,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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 { calverRegexp } from '../../helpers/tagParts/getCalverTagParts';
|
||||
import { GetAllTagsResult } from '../../api/PluginApiClient';
|
||||
import { getMappedReleases } from './getMappedReleases';
|
||||
import { Project } from '../../contexts/ProjectContext';
|
||||
import { semverRegexp } from '../../helpers/tagParts/getSemverTagParts';
|
||||
|
||||
export function getTags({
|
||||
allTags,
|
||||
project,
|
||||
mappedReleases,
|
||||
}: {
|
||||
allTags: GetAllTagsResult;
|
||||
project: Project;
|
||||
mappedReleases: ReturnType<typeof getMappedReleases>;
|
||||
}) {
|
||||
return allTags.reduce(
|
||||
(acc: { unmatched: string[]; unmappable: string[] }, tag) => {
|
||||
const match =
|
||||
project.versioningStrategy === 'semver'
|
||||
? tag.tagName.match(semverRegexp)
|
||||
: tag.tagName.match(calverRegexp);
|
||||
|
||||
if (!match) {
|
||||
acc.unmatched.push(tag.tagName);
|
||||
return acc;
|
||||
}
|
||||
|
||||
const prefix = match[1];
|
||||
const baseVersion =
|
||||
project.versioningStrategy === 'semver'
|
||||
? `${match[2]}.${match[3]}`
|
||||
: match[2];
|
||||
|
||||
if (!mappedReleases.releases[baseVersion]) {
|
||||
acc.unmappable.push(tag.tagName);
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (
|
||||
prefix === 'rc' &&
|
||||
!mappedReleases.releases[baseVersion].candidates.includes(tag.tagName)
|
||||
) {
|
||||
mappedReleases.releases[baseVersion].candidates.push(tag.tagName);
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (
|
||||
prefix === 'version' &&
|
||||
!mappedReleases.releases[baseVersion].versions.includes(tag.tagName)
|
||||
) {
|
||||
mappedReleases.releases[baseVersion].versions.push(tag.tagName);
|
||||
return acc;
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{ unmatched: [], unmappable: [] },
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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 { useAsync } from 'react-use';
|
||||
|
||||
import { usePluginApiClientContext } from '../../../contexts/PluginApiClientContext';
|
||||
import { useProjectContext } from '../../../contexts/ProjectContext';
|
||||
|
||||
export const useGetStats = () => {
|
||||
const { pluginApiClient } = usePluginApiClientContext();
|
||||
const { project } = useProjectContext();
|
||||
|
||||
const stats = useAsync(async () => {
|
||||
const [allReleases, allTags] = await Promise.all([
|
||||
pluginApiClient.stats.getAllReleases({
|
||||
owner: project.owner,
|
||||
repo: project.repo,
|
||||
}),
|
||||
pluginApiClient.stats.getAllTags({
|
||||
owner: project.owner,
|
||||
repo: project.repo,
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
allReleases,
|
||||
allTags,
|
||||
};
|
||||
}, [project]);
|
||||
|
||||
return {
|
||||
stats,
|
||||
};
|
||||
};
|
||||
@@ -49,6 +49,10 @@ describe('testHelpers', () => {
|
||||
"promoteRc": Object {
|
||||
"promoteRelease": [MockFunction],
|
||||
},
|
||||
"stats": Object {
|
||||
"getAllReleases": [MockFunction],
|
||||
"getAllTags": [MockFunction],
|
||||
},
|
||||
},
|
||||
"mockBumpedTag": "rc-2020.01.01_1337",
|
||||
"mockCalverProject": Object {
|
||||
|
||||
@@ -278,4 +278,14 @@ export const mockApiClient: IPluginApiClient = {
|
||||
htmlUrl: 'mock_release_html_url',
|
||||
})),
|
||||
},
|
||||
|
||||
stats: {
|
||||
getAllTags: jest.fn(async () => {
|
||||
throw new Error('Not implemented');
|
||||
}),
|
||||
|
||||
getAllReleases: jest.fn(async () => {
|
||||
throw new Error('Not implemented');
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user