From 6691570ec105091326e7696590dc903ef04b9439 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Wed, 14 Apr 2021 23:20:42 +0200 Subject: [PATCH] Replace root component props with input fields Signed-off-by: Erik Engervall --- plugins/github-release-manager/package.json | 1 + .../src/GitHubReleaseManager.tsx | 135 ++++++++++-------- .../src/api/PluginApiClient.ts | 49 ++++++- .../src/cards/patchRc/PatchBody.tsx | 4 +- .../src/cards/projectForm/Owner.tsx | 85 +++++++++++ .../src/cards/projectForm/Repo.tsx | 90 ++++++++++++ .../src/cards/projectForm/RepoDetailsForm.tsx | 66 +++++++++ .../cards/projectForm/VersioningStrategy.tsx | 62 ++++++++ .../src/cards/projectForm/isProjectValid.tsx | 25 ++++ .../src/cards/projectForm/styles.ts | 29 ++++ .../components/CenteredCircularProgress.tsx | 26 ++++ .../ResponseStepList/ResponseStepList.tsx | 4 +- .../src/helpers/tagParts/getCalverTagParts.ts | 6 +- .../src/helpers/tagParts/getSemverTagParts.ts | 5 + .../src/helpers/tagParts/getTagParts.test.ts | 8 ++ 15 files changed, 526 insertions(+), 69 deletions(-) create mode 100644 plugins/github-release-manager/src/cards/projectForm/Owner.tsx create mode 100644 plugins/github-release-manager/src/cards/projectForm/Repo.tsx create mode 100644 plugins/github-release-manager/src/cards/projectForm/RepoDetailsForm.tsx create mode 100644 plugins/github-release-manager/src/cards/projectForm/VersioningStrategy.tsx create mode 100644 plugins/github-release-manager/src/cards/projectForm/isProjectValid.tsx create mode 100644 plugins/github-release-manager/src/cards/projectForm/styles.ts create mode 100644 plugins/github-release-manager/src/components/CenteredCircularProgress.tsx diff --git a/plugins/github-release-manager/package.json b/plugins/github-release-manager/package.json index 5eebc11105..ec067920e4 100644 --- a/plugins/github-release-manager/package.json +++ b/plugins/github-release-manager/package.json @@ -29,6 +29,7 @@ "@octokit/rest": "^18.0.12", "luxon": "^1.26.0", "react-dom": "^16.13.1", + "react-hook-form": "^6.6.0", "react-router": "6.0.0-beta.0", "react-use": "^15.3.3", "react": "^16.13.1" diff --git a/plugins/github-release-manager/src/GitHubReleaseManager.tsx b/plugins/github-release-manager/src/GitHubReleaseManager.tsx index 83ccab5dd1..3f7887ad53 100644 --- a/plugins/github-release-manager/src/GitHubReleaseManager.tsx +++ b/plugins/github-release-manager/src/GitHubReleaseManager.tsx @@ -15,10 +15,11 @@ */ import { Alert } from '@material-ui/lab'; -import { CircularProgress, makeStyles } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core'; import { useAsync } from 'react-use'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { useApi, ContentHeader, ErrorBoundary } from '@backstage/core'; +import { useForm } from 'react-hook-form'; import { CreateRc } from './cards/createRc/CreateRc'; import { getGitHubBatchInfo } from './sideEffects/getGitHubBatchInfo'; @@ -35,11 +36,11 @@ import { PluginApiClientContext, usePluginApiClientContext, } from './contexts/PluginApiClientContext'; -import { - ProjectContext, - useProjectContext, - Project, -} from './contexts/ProjectContext'; +import { ProjectContext, Project } from './contexts/ProjectContext'; +import { isProjectValid } from './cards/projectForm/isProjectValid'; +import { InfoCardPlus } from './components/InfoCardPlus'; +import { RepoDetailsForm } from './cards/projectForm/RepoDetailsForm'; +import { CenteredCircularProgress } from './components/CenteredCircularProgress'; interface GitHubReleaseManagerProps { components?: { @@ -62,30 +63,54 @@ export function GitHubReleaseManager({ }: GitHubReleaseManagerProps) { const pluginApiClient = useApi(githubReleaseManagerApiRef); const classes = useStyles(); + const usernameResponse = useAsync(() => pluginApiClient.getUsername()); + const { control, watch } = useForm(); + const project: Project = watch('repo-details-form'); - const project: Project = { - owner: 'erikengervall', - repo: 'playground', - versioningStrategy: 'semver', - }; + if (usernameResponse.error) { + return {usernameResponse.error.message}; + } + + if (usernameResponse.loading) { + return ; + } + + if (!usernameResponse.value?.username) { + return Unable to retrieve username; + } return ( - - {/* @ts-ignore-error TODO: Update interface for PluginApiClient */} - -
- + +
+ - -
-
- + + + + + {isProjectValid(project) && ( + + )} +
+
); } -function Cards({ components }: GitHubReleaseManagerProps) { +function Cards({ + components, + project, +}: { + components: GitHubReleaseManagerProps['components']; + project: Project; +}) { const pluginApiClient = usePluginApiClientContext(); - const project = useProjectContext(); const [refetch, setRefetch] = useState(0); const gitHubBatchInfo = useAsync( getGitHubBatchInfo({ project, pluginApiClient }), @@ -97,11 +122,7 @@ function Cards({ components }: GitHubReleaseManagerProps) { } if (gitHubBatchInfo.loading) { - return ( -
- -
- ); + return ; } if (gitHubBatchInfo.value === undefined) { @@ -120,38 +141,40 @@ function Cards({ components }: GitHubReleaseManagerProps) { } return ( - - - - {components?.default?.createRc?.omit !== true && ( - + + - )} - {components?.default?.promoteRc?.omit !== true && ( - - )} + {components?.default?.createRc?.omit !== true && ( + + )} - {components?.default?.patch?.omit !== true && ( - - )} - + {components?.default?.promoteRc?.omit !== true && ( + + )} + + {components?.default?.patch?.omit !== true && ( + + )} + +
); } diff --git a/plugins/github-release-manager/src/api/PluginApiClient.ts b/plugins/github-release-manager/src/api/PluginApiClient.ts index 0615221c77..c0a6476b2b 100644 --- a/plugins/github-release-manager/src/api/PluginApiClient.ts +++ b/plugins/github-release-manager/src/api/PluginApiClient.ts @@ -157,10 +157,13 @@ export interface IPluginApiClient { } & PartialProject, ) => Promise; }; + + getOrganizations: (args: { ownerIsUser: boolean }) => Promise; + getUsername: () => Promise<{ username: string }>; + getRepositories: (args: { owner: string; username: string }) => Promise; } export class PluginApiClient implements IPluginApiClient { - // private readonly getAccessToken: any; private readonly githubAuthApi: OAuthApi; private readonly baseUrl: string; readonly host: string; @@ -174,8 +177,6 @@ export class PluginApiClient implements IPluginApiClient { }) { this.githubAuthApi = githubAuthApi; - // this.getAccessToken = () => this.githubAuthApi.getAccessToken(); - const githubIntegrationConfig = this.getGithubIntegrationConfig({ configApi, }); @@ -190,11 +191,13 @@ export class PluginApiClient implements IPluginApiClient { configApi.getOptionalConfigArray('integrations.github') ?? [], ); - const githubIntegrationConfig = configs.find( - v => v.host === 'github.com' || v.host.startsWith('ghe.'), + const githubIntegrationEnterpriseConfig = configs.find(v => + v.host.startsWith('ghe.'), ); + const githubIntegrationConfig = configs.find(v => v.host === 'github.com'); - return githubIntegrationConfig; + // Prioritize enterprise configs if available + return githubIntegrationEnterpriseConfig ?? githubIntegrationConfig; } private async getOctokit() { @@ -216,6 +219,40 @@ export class PluginApiClient implements IPluginApiClient { return `${owner}/${repo}`; } + async getOrganizations() { + const { octokit } = await this.getOctokit(); + const { data: orgs } = await octokit.orgs.listForAuthenticatedUser(); + + return { orgs }; + } + + async getRepositories({ + owner, + username, + }: { + owner: string; + username: string; + }) { + const { octokit } = await this.getOctokit(); + + if (owner === username) { + const { data: repos } = await octokit.repos.listForUser({ username }); + + return { repos }; + } + + const { data: repos } = await octokit.repos.listForOrg({ org: owner }); + + return { repos }; + } + + async getUsername() { + const { octokit } = await this.getOctokit(); + const { data: user } = await octokit.users.getAuthenticated(); + + return { username: user.login }; + } + async getRecentCommits({ owner, repo, diff --git a/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx b/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx index 22edde9ad9..7eea133827 100644 --- a/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx +++ b/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx @@ -20,7 +20,6 @@ import { useAsync, useAsyncFn } from 'react-use'; import { Button, Checkbox, - CircularProgress, IconButton, Link, List, @@ -50,6 +49,7 @@ import { useStyles } from '../../styles/styles'; import { TEST_IDS } from '../../test-helpers/test-ids'; import { patch } from './sideEffects/patch'; import { useProjectContext } from '../../contexts/ProjectContext'; +import { CenteredCircularProgress } from '../../components/CenteredCircularProgress'; interface PatchBodyProps { bumpedTag: string; @@ -124,7 +124,7 @@ export const PatchBody = ({ return {patchReleaseResponse.error.message}; } if (githubDataResponse.loading) { - return ; + return ; } function Description() { diff --git a/plugins/github-release-manager/src/cards/projectForm/Owner.tsx b/plugins/github-release-manager/src/cards/projectForm/Owner.tsx new file mode 100644 index 0000000000..f95d66159f --- /dev/null +++ b/plugins/github-release-manager/src/cards/projectForm/Owner.tsx @@ -0,0 +1,85 @@ +/* + * 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 { useAsync } from 'react-use'; +import { ControllerRenderProps } from 'react-hook-form'; +import { Alert } from '@material-ui/lab'; +import { FormControl, InputLabel, MenuItem, Select } from '@material-ui/core'; + +import { usePluginApiClientContext } from '../../contexts/PluginApiClientContext'; +import { useFormClasses } from './styles'; +import { CenteredCircularProgress } from '../../components/CenteredCircularProgress'; +import { Project } from '../../contexts/ProjectContext'; + +export function Owner({ + controllerRenderProps, + username, +}: { + controllerRenderProps: ControllerRenderProps; + username: string; +}) { + const pluginApiClient = usePluginApiClientContext(); + const formClasses = useFormClasses(); + const project: Project = controllerRenderProps.value; + + const { loading, error, value } = useAsync(() => + pluginApiClient.getOrganizations(), + ); + + if (error) { + return {error.message}; + } + + if (loading) { + return ; + } + + if (!value?.orgs) { + return Could not fetch organizations; + } + + return ( + + Organizations + + + ); +} diff --git a/plugins/github-release-manager/src/cards/projectForm/Repo.tsx b/plugins/github-release-manager/src/cards/projectForm/Repo.tsx new file mode 100644 index 0000000000..9c83c30565 --- /dev/null +++ b/plugins/github-release-manager/src/cards/projectForm/Repo.tsx @@ -0,0 +1,90 @@ +/* + * 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 { useAsync } from 'react-use'; +import { FormControl, InputLabel, Select, MenuItem } from '@material-ui/core'; +import { Alert } from '@material-ui/lab'; +import { ControllerRenderProps, useForm } from 'react-hook-form'; + +import { usePluginApiClientContext } from '../../contexts/PluginApiClientContext'; +import { useFormClasses } from './styles'; +import { CenteredCircularProgress } from '../../components/CenteredCircularProgress'; +import { Project } from '../../contexts/ProjectContext'; + +export function Repo({ + username, + controllerRenderProps, +}: { + username: string; + controllerRenderProps: ControllerRenderProps; +}) { + const pluginApiClient = usePluginApiClientContext(); + const formClasses = useFormClasses(); + const project: Project = controllerRenderProps.value; + + const { loading, error, value } = useAsync( + () => + pluginApiClient.getRepositories({ + owner: project.owner, + username, + }), + [project.owner], + ); + + if (error) { + return {error.message}; + } + + if (loading) { + return ; + } + + if (!value?.repos) { + return ( + + Could not fetch repositories for "{project.owner}" + + ); + } + + return ( + + Repositories + + + ); +} diff --git a/plugins/github-release-manager/src/cards/projectForm/RepoDetailsForm.tsx b/plugins/github-release-manager/src/cards/projectForm/RepoDetailsForm.tsx new file mode 100644 index 0000000000..03996b484d --- /dev/null +++ b/plugins/github-release-manager/src/cards/projectForm/RepoDetailsForm.tsx @@ -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 { Controller, useForm } from 'react-hook-form'; +import { Project } from '../../contexts/ProjectContext'; + +import { VersioningStrategy } from './VersioningStrategy'; +import { Owner } from './Owner'; +import { Repo } from './Repo'; + +export function RepoDetailsForm({ + control, + username, +}: { + control: ReturnType['control']; + username: string; +}) { + return ( + { + const project: Project = controllerRenderProps.value; + + return ( + <> + + + + + {project.owner.length > 0 && ( + + )} + + ); + }} + control={control} + name="repo-details-form" + defaultValue={ + { + owner: '', + repo: '', + versioningStrategy: 'semver', + } as Project + } + /> + ); +} diff --git a/plugins/github-release-manager/src/cards/projectForm/VersioningStrategy.tsx b/plugins/github-release-manager/src/cards/projectForm/VersioningStrategy.tsx new file mode 100644 index 0000000000..b6fa367b00 --- /dev/null +++ b/plugins/github-release-manager/src/cards/projectForm/VersioningStrategy.tsx @@ -0,0 +1,62 @@ +/* + * 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 { + FormControl, + FormControlLabel, + FormLabel, + Radio, + RadioGroup, +} from '@material-ui/core'; +import React from 'react'; +import { ControllerRenderProps } from 'react-hook-form'; +import { Project } from '../../contexts/ProjectContext'; + +export function VersioningStrategy({ + controllerRenderProps, +}: { + controllerRenderProps: ControllerRenderProps; +}) { + const project: Project = controllerRenderProps.value; + + return ( + + Calendar strategy + { + controllerRenderProps.onChange({ + ...project, + versioningStrategy: event.target.value, + } as Project); + }} + > + } + label="Semantic versioning" + /> + } + label="Calendar versioning" + /> + + + ); +} diff --git a/plugins/github-release-manager/src/cards/projectForm/isProjectValid.tsx b/plugins/github-release-manager/src/cards/projectForm/isProjectValid.tsx new file mode 100644 index 0000000000..dd6e9871bb --- /dev/null +++ b/plugins/github-release-manager/src/cards/projectForm/isProjectValid.tsx @@ -0,0 +1,25 @@ +/* + * 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 { Project } from '../../contexts/ProjectContext'; + +export function isProjectValid(project: any): project is Project { + return ( + project?.owner?.length > 0 && + project?.repo?.length > 0 && + project?.versioningStrategy?.length > 0 + ); +} diff --git a/plugins/github-release-manager/src/cards/projectForm/styles.ts b/plugins/github-release-manager/src/cards/projectForm/styles.ts new file mode 100644 index 0000000000..274d0523ac --- /dev/null +++ b/plugins/github-release-manager/src/cards/projectForm/styles.ts @@ -0,0 +1,29 @@ +/* + * 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 { createStyles, makeStyles, Theme } from '@material-ui/core'; + +export const useFormClasses = makeStyles((theme: Theme) => + createStyles({ + formControl: { + margin: theme.spacing(1), + minWidth: 120, + }, + selectEmpty: { + marginTop: theme.spacing(2), + }, + }), +); diff --git a/plugins/github-release-manager/src/components/CenteredCircularProgress.tsx b/plugins/github-release-manager/src/components/CenteredCircularProgress.tsx new file mode 100644 index 0000000000..83b6314021 --- /dev/null +++ b/plugins/github-release-manager/src/components/CenteredCircularProgress.tsx @@ -0,0 +1,26 @@ +/* + * 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 { CircularProgress } from '@material-ui/core'; + +export const CenteredCircularProgress = () => { + return ( +
+ +
+ ); +}; diff --git a/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.tsx b/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.tsx index ab545153f7..29b8b13dac 100644 --- a/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.tsx +++ b/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.tsx @@ -17,7 +17,6 @@ import React, { PropsWithChildren } from 'react'; import { List, - CircularProgress, Button, Dialog, DialogActions, @@ -28,6 +27,7 @@ import { import { ResponseStep, SetRefetch } from '../../types/types'; import { TEST_IDS } from '../../test-helpers/test-ids'; import { ResponseStepListItem } from './ResponseStepListItem'; +import { CenteredCircularProgress } from '../CenteredCircularProgress'; interface ResponseStepListProps { responseSteps?: ResponseStep[]; @@ -68,7 +68,7 @@ export const ResponseStepList = ({ {loading || !responseSteps ? (
-
diff --git a/plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.ts b/plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.ts index 27fe59844a..0fdd31444e 100644 --- a/plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.ts +++ b/plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.ts @@ -22,10 +22,10 @@ export type CalverTagParts = { patch: number; }; +export const calverRegexp = /(rc|version)-([0-9]{4}\.[0-9]{2}\.[0-9]{2})_([0-9]+)/; + export function getCalverTagParts(tag: string) { - const result = tag.match( - /(rc|version)-([0-9]{4}\.[0-9]{2}\.[0-9]{2})_([0-9]+)/, - ); + const result = tag.match(calverRegexp); if (result === null || result.length < 4) { throw new GitHubReleaseManagerError('Invalid calver tag'); diff --git a/plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.ts b/plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.ts index eabf1c0a7e..018dedcee0 100644 --- a/plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.ts +++ b/plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.ts @@ -15,6 +15,7 @@ */ import { GitHubReleaseManagerError } from '../../errors/GitHubReleaseManagerError'; +import { calverRegexp } from './getCalverTagParts'; export type SemverTagParts = { prefix: string; @@ -30,6 +31,10 @@ export function getSemverTagParts(tag: string) { throw new GitHubReleaseManagerError('Invalid semver tag'); } + if (tag.match(calverRegexp)) { + throw new GitHubReleaseManagerError('Invalid semver tag, found calver'); + } + const tagParts: SemverTagParts = { prefix: result[1], major: parseInt(result[2], 10), diff --git a/plugins/github-release-manager/src/helpers/tagParts/getTagParts.test.ts b/plugins/github-release-manager/src/helpers/tagParts/getTagParts.test.ts index 7493887d15..91a753dbe3 100644 --- a/plugins/github-release-manager/src/helpers/tagParts/getTagParts.test.ts +++ b/plugins/github-release-manager/src/helpers/tagParts/getTagParts.test.ts @@ -109,5 +109,13 @@ describe('getTagParts', () => { getTagParts({ project: mockSemverProject, tag: 'rc-1.2' }), ).toThrowErrorMatchingInlineSnapshot(`"Invalid semver tag"`); }); + + it('should throw for invalid semver (founds calver)', () => { + expect(() => + getTagParts({ project: mockSemverProject, tag: 'rc-1337.01.01_1' }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid semver tag, found calver"`, + ); + }); }); });