Skip to content

Commit

Permalink
feat(releasescard): add releases widget
Browse files Browse the repository at this point in the history
BREAKING CHANGE: GitlabCIApi type has the new method getReleaseSummary

Signed-off-by: Manuel Stein <[email protected]>
  • Loading branch information
manuelstein committed Mar 8, 2023
1 parent 6b094f0 commit 8f25374
Show file tree
Hide file tree
Showing 11 changed files with 1,411 additions and 4 deletions.
1,119 changes: 1,119 additions & 0 deletions packages/gitlab/dev/mock-gitlab/api-v4-v15.7.0.ts

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion packages/gitlab/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.61",
"dayjs": "^1.11.7",
"react-use": "^17.2.4"
"react-use": "^17.2.4",
"semver": "^7.3.8"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0",
Expand Down Expand Up @@ -72,6 +73,7 @@
"@testing-library/user-event": "^14.3.0",
"@types/jest": "^27.4.0",
"@types/node": "^18.13.0",
"@types/semver": "^7.3.12",
"cross-fetch": "^3.1.4",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
Expand Down
6 changes: 6 additions & 0 deletions packages/gitlab/src/api/GitlabCIApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
MergeRequest,
PipelineObject,
IssueObject,
ReleaseData,
} from '../components/types';

export interface PipelineSummary {
Expand All @@ -30,6 +31,10 @@ export interface IssuesSummary {
getIssuesData: IssueObject[];
}

export interface ReleasesSummary {
getReleasesData: ReleaseData[];
}

export const GitlabCIApiRef = createApiRef<GitlabCIBuilder>({
id: 'plugin.gitlabci.service',
});
Expand Down Expand Up @@ -67,6 +72,7 @@ export type GitlabCIApi = {
branch?: string,
filePath?: string
): Promise<PeopleCardEntityData[]>;
getReleasesSummary(projectID: string): Promise<ReleasesSummary | undefined>;

getContributorsLink(
projectWebUrl: string | undefined,
Expand Down
14 changes: 14 additions & 0 deletions packages/gitlab/src/api/GitlabCIClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
MergeRequest,
PipelineObject,
FileOwnership,
ReleaseData,
} from '../components/types';
import { parseCodeOwners } from '../components/utils';
import { IssueObject } from './../components/types';
Expand All @@ -15,6 +16,7 @@ import {
MergeRequestsStatusSummary,
MergeRequestsSummary,
PipelineSummary,
ReleasesSummary,
} from './GitlabCIApi';

export class GitlabCIClient implements GitlabCIApi {
Expand Down Expand Up @@ -254,6 +256,18 @@ export class GitlabCIClient implements GitlabCIApi {
};
}

async getReleasesSummary(
projectID: string
): Promise<ReleasesSummary | undefined> {
const releaseObjects = await this.callApi<ReleaseData[]>(
'projects/' + projectID + '/releases',
{}
);
return {
getReleasesData: releaseObjects!,
};
}

async getProjectDetails(projectSlug?: string): Promise<any | undefined> {
let projectDetails: any;
if (projectSlug) {
Expand Down
10 changes: 7 additions & 3 deletions packages/gitlab/src/components/GitlabCI/GitlabCI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,23 @@ import {
MergeRequestsTable,
MergeRequestStats,
PipelinesTable,
ReleasesCard,
} from '../widgets';

export const GitlabCI = () => (
<Page themeId="tool">
<Content>
<Grid container spacing={6} direction="row" alignItems="stretch">
<Grid item sm={12} md={6} lg={4}>
<Grid item sm={12} md={6} lg={6}>
<PeopleCard />
</Grid>
<Grid item sm={12} md={6} lg={4}>
<Grid item sm={12} md={6} lg={6}>
<LanguagesCard />
</Grid>
<Grid item sm={12} md={6} lg={4}>
<Grid item sm={12} md={6} lg={6}>
<ReleasesCard />
</Grid>
<Grid item sm={12} md={6} lg={6}>
<MergeRequestStats />
</Grid>
<Grid item md={12}>
Expand Down
7 changes: 7 additions & 0 deletions packages/gitlab/src/components/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
export type ReleaseData = {
tag_name: string;
description: string;
name: string;
created_at: string;
};

export type PeopleCardEntityData = {
name: string;
email?: string;
Expand Down
224 changes: 224 additions & 0 deletions packages/gitlab/src/components/widgets/ReleasesCard/ReleasesCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
/*
* Copyright 2022 Nokia
*
* 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
*
* https://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 { makeStyles } from '@material-ui/core/styles';
import { Link, Grid } from '@material-ui/core';
import LocalOfferOutlinedIcon from '@material-ui/icons/LocalOfferOutlined';
import Alert from '@material-ui/lab/Alert';
import { InfoCard, Progress } from '@backstage/core-components';
import { GitlabCIApiRef } from '../../../api';
import { useApi } from '@backstage/core-plugin-api';
import { useAsync } from 'react-use';
import {
gitlabProjectId,
gitlabProjectSlug,
gitlabInstance,
} from '../../gitlabAppData';
import { ReleaseData, ProjectDetail } from '../../types';
import { rcompare, valid, prerelease } from 'semver';

const useStyles = makeStyles((theme) => ({
infoCard: {
marginBottom: theme.spacing(3),
'& + .MuiAlert-root': {
marginTop: theme.spacing(3),
},
},
releaseTitle: {
...theme.typography.h6,
margin: 0,
marginRight: '0.5rem',
},
releaseTagIcon: {
verticalAlign: 'middle',
},
}));

/**
* Properties for {@link ReleasesCard}
*
* @public
*/
export interface ReleasesCardProps {
/**
* Filter
*
* all - show all releases (default)
* patch - major, minor, patch - no pre-releases
*
*/
show?: string;
/**
* Limit - show only a maximum number of releases
*/
limit?: number;
}

function makeFilter(
show: string
): (value: ReleaseData, index: number, array: ReleaseData[]) => boolean {
switch (show) {
case 'patch':
return (value: ReleaseData) => {
try {
return prerelease(value.tag_name) == null;
} catch (error) {
return true;
}
};
case 'all':
default:
return () => true;
}
}

/**
* ReleasesCard
*
* @public
*/
export const ReleasesCard = (props: ReleasesCardProps) => {
const { show = 'all', limit = 6 } = props;
const classes = useStyles();
const project_id = gitlabProjectId();
const project_slug = gitlabProjectSlug();
const gitlab_instance = gitlabInstance();

const GitlabCIAPI = useApi(GitlabCIApiRef).build(
gitlab_instance || '0'
);
/* TODO: to change the below logic to get contributors data*/
const { value, loading, error } = useAsync(async (): Promise<{
releases: ReleaseData[];
projectDetails: ProjectDetail;
}> => {
const projectDetails: any = await GitlabCIAPI.getProjectDetails(
project_slug || project_id
);
const projectDetailsData: ProjectDetail = {
project_web_url: projectDetails?.web_url,
project_default_branch: projectDetails?.default_branch,
};
const projectId = project_id || projectDetails?.id;
const gitlabObj = await GitlabCIAPI.getReleasesSummary(projectId);
const releaseData: ReleaseData[] | undefined =
gitlabObj?.getReleasesData;
return {
releases: releaseData!,
projectDetails: projectDetailsData!,
};
}, []);

const project_web_url = value?.projectDetails.project_web_url;

if (loading) {
return <Progress />;
} else if (error) {
return (
<Alert severity="error" className={classes.infoCard}>
{error.message}
</Alert>
);
}

// shortest prefix match to dedupe the list, i.e.
// prefer full releases, e.g. when v1.2.3 exists, we drop all pre-releases v1.2.3-*
// prefer the latest in a series of pre-releases
let releases: ReleaseData[] = [];
// sort (e.g. v1.2.3 comes before v1.2.3-alpha-4.5) and walk through list of releases
// NOTE: rcompare sorts descending, but full release comes before release candidates (v1.2.3 before v1.2.3-alpha-4.5)
// use string comparison for non-compliant tags
if (value?.releases != null) {
value?.releases
.filter(makeFilter(show))
.sort((a, b) => {
try {
return rcompare(a.tag_name, b.tag_name);
} catch (error) {
return a.tag_name > b.tag_name ? 1 : -1;
}
})
.forEach((release) => {
// always add invalid release names
if (!valid(release.tag_name)) {
releases.push(release);
return;
}
// see if there's already a release whose tag_name is a prefix of the current item's tag_name
const idx = releases.findIndex((value) => {
return release.tag_name.startsWith(value.tag_name);
});
// if no item was found, the version is not yet in the list
if (idx < 0) releases.push(release);
});
}
// sort the remaining releases in descending order (latest release first)
releases = releases
.sort((a, b) => {
try {
return rcompare(a.tag_name, b.tag_name);
} catch (error) {
return a.tag_name > b.tag_name ? -1 : 1;
}
})
.slice(0, limit);

if (releases.length == 0) {
return <></>;
}

return (
<InfoCard
title="Releases"
deepLink={{
link: `${project_web_url}/-/releases`,
title: 'go to Releases',
onClick: (e) => {
e.preventDefault();
window.open(`${project_web_url}/-/releases`);
},
}}
className={classes.infoCard}
>
<Grid container spacing={2} justifyContent="flex-start">
{releases.map((release: ReleaseData) => (
<Grid item>
<Link
href={
project_web_url +
'/-/releases/' +
release.tag_name
}
color="inherit"
target="_blank"
rel="noopener noreferrer"
>
<div className={classes.releaseTitle}>
{release.name}
</div>
<LocalOfferOutlinedIcon
fontSize="inherit"
className={classes.releaseTagIcon}
/>
{release.tag_name}
</Link>
</Grid>
))}
</Grid>
</InfoCard>
);
};
17 changes: 17 additions & 0 deletions packages/gitlab/src/components/widgets/ReleasesCard/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright 2022 Nokia
*
* 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
*
* https://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.
*/

export { ReleasesCard } from './ReleasesCard';
1 change: 1 addition & 0 deletions packages/gitlab/src/components/widgets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export { PeopleCard } from './PeopleCard';
export { MergeRequestStats } from './MergeRequestStats';
export { MergeRequestsTable } from './MergeRequestsTable';
export { PipelinesTable } from './PipelinesTable';
export { ReleasesCard } from './ReleasesCard';
export { IssuesTable } from './IssuesTable';
1 change: 1 addition & 0 deletions packages/gitlab/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export {
EntityGitlabMergeRequestsTable,
EntityGitlabMergeRequestStatsCard,
EntityGitlabPipelinesTable,
EntityGitlabReleasesCard,
gitlabPlugin,
} from './plugin';
export * from './api';
Expand Down
12 changes: 12 additions & 0 deletions packages/gitlab/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,18 @@ export const EntityGitlabPipelinesTable = gitlabPlugin.provide(
})
);

export const EntityGitlabReleasesCard = gitlabPlugin.provide(
createComponentExtension({
name: 'EntityGitlabReleasesCard',
component: {
lazy: () =>
import('./components/widgets/index').then(
(m) => m.ReleasesCard
),
},
})
);

export const EntityGitlabIssuesTable = gitlabPlugin.provide(
createComponentExtension({
name: 'EntityGitlabIssuesTable',
Expand Down

0 comments on commit 8f25374

Please sign in to comment.