Replace react-use-form with URL query params

Introduce RefetchContext instead of passing setRefresh throughout entire project

Signed-off-by: Erik Engervall <erik.engervall@gmail.com>
This commit is contained in:
Erik Engervall
2021-04-16 16:17:44 +02:00
parent eddea6f124
commit 1f6b4a0bd6
28 changed files with 492 additions and 378 deletions
+15 -2
View File
@@ -22,14 +22,27 @@ import {
GitHubReleaseManagerPage,
} from '../src/plugin';
function DevWrapper({ children }: { children: React.ReactNode }) {
return <div style={{ padding: 30 }}>{children}</div>;
}
createDevApp()
.registerPlugin(gitHubReleaseManagerPlugin)
.addPage({
title: 'Page 1',
element: <GitHubReleaseManagerPage />,
element: (
<DevWrapper>
<GitHubReleaseManagerPage />
</DevWrapper>
),
})
.addPage({
title: 'Page 2',
element: <GitHubReleaseManagerPage />,
element: (
<DevWrapper>
{' '}
<GitHubReleaseManagerPage />
</DevWrapper>
),
})
.render();
+1 -1
View File
@@ -28,8 +28,8 @@
"@material-ui/lab": "4.0.0-alpha.45",
"@octokit/rest": "^18.0.12",
"luxon": "^1.26.0",
"qs": "^6.10.1",
"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"
@@ -16,7 +16,6 @@
import React, { useState } from 'react';
import { useAsync } from 'react-use';
import { useForm } from 'react-hook-form';
import { Alert } from '@material-ui/lab';
import { makeStyles } from '@material-ui/core';
import { useApi, ContentHeader, ErrorBoundary } from '@backstage/core';
@@ -42,6 +41,9 @@ import { InfoCardPlus } from './components/InfoCardPlus';
import { RepoDetailsForm } from './cards/projectForm/RepoDetailsForm';
import { CenteredCircularProgress } from './components/CenteredCircularProgress';
import { useVersioningStrategyMatchesRepoTags } from './helpers/useVersioningStrategyMatchesRepoTags';
import { useQuery } from './helpers/useQuery';
import { getParsedQuery } from './helpers/getNewQueryParams';
import { RefetchContext } from './contexts/RefetchContext';
interface GitHubReleaseManagerProps {
components?: {
@@ -65,8 +67,14 @@ export function GitHubReleaseManager({
const pluginApiClient = useApi(githubReleaseManagerApiRef);
const classes = useStyles();
const usernameResponse = useAsync(() => pluginApiClient.getUsername());
const { control, watch } = useForm();
const project: Project = watch('repo-details-form');
const query = useQuery();
const parsedQuery = getParsedQuery({ query });
const project: Project = {
owner: parsedQuery.owner ?? '',
repo: parsedQuery.repo ?? '',
versioningStrategy: parsedQuery.versioningStrategy ?? 'semver',
};
if (usernameResponse.error) {
return <Alert severity="error">{usernameResponse.error.message}</Alert>;
@@ -87,8 +95,8 @@ export function GitHubReleaseManager({
<InfoCardPlus>
<RepoDetailsForm
control={control}
username={usernameResponse.value.username}
project={project}
/>
</InfoCardPlus>
@@ -108,10 +116,10 @@ function Cards({
project: Project;
}) {
const pluginApiClient = usePluginApiClientContext();
const [refetch, setRefetch] = useState(0);
const [refetchTrigger, setRefetchTrigger] = useState(0);
const gitHubBatchInfo = useAsync(
getGitHubBatchInfo({ project, pluginApiClient }),
[project, refetch],
[project, refetchTrigger],
);
const { versioningStrategyMatches } = useVersioningStrategyMatchesRepoTags({
@@ -121,7 +129,12 @@ function Cards({
});
if (gitHubBatchInfo.error) {
return <Alert severity="error">{gitHubBatchInfo.error.message}</Alert>;
return (
<Alert severity="error">
Error occured while fetching information for "{project.owner}/
{project.repo}" ({gitHubBatchInfo.error.message})
</Alert>
);
}
if (gitHubBatchInfo.loading) {
@@ -154,39 +167,38 @@ function Cards({
return (
<ProjectContext.Provider value={project}>
<ErrorBoundary>
<Info
latestRelease={gitHubBatchInfo.value.latestRelease}
releaseBranch={gitHubBatchInfo.value.releaseBranch}
/>
{components?.default?.createRc?.omit !== true && (
<CreateRc
<RefetchContext.Provider value={{ refetchTrigger, setRefetchTrigger }}>
<ErrorBoundary>
<Info
latestRelease={gitHubBatchInfo.value.latestRelease}
releaseBranch={gitHubBatchInfo.value.releaseBranch}
defaultBranch={gitHubBatchInfo.value.repository.defaultBranch}
setRefetch={setRefetch}
successCb={components?.default?.createRc?.successCb}
/>
)}
{components?.default?.promoteRc?.omit !== true && (
<PromoteRc
latestRelease={gitHubBatchInfo.value.latestRelease}
setRefetch={setRefetch}
successCb={components?.default?.promoteRc?.successCb}
/>
)}
{components?.default?.createRc?.omit !== true && (
<CreateRc
latestRelease={gitHubBatchInfo.value.latestRelease}
releaseBranch={gitHubBatchInfo.value.releaseBranch}
defaultBranch={gitHubBatchInfo.value.repository.defaultBranch}
successCb={components?.default?.createRc?.successCb}
/>
)}
{components?.default?.patch?.omit !== true && (
<Patch
latestRelease={gitHubBatchInfo.value.latestRelease}
releaseBranch={gitHubBatchInfo.value.releaseBranch}
setRefetch={setRefetch}
successCb={components?.default?.patch?.successCb}
/>
)}
</ErrorBoundary>
{components?.default?.promoteRc?.omit !== true && (
<PromoteRc
latestRelease={gitHubBatchInfo.value.latestRelease}
successCb={components?.default?.promoteRc?.successCb}
/>
)}
{components?.default?.patch?.omit !== true && (
<Patch
latestRelease={gitHubBatchInfo.value.latestRelease}
releaseBranch={gitHubBatchInfo.value.releaseBranch}
successCb={components?.default?.patch?.successCb}
/>
)}
</ErrorBoundary>
</RefetchContext.Provider>
</ProjectContext.Provider>
);
}
@@ -40,7 +40,7 @@ export interface IPluginApiClient {
getRepoPath: (args: PartialProject) => string;
getOrganizations: () => Promise<{ organizations: string[] }>;
getOwners: () => Promise<{ owners: string[] }>;
getRepositories: (args: {
owner: string;
@@ -50,8 +50,8 @@ export interface IPluginApiClient {
getRecentCommits: (
args: { releaseBranchName?: string } & PartialProject,
) => Promise<{
recentCommits: {
) => Promise<
{
htmlUrl: string;
sha: string;
author: {
@@ -62,8 +62,8 @@ export interface IPluginApiClient {
message: string;
};
firstParentSha?: string;
}[];
}>;
}[]
>;
getLatestRelease: (
args: PartialProject,
@@ -154,7 +154,7 @@ export interface IPluginApiClient {
tagParts: SemverTagParts | CalverTagParts;
releaseBranchTree: string;
selectedPatchCommit: UnboxArray<
ApiMethodRetval<IPluginApiClient['getRecentCommits']>['recentCommits']
ApiMethodRetval<IPluginApiClient['getRecentCommits']>
>;
} & PartialProject,
) => Promise<{
@@ -191,7 +191,7 @@ export interface IPluginApiClient {
args: {
bumpedTag: string;
selectedPatchCommit: UnboxArray<
ApiMethodRetval<IPluginApiClient['getRecentCommits']>['recentCommits']
ApiMethodRetval<IPluginApiClient['getRecentCommits']>
>;
mergeTree: string;
releaseBranchSha: string;
@@ -247,7 +247,7 @@ export interface IPluginApiClient {
>;
tagParts: SemverTagParts | CalverTagParts;
selectedPatchCommit: UnboxArray<
ApiMethodRetval<IPluginApiClient['getRecentCommits']>['recentCommits']
ApiMethodRetval<IPluginApiClient['getRecentCommits']>
>;
} & PartialProject,
) => Promise<{
@@ -273,6 +273,12 @@ export interface IPluginApiClient {
};
}
const DISABLE_CACHE = {
headers: {
'If-None-Match': '',
},
};
export class PluginApiClient implements IPluginApiClient {
private readonly githubAuthApi: OAuthApi;
private readonly baseUrl: string;
@@ -329,7 +335,7 @@ export class PluginApiClient implements IPluginApiClient {
return `${owner}/${repo}`;
}
async getOrganizations() {
async getOwners() {
const { octokit } = await this.getOctokit();
const orgListResponse = await octokit.paginate(
octokit.orgs.listForAuthenticatedUser,
@@ -337,7 +343,7 @@ export class PluginApiClient implements IPluginApiClient {
);
return {
organizations: orgListResponse.map(organization => organization.login),
owners: orgListResponse.map(organization => organization.login),
};
}
@@ -385,22 +391,21 @@ export class PluginApiClient implements IPluginApiClient {
owner,
repo,
...(releaseBranchName ? { sha: releaseBranchName } : {}),
...DISABLE_CACHE,
});
return {
recentCommits: recentCommitsResponse.data.map(commit => ({
htmlUrl: commit.html_url,
sha: commit.sha,
author: {
htmlUrl: commit.author?.html_url,
login: commit.author?.login,
},
commit: {
message: commit.commit.message,
},
firstParentSha: commit.parents?.[0].sha,
})),
};
return recentCommitsResponse.data.map(commit => ({
htmlUrl: commit.html_url,
sha: commit.sha,
author: {
htmlUrl: commit.author?.html_url,
login: commit.author?.login,
},
commit: {
message: commit.commit.message,
},
firstParentSha: commit.parents?.[0]?.sha,
}));
}
async getLatestRelease({ owner, repo }: PartialProject) {
@@ -409,6 +414,7 @@ export class PluginApiClient implements IPluginApiClient {
owner,
repo,
per_page: 1,
...DISABLE_CACHE,
});
if (latestReleases.length === 0) {
@@ -433,10 +439,10 @@ export class PluginApiClient implements IPluginApiClient {
async getRepository({ owner, repo }: PartialProject) {
const { octokit } = await this.getOctokit();
const { data: repository } = await octokit.repos.get({
owner,
repo,
...DISABLE_CACHE,
});
return {
@@ -458,6 +464,7 @@ export class PluginApiClient implements IPluginApiClient {
owner,
repo,
ref: defaultBranch,
...DISABLE_CACHE,
});
return {
@@ -480,6 +487,7 @@ export class PluginApiClient implements IPluginApiClient {
owner,
repo,
branch: branchName,
...DISABLE_CACHE,
});
return {
@@ -585,7 +593,7 @@ export class PluginApiClient implements IPluginApiClient {
tagParts: SemverTagParts | CalverTagParts;
releaseBranchTree: string;
selectedPatchCommit: UnboxArray<
ApiMethodRetval<IPluginApiClient['getRecentCommits']>['recentCommits']
ApiMethodRetval<IPluginApiClient['getRecentCommits']>
>;
} & PartialProject) => {
const { octokit } = await this.getOctokit();
@@ -659,7 +667,7 @@ export class PluginApiClient implements IPluginApiClient {
}: {
bumpedTag: string;
selectedPatchCommit: UnboxArray<
ApiMethodRetval<IPluginApiClient['getRecentCommits']>['recentCommits']
ApiMethodRetval<IPluginApiClient['getRecentCommits']>
>;
mergeTree: string;
releaseBranchSha: string;
@@ -773,7 +781,7 @@ export class PluginApiClient implements IPluginApiClient {
>;
tagParts: SemverTagParts | CalverTagParts;
selectedPatchCommit: UnboxArray<
ApiMethodRetval<IPluginApiClient['getRecentCommits']>['recentCommits']
ApiMethodRetval<IPluginApiClient['getRecentCommits']>
>;
} & PartialProject) => {
const { octokit } = await this.getOctokit();
@@ -47,7 +47,6 @@ describe('CreateRc', () => {
<CreateRc
defaultBranch="mockDefaultBranch"
latestRelease={mockRcRelease}
setRefetch={jest.fn()}
releaseBranch={mockReleaseBranch}
/>,
);
@@ -62,7 +61,6 @@ describe('CreateRc', () => {
<CreateRc
defaultBranch="mockDefaultBranch"
latestRelease={mockReleaseVersion}
setRefetch={jest.fn()}
releaseBranch={mockReleaseBranch}
/>,
);
@@ -30,7 +30,7 @@ import { createRc } from './sideEffects/createRc';
import { Differ } from '../../components/Differ';
import { getRcGitHubInfo } from './getRcGitHubInfo';
import { InfoCardPlus } from '../../components/InfoCardPlus';
import { ComponentConfigCreateRc, SetRefetch } from '../../types/types';
import { ComponentConfigCreateRc } from '../../types/types';
import { ResponseStepList } from '../../components/ResponseStepList/ResponseStepList';
import { SEMVER_PARTS } from '../../constants/constants';
import { TEST_IDS } from '../../test-helpers/test-ids';
@@ -47,7 +47,6 @@ interface CreateRcProps {
IPluginApiClient['getLatestRelease']
>['latestRelease'];
releaseBranch: ApiMethodRetval<IPluginApiClient['getBranch']> | null;
setRefetch: SetRefetch;
successCb?: ComponentConfigCreateRc['successCb'];
}
@@ -55,7 +54,6 @@ export const CreateRc = ({
defaultBranch,
latestRelease,
releaseBranch,
setRefetch,
successCb,
}: CreateRcProps) => {
const pluginApiClient = usePluginApiClientContext();
@@ -149,7 +147,6 @@ export const CreateRc = ({
responseSteps={createGitHubReleaseResponse.value}
loading={createGitHubReleaseResponse.loading}
title="Create RC result"
setRefetch={setRefetch}
/>
);
}
@@ -32,11 +32,7 @@ import { Patch } from './Patch';
describe('Patch', () => {
it('should return early if no latestRelease exists', () => {
const { getByTestId } = render(
<Patch
latestRelease={null}
setRefetch={jest.fn()}
releaseBranch={mockReleaseBranch}
/>,
<Patch latestRelease={null} releaseBranch={mockReleaseBranch} />,
);
expect(
@@ -18,7 +18,7 @@ import React from 'react';
import { Typography } from '@material-ui/core';
import { ApiMethodRetval, IPluginApiClient } from '../../api/PluginApiClient';
import { ComponentConfigPatch, SetRefetch } from '../../types/types';
import { ComponentConfigPatch } from '../../types/types';
import { getBumpedTag } from '../../helpers/getBumpedTag';
import { InfoCardPlus } from '../../components/InfoCardPlus';
import { NoLatestRelease } from '../../components/NoLatestRelease';
@@ -31,14 +31,12 @@ interface PatchProps {
IPluginApiClient['getLatestRelease']
>['latestRelease'];
releaseBranch: ApiMethodRetval<IPluginApiClient['getBranch']> | null;
setRefetch: SetRefetch;
successCb?: ComponentConfigPatch['successCb'];
}
export const Patch = ({
latestRelease,
releaseBranch,
setRefetch,
successCb,
}: PatchProps) => {
const project = useProjectContext();
@@ -64,7 +62,6 @@ export const Patch = ({
bumpedTag={bumpedTag}
latestRelease={latestRelease}
releaseBranch={releaseBranch}
setRefetch={setRefetch}
successCb={successCb}
tagParts={tagParts}
/>
@@ -50,7 +50,6 @@ describe('PatchBody', () => {
bumpedTag={mockBumpedTag}
latestRelease={mockRcRelease}
releaseBranch={mockReleaseBranch}
setRefetch={jest.fn()}
tagParts={mockTagParts}
/>,
);
@@ -66,7 +65,6 @@ describe('PatchBody', () => {
const { getByTestId } = render(
<PatchBody
latestRelease={mockReleaseVersion}
setRefetch={jest.fn()}
releaseBranch={mockReleaseBranch}
bumpedTag={mockBumpedTag}
tagParts={mockTagParts}
@@ -35,7 +35,7 @@ import OpenInNewIcon from '@material-ui/icons/OpenInNew';
import { CalverTagParts } from '../../helpers/tagParts/getCalverTagParts';
import { CenteredCircularProgress } from '../../components/CenteredCircularProgress';
import { ComponentConfigPatch, SetRefetch } from '../../types/types';
import { ComponentConfigPatch } from '../../types/types';
import { Differ } from '../../components/Differ';
import { patch } from './sideEffects/patch';
import { ResponseStepList } from '../../components/ResponseStepList/ResponseStepList';
@@ -49,6 +49,7 @@ import {
IPluginApiClient,
UnboxArray,
} from '../../api/PluginApiClient';
import { GitHubReleaseManagerError } from '../../errors/GitHubReleaseManagerError';
interface PatchBodyProps {
bumpedTag: string;
@@ -56,7 +57,6 @@ interface PatchBodyProps {
ApiMethodRetval<IPluginApiClient['getLatestRelease']>['latestRelease']
>;
releaseBranch: ApiMethodRetval<IPluginApiClient['getBranch']>;
setRefetch: SetRefetch;
successCb?: ComponentConfigPatch['successCb'];
tagParts: NonNullable<CalverTagParts | SemverTagParts>;
}
@@ -65,7 +65,6 @@ export const PatchBody = ({
bumpedTag,
latestRelease,
releaseBranch,
setRefetch,
successCb,
tagParts,
}: PatchBodyProps) => {
@@ -75,15 +74,19 @@ export const PatchBody = ({
const githubDataResponse = useAsync(async () => {
const [
{ recentCommits: recentCommitsOnDefaultBranch },
] = await Promise.all([pluginApiClient.getRecentCommits({ ...project })]);
const {
recentCommits: recentCommitsOnReleaseBranch,
} = await pluginApiClient.getRecentCommits({
...project,
releaseBranchName: releaseBranch.name,
});
recentCommitsOnDefaultBranch,
recentCommitsOnReleaseBranch,
] = await Promise.all([
pluginApiClient.getRecentCommits({
owner: project.owner,
repo: project.repo,
}),
pluginApiClient.getRecentCommits({
owner: project.owner,
repo: project.repo,
releaseBranchName: releaseBranch.name,
}),
]);
return {
recentCommitsOnReleaseBranch,
@@ -93,7 +96,7 @@ export const PatchBody = ({
const [patchReleaseResponse, patchReleaseFn] = useAsyncFn(async (...args) => {
const selectedPatchCommit: UnboxArray<
ApiMethodRetval<IPluginApiClient['getRecentCommits']>['recentCommits']
ApiMethodRetval<IPluginApiClient['getRecentCommits']>
> = args[0];
const patchResponseSteps = await patch({
project,
@@ -111,13 +114,15 @@ export const PatchBody = ({
if (githubDataResponse.error) {
return (
<Alert data-testid={TEST_IDS.patch.error} severity="error">
{githubDataResponse.error.message}
Unexpected error: {githubDataResponse.error.message}
</Alert>
);
}
if (patchReleaseResponse.error) {
return <Alert severity="error">{patchReleaseResponse.error.message}</Alert>;
}
if (githubDataResponse.loading) {
return <CenteredCircularProgress data-testid={TEST_IDS.patch.loading} />;
}
@@ -156,9 +161,11 @@ export const PatchBody = ({
<List>
{githubDataResponse.value.recentCommitsOnDefaultBranch.map(
(commit, index) => {
// FIXME: Performance improvement opportunity: Convert to object lookup
const commitExistsOnReleaseBranch = !!githubDataResponse.value?.recentCommitsOnReleaseBranch.find(
releaseBranchCommit => releaseBranchCommit.sha === commit.sha,
);
const hasNoParent = !commit.firstParentSha;
return (
<div style={{ position: 'relative' }} key={`commit-${index}`}>
@@ -190,7 +197,8 @@ export const PatchBody = ({
patchReleaseResponse.loading ||
(patchReleaseResponse.value &&
patchReleaseResponse.value.length > 0) ||
commitExistsOnReleaseBranch
commitExistsOnReleaseBranch ||
hasNoParent
}
role={undefined}
dense
@@ -264,36 +272,28 @@ export const PatchBody = ({
responseSteps={patchReleaseResponse.value}
loading={patchReleaseResponse.loading}
title="Patch result"
setRefetch={setRefetch}
closeable
/>
);
}
if (
!githubDataResponse.value?.recentCommitsOnDefaultBranch[
const selectedPatchCommit =
githubDataResponse.value?.recentCommitsOnDefaultBranch[
checkedCommitIndex
]
) {
return (
<Button disabled variant="contained" color="primary">
Patch Release Candidate
</Button>
);
}
];
return (
<Button
disabled={checkedCommitIndex === -1}
disabled={checkedCommitIndex === -1 || !selectedPatchCommit}
variant="contained"
color="primary"
onClick={() => {
// FIXME: Optional chaining shouldn't be needed here due to the if-statement above
patchReleaseFn(
githubDataResponse.value?.recentCommitsOnDefaultBranch[
checkedCommitIndex
],
);
if (!selectedPatchCommit) {
throw new GitHubReleaseManagerError(
'Could not find selected patch commit',
);
}
patchReleaseFn(selectedPatchCommit);
}}
>
Patch Release Candidate
@@ -16,7 +16,6 @@
import { ComponentConfigPatch, ResponseStep } from '../../../types/types';
import { CalverTagParts } from '../../../helpers/tagParts/getCalverTagParts';
import { GitHubReleaseManagerError } from '../../../errors/GitHubReleaseManagerError';
import {
ApiMethodRetval,
IPluginApiClient,
@@ -33,13 +32,13 @@ interface Patch {
pluginApiClient: IPluginApiClient;
project: Project;
selectedPatchCommit: UnboxArray<
ApiMethodRetval<IPluginApiClient['getRecentCommits']>['recentCommits']
ApiMethodRetval<IPluginApiClient['getRecentCommits']>
>;
successCb?: ComponentConfigPatch['successCb'];
tagParts: NonNullable<CalverTagParts | SemverTagParts>;
}
// Inspo: https://stackoverflow.com/questions/53859199/how-to-cherry-pick-through-githubs-api
// Inspiration: https://stackoverflow.com/questions/53859199/how-to-cherry-pick-through-githubs-api
export async function patch({
bumpedTag,
latestRelease,
@@ -51,9 +50,6 @@ export async function patch({
}: Patch) {
const responseSteps: ResponseStep[] = [];
if (!selectedPatchCommit || !selectedPatchCommit.sha) {
throw new GitHubReleaseManagerError('Invalid commit');
}
const releaseBranchName = latestRelease.targetCommitish;
/**
* 1. Here is the branch we want to cherry-pick to:
@@ -15,71 +15,101 @@
*/
import React from 'react';
import { useNavigate } from 'react-router';
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 {
FormControl,
FormHelperText,
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';
import { getNewQueryParams } from '../../helpers/getNewQueryParams';
import { useQuery } from '../../helpers/useQuery';
export function Owner({
controllerRenderProps,
username,
project,
}: {
controllerRenderProps: ControllerRenderProps;
username: string;
project: Project;
}) {
const pluginApiClient = usePluginApiClientContext();
const formClasses = useFormClasses();
const project: Project = controllerRenderProps.value;
const navigate = useNavigate();
const query = useQuery();
const { loading, error, value } = useAsync(() =>
pluginApiClient.getOrganizations(),
);
if (error) {
return <Alert severity="error">{error.message}</Alert>;
}
if (loading) {
return <CenteredCircularProgress />;
}
if (!value?.organizations) {
return <Alert severity="error">Could not fetch organizations</Alert>;
}
const { loading, error, value } = useAsync(() => pluginApiClient.getOwners());
const owners = value?.owners ?? [];
const customOwnerFromUrl = !owners
.concat(['', username])
.includes(project.owner);
return (
<FormControl className={formClasses.formControl}>
<InputLabel id="owner-select-label">Organizations</InputLabel>
<Select
labelId="owner-select-label"
id="owner-select"
value={project.owner}
onChange={event => {
controllerRenderProps.onChange({
...project,
owner: event.target.value,
repo: '',
} as Project);
}}
className={formClasses.selectEmpty}
>
<MenuItem value="">
<em>None</em>
</MenuItem>
<MenuItem value={username}>
<strong>{username}</strong>
</MenuItem>
{value.organizations.map((orgName, index) => (
<MenuItem key={`organization-${index}`} value={orgName}>
{orgName}
</MenuItem>
))}
</Select>
<FormControl className={formClasses.formControl} required error={!!error}>
{loading ? (
<CenteredCircularProgress />
) : (
<>
<InputLabel id="owner-select-label">Owners</InputLabel>
<Select
labelId="owner-select-label"
id="owner-select"
value={project.owner}
defaultValue=""
onChange={event => {
const queryParams = getNewQueryParams({
query,
key: 'owner',
value: event.target.value as string,
});
navigate(`?${queryParams}`, { replace: true });
}}
className={formClasses.selectEmpty}
>
<MenuItem value="">
<em>None</em>
</MenuItem>
<MenuItem value={username}>
<strong>{username}</strong>
</MenuItem>
{!error && customOwnerFromUrl && (
<MenuItem value={project.owner}>
<strong>From URL: {project.owner}</strong>
</MenuItem>
)}
{owners.map((orgName, index) => (
<MenuItem key={`organization-${index}`} value={orgName}>
{orgName}
</MenuItem>
))}
</Select>
{error && (
<FormHelperText>
Encountered an error ({error.message})
</FormHelperText>
)}
{!error && project.owner.length === 0 && (
<>
<FormHelperText>Select an owner (org or user)</FormHelperText>
<FormHelperText>
Custom queries can be made via the query param{' '}
<strong>owner</strong>
</FormHelperText>
</>
)}
</>
)}
</FormControl>
);
}
@@ -16,69 +16,92 @@
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 } from 'react-hook-form';
import { useNavigate } from 'react-router';
import {
FormControl,
InputLabel,
Select,
MenuItem,
FormHelperText,
} from '@material-ui/core';
import { usePluginApiClientContext } from '../../contexts/PluginApiClientContext';
import { useFormClasses } from './styles';
import { CenteredCircularProgress } from '../../components/CenteredCircularProgress';
import { Project } from '../../contexts/ProjectContext';
import { getNewQueryParams } from '../../helpers/getNewQueryParams';
import { useQuery } from '../../helpers/useQuery';
export function Repo({
controllerRenderProps,
}: {
controllerRenderProps: ControllerRenderProps;
}) {
export function Repo({ project }: { project: Project }) {
const pluginApiClient = usePluginApiClientContext();
const navigate = useNavigate();
const formClasses = useFormClasses();
const project: Project = controllerRenderProps.value;
const query = useQuery();
const { loading, error, value } = useAsync(
async () => pluginApiClient.getRepositories({ owner: project.owner }),
[project.owner],
);
if (error) {
return <Alert severity="error">{error.message}</Alert>;
}
if (loading) {
return <CenteredCircularProgress />;
}
if (!value?.repositories) {
return (
<Alert severity="error">
Could not fetch repositories for "{project.owner}"
</Alert>
);
}
const repositories = value?.repositories ?? [];
const customRepoFromUrl = !repositories.concat(['']).includes(project.repo);
return (
<FormControl className={formClasses.formControl}>
<InputLabel id="repo-select-label">Repositories</InputLabel>
<Select
labelId="repo-select-label"
id="repo-select"
value={project.repo}
onChange={event => {
controllerRenderProps.onChange({
...project,
repo: event.target.value,
} as Project);
}}
className={formClasses.selectEmpty}
>
<MenuItem value="">
<em>None</em>
</MenuItem>
{value.repositories.map((repositoryName, index) => (
<MenuItem key={`repository-${index}`} value={repositoryName}>
{repositoryName}
</MenuItem>
))}
</Select>
<FormControl className={formClasses.formControl} required error={!!error}>
{loading ? (
<CenteredCircularProgress />
) : (
<>
<InputLabel id="repo-select-label">Repositories</InputLabel>
<Select
labelId="repo-select-label"
id="repo-select"
value={project.repo}
defaultValue=""
onChange={event => {
const queryParams = getNewQueryParams({
query,
key: 'repo',
value: event.target.value as string,
});
navigate(`?${queryParams}`, { replace: true });
}}
className={formClasses.selectEmpty}
>
<MenuItem value="">
<em>None</em>
</MenuItem>
{!error && customRepoFromUrl && (
<MenuItem value={project.repo}>
<strong>From URL: {project.repo}</strong>
</MenuItem>
)}
{repositories.map((repositoryName, index) => (
<MenuItem key={`repository-${index}`} value={repositoryName}>
{repositoryName}
</MenuItem>
))}
</Select>
{error && (
<FormHelperText>
Encountered an error ({error.message}")
</FormHelperText>
)}
{!error && project.repo.length === 0 && (
<>
<FormHelperText>Select a repository</FormHelperText>
<FormHelperText>
Custom queries can be made via the query param{' '}
<strong>repo</strong>
</FormHelperText>
</>
)}
</>
)}
</FormControl>
);
}
@@ -15,49 +15,26 @@
*/
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 { Project } from '../../contexts/ProjectContext';
import { Repo } from './Repo';
import { VersioningStrategy } from './VersioningStrategy';
export function RepoDetailsForm({
control,
username,
project,
}: {
control: ReturnType<typeof useForm>['control'];
username: string;
project: Project;
}) {
return (
<Controller
render={controllerRenderProps => {
const project: Project = controllerRenderProps.value;
<>
<VersioningStrategy project={project} />
return (
<>
<VersioningStrategy controllerRenderProps={controllerRenderProps} />
<Owner project={project} username={username} />
<Owner
controllerRenderProps={controllerRenderProps}
username={username}
/>
{project.owner.length > 0 && (
<Repo controllerRenderProps={controllerRenderProps} />
)}
</>
);
}}
control={control}
name="repo-details-form"
defaultValue={
{
owner: '',
repo: '',
versioningStrategy: 'semver',
} as Project
}
/>
{project.owner.length > 0 && <Repo project={project} />}
</>
);
}
@@ -14,6 +14,8 @@
* limitations under the License.
*/
import React, { useEffect } from 'react';
import { useNavigate } from 'react-router';
import {
FormControl,
FormControlLabel,
@@ -21,29 +23,48 @@ import {
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;
import { Project } from '../../contexts/ProjectContext';
import { useQuery } from '../../helpers/useQuery';
import {
getNewQueryParams,
getParsedQuery,
} from '../../helpers/getNewQueryParams';
export function VersioningStrategy({ project }: { project: Project }) {
const navigate = useNavigate();
const query = useQuery();
useEffect(() => {
const parsedQuery = getParsedQuery({ query });
if (!parsedQuery.versioningStrategy) {
const queryParams = getNewQueryParams({
query,
key: 'versioningStrategy',
value: project.versioningStrategy,
});
navigate(`?${queryParams}`, { replace: true });
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps
return (
<FormControl component="fieldset">
<FormControl component="fieldset" required>
<FormLabel component="legend">Calendar strategy</FormLabel>
<RadioGroup
aria-label="calendar-strategy"
name="calendar-strategy"
value={project.versioningStrategy}
defaultValue="semver"
onChange={event => {
controllerRenderProps.onChange({
...project,
versioningStrategy: event.target.value,
} as Project);
const queryParams = getNewQueryParams({
query,
key: 'versioningStrategy',
value: event.target.value,
});
navigate(`?${queryParams}`, { replace: true });
}}
>
<FormControlLabel
@@ -33,9 +33,7 @@ import { PromoteRc } from './PromoteRc';
describe('PromoteRc', () => {
it('return early if no latest release present', () => {
const { getByTestId } = render(
<PromoteRc latestRelease={null} setRefetch={jest.fn()} />,
);
const { getByTestId } = render(<PromoteRc latestRelease={null} />);
expect(
getByTestId(TEST_IDS.components.noLatestRelease),
@@ -44,16 +42,14 @@ describe('PromoteRc', () => {
it('should display not-rc warning', () => {
const { getByTestId } = render(
<PromoteRc latestRelease={mockReleaseVersion} setRefetch={jest.fn()} />,
<PromoteRc latestRelease={mockReleaseVersion} />,
);
expect(getByTestId(TEST_IDS.promoteRc.notRcWarning)).toBeInTheDocument();
});
it('should display PromoteRcBody', () => {
const { getByTestId } = render(
<PromoteRc latestRelease={mockRcRelease} setRefetch={jest.fn()} />,
);
const { getByTestId } = render(<PromoteRc latestRelease={mockRcRelease} />);
expect(
getByTestId(TEST_IDS.promoteRc.mockedPromoteRcBody),
@@ -20,7 +20,7 @@ import { Typography } from '@material-ui/core';
import { InfoCardPlus } from '../../components/InfoCardPlus';
import { NoLatestRelease } from '../../components/NoLatestRelease';
import { ComponentConfigPromoteRc, SetRefetch } from '../../types/types';
import { ComponentConfigPromoteRc } from '../../types/types';
import { PromoteRcBody } from './PromoteRcBody';
import { useStyles } from '../../styles/styles';
import { TEST_IDS } from '../../test-helpers/test-ids';
@@ -30,15 +30,10 @@ interface PromoteRcProps {
latestRelease: ApiMethodRetval<
IPluginApiClient['getLatestRelease']
>['latestRelease'];
setRefetch: SetRefetch;
successCb?: ComponentConfigPromoteRc['successCb'];
}
export const PromoteRc = ({
latestRelease,
setRefetch,
successCb,
}: PromoteRcProps) => {
export const PromoteRc = ({ latestRelease, successCb }: PromoteRcProps) => {
const classes = useStyles();
function Body() {
@@ -61,13 +56,7 @@ export const PromoteRc = ({
);
}
return (
<PromoteRcBody
rcRelease={latestRelease}
setRefetch={setRefetch}
successCb={successCb}
/>
);
return <PromoteRcBody rcRelease={latestRelease} successCb={successCb} />;
}
return (
@@ -35,9 +35,7 @@ import { PromoteRcBody } from './PromoteRcBody';
describe('PromoteRcBody', () => {
it('should display CTA', () => {
const { getByTestId } = render(
<PromoteRcBody rcRelease={mockRcRelease} setRefetch={jest.fn()} />,
);
const { getByTestId } = render(<PromoteRcBody rcRelease={mockRcRelease} />);
expect(getByTestId(TEST_IDS.promoteRc.cta)).toBeInTheDocument();
});
@@ -20,7 +20,7 @@ import { Alert } from '@material-ui/lab';
import { Button, Typography } from '@material-ui/core';
import { Differ } from '../../components/Differ';
import { ComponentConfigPromoteRc, SetRefetch } from '../../types/types';
import { ComponentConfigPromoteRc } from '../../types/types';
import { promoteRc } from './sideEffects/promoteRc';
import { ResponseStepList } from '../../components/ResponseStepList/ResponseStepList';
import { TEST_IDS } from '../../test-helpers/test-ids';
@@ -33,15 +33,10 @@ interface PromoteRcBodyProps {
rcRelease: NonNullable<
ApiMethodRetval<IPluginApiClient['getLatestRelease']>['latestRelease']
>;
setRefetch: SetRefetch;
successCb?: ComponentConfigPromoteRc['successCb'];
}
export const PromoteRcBody = ({
rcRelease,
setRefetch,
successCb,
}: PromoteRcBodyProps) => {
export const PromoteRcBody = ({ rcRelease, successCb }: PromoteRcBodyProps) => {
const pluginApiClient = usePluginApiClientContext();
const project = useProjectContext();
const classes = useStyles();
@@ -82,7 +77,6 @@ export const PromoteRcBody = ({
<ResponseStepList
responseSteps={promoteGitHubRcResponse.value}
title="Promote RC result"
setRefetch={setRefetch}
loading={promoteGitHubRcResponse.loading}
/>
);
@@ -17,17 +17,18 @@
import React from 'react';
import { render } from '@testing-library/react';
import { TEST_IDS } from '../../test-helpers/test-ids';
import { mockRefetch } from '../../test-helpers/test-helpers';
import { ResponseStepList } from './ResponseStepList';
import { TEST_IDS } from '../../test-helpers/test-ids';
jest.mock('../../contexts/RefetchContext', () => ({
useRefetchContext: jest.fn(() => mockRefetch),
}));
describe('ResponseStepList', () => {
it('should render loading state when loading', () => {
const { getByTestId } = render(
<ResponseStepList
loading
setRefetch={jest.fn()}
title="mock_responseStepList_title"
/>,
<ResponseStepList loading title="mock_responseStepList_title" />,
);
expect(
@@ -37,11 +38,7 @@ describe('ResponseStepList', () => {
it('should render loading state when no responseSteps', () => {
const { getByTestId } = render(
<ResponseStepList
loading={false}
setRefetch={jest.fn()}
title="mock_responseStepList_title"
/>,
<ResponseStepList loading={false} title="mock_responseStepList_title" />,
);
expect(
@@ -53,7 +50,6 @@ describe('ResponseStepList', () => {
const { getByTestId } = render(
<ResponseStepList
loading={false}
setRefetch={jest.fn()}
title="mock_responseStepList_title"
responseSteps={[]}
/>,
@@ -16,22 +16,22 @@
import React, { PropsWithChildren } from 'react';
import {
List,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
List,
} from '@material-ui/core';
import { ResponseStep, SetRefetch } from '../../types/types';
import { TEST_IDS } from '../../test-helpers/test-ids';
import { ResponseStepListItem } from './ResponseStepListItem';
import { CenteredCircularProgress } from '../CenteredCircularProgress';
import { ResponseStep } from '../../types/types';
import { ResponseStepListItem } from './ResponseStepListItem';
import { TEST_IDS } from '../../test-helpers/test-ids';
import { useRefetchContext } from '../../contexts/RefetchContext';
interface ResponseStepListProps {
responseSteps?: ResponseStep[];
setRefetch: SetRefetch;
title: string;
animationDelay?: number;
loading: boolean;
@@ -42,7 +42,6 @@ interface ResponseStepListProps {
export const ResponseStepList = ({
responseSteps,
animationDelay,
setRefetch,
loading = false,
closeable = false,
denseList = false,
@@ -50,6 +49,7 @@ export const ResponseStepList = ({
children,
}: PropsWithChildren<ResponseStepListProps>) => {
const [open, setOpen] = React.useState(true);
const { setRefetchTrigger } = useRefetchContext();
const handleClose = () => setOpen(false);
@@ -102,7 +102,7 @@ export const ResponseStepList = ({
</Button>
)}
<Button
onClick={() => setRefetch(Date.now())}
onClick={() => setRefetchTrigger(Date.now())}
color="primary"
variant="contained"
size="large"
@@ -0,0 +1,38 @@
/*
* 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 { createContext, useContext } from 'react';
import { GitHubReleaseManagerError } from '../errors/GitHubReleaseManagerError';
export interface Refetch {
refetchTrigger: number;
setRefetchTrigger: React.Dispatch<React.SetStateAction<number>>;
}
export const RefetchContext = createContext<Refetch | undefined>(undefined);
export const useRefetchContext = () => {
const refetch = useContext(RefetchContext);
if (!refetch) {
throw new GitHubReleaseManagerError('refetch not found');
}
return {
setRefetchTrigger: refetch.setRefetchTrigger,
};
};
@@ -0,0 +1,49 @@
/*
* 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 qs from 'qs';
import { Project } from '../contexts/ProjectContext';
export function getParsedQuery({ query }: { query: URLSearchParams }) {
const parsedQuery: Partial<Project> = qs.parse(query.toString());
return parsedQuery;
}
export function getNewQueryParams({
query,
key,
value,
}: {
query: URLSearchParams;
key: keyof Project;
value: string;
}) {
const queryParams = qs.parse(query.toString());
queryParams[key] = value;
return qs.stringify(queryParams);
}
// TODO:
// import { useQuery } from './useQuery';
// export function useGetNewQueryParams({}) {
// const query = useQuery();
// return 1;
// }
@@ -0,0 +1,21 @@
/*
* 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 { useLocation } from 'react-router';
export function useQuery(): URLSearchParams {
return new URLSearchParams(useLocation().search);
}
@@ -30,7 +30,7 @@ describe('testHelpers', () => {
"getHost": [MockFunction],
"getLatestCommit": [MockFunction],
"getLatestRelease": [MockFunction],
"getOrganizations": [MockFunction],
"getOwners": [MockFunction],
"getRecentCommits": [MockFunction],
"getRepoPath": [MockFunction],
"getRepositories": [MockFunction],
@@ -69,6 +69,7 @@ describe('testHelpers', () => {
"tagName": "rc-2020.01.01_1",
"targetCommitish": "rc/1.2.3",
},
"mockRefetch": [MockFunction],
"mockReleaseBranch": Object {
"commit": Object {
"commit": Object {
@@ -105,11 +105,7 @@ export const mockReleaseBranch = createMockBranch();
const createMockCommit = ({
...rest
}: Partial<
NonNullable<
UnboxArray<
ApiMethodRetval<IPluginApiClient['getRecentCommits']>['recentCommits']
>
>
NonNullable<UnboxArray<ApiMethodRetval<IPluginApiClient['getRecentCommits']>>>
>) =>
({
author: {
@@ -123,15 +119,15 @@ const createMockCommit = ({
firstParentSha: 'mock_first_parent_sha',
...rest,
} as NonNullable<
UnboxArray<
ApiMethodRetval<IPluginApiClient['getRecentCommits']>['recentCommits']
>
UnboxArray<ApiMethodRetval<IPluginApiClient['getRecentCommits']>>
>);
export const mockSelectedPatchCommit = createMockCommit({
sha: 'mock_sha_selected_patch_commit',
});
export const mockRefetch = jest.fn();
/**
* MOCK API CLIENT
*/
@@ -140,18 +136,18 @@ export const mockApiClient: IPluginApiClient = {
getRepoPath: jest.fn(() => 'erikengervall/playground'),
getOrganizations: jest.fn(),
getOwners: jest.fn(),
getRepositories: jest.fn(),
getUsername: jest.fn(),
getRecentCommits: jest.fn().mockResolvedValue({
recentCommits: [
getRecentCommits: jest
.fn()
.mockResolvedValue([
createMockCommit({ sha: 'mock_sha_recent_commits_1' }),
createMockCommit({ sha: 'mock_sha_recent_commits_2' }),
],
}),
]),
getLatestRelease: jest.fn(), // TODO:
@@ -19,7 +19,7 @@ interface ComponentConfig<Args = void> {
omit?: boolean;
}
export interface ComponentConfigCreateRcSuccessCbArgs {
interface ComponentConfigCreateRcSuccessCbArgs {
gitHubReleaseUrl: string;
gitHubReleaseName: string | null;
comparisonUrl: string;
@@ -28,7 +28,7 @@ export interface ComponentConfigCreateRcSuccessCbArgs {
}
export type ComponentConfigCreateRc = ComponentConfig<ComponentConfigCreateRcSuccessCbArgs>;
export interface ComponentConfigPromoteRcSuccessCbArgs {
interface ComponentConfigPromoteRcSuccessCbArgs {
gitHubReleaseUrl: string;
gitHubReleaseName: string | null;
previousTagUrl: string;
@@ -38,7 +38,7 @@ export interface ComponentConfigPromoteRcSuccessCbArgs {
}
export type ComponentConfigPromoteRc = ComponentConfig<ComponentConfigPromoteRcSuccessCbArgs>;
export interface ComponentConfigPatchSuccessCbArgs {
interface ComponentConfigPatchSuccessCbArgs {
updatedReleaseUrl: string;
updatedReleaseName: string | null;
previousTag: string;
@@ -48,8 +48,6 @@ export interface ComponentConfigPatchSuccessCbArgs {
}
export type ComponentConfigPatch = ComponentConfig<ComponentConfigPatchSuccessCbArgs>;
export type SetRefetch = React.Dispatch<React.SetStateAction<number>>;
export interface ResponseStep {
message: string | React.ReactNode;
secondaryMessage?: string | React.ReactNode;
+16 -44
View File
@@ -1679,50 +1679,6 @@
lodash "^4.17.19"
to-fast-properties "^2.0.0"
"@backstage/core@^0.7.5":
version "0.7.5"
resolved "https://artifactory.spotify.net/artifactory/api/npm/virtual-npm/@backstage/core/-/core-0.7.5.tgz#27e0a7982dbab40973eefaa185d89babdd454913"
integrity sha1-J+CnmC26tAlz7vqhhdibq91FSRM=
dependencies:
"@backstage/config" "^0.1.4"
"@backstage/core-api" "^0.2.16"
"@backstage/errors" "^0.1.1"
"@backstage/theme" "^0.2.5"
"@material-ui/core" "^4.11.0"
"@material-ui/icons" "^4.9.1"
"@material-ui/lab" "4.0.0-alpha.45"
"@testing-library/react-hooks" "^3.4.2"
"@types/dagre" "^0.7.44"
"@types/prop-types" "^15.7.3"
"@types/react" "^16.9"
"@types/react-sparklines" "^1.7.0"
"@types/react-text-truncate" "^0.14.0"
classnames "^2.2.6"
clsx "^1.1.0"
d3-selection "^2.0.0"
d3-shape "^2.0.0"
d3-zoom "^2.0.0"
dagre "^0.8.5"
immer "^9.0.1"
lodash "^4.17.15"
material-table "^1.69.1"
prop-types "^15.7.2"
qs "^6.9.4"
rc-progress "^3.0.0"
react "^16.12.0"
react-dom "^16.12.0"
react-helmet "6.1.0"
react-hook-form "^6.6.0"
react-markdown "^5.0.2"
react-router "6.0.0-beta.0"
react-router-dom "6.0.0-beta.0"
react-sparklines "^1.7.0"
react-syntax-highlighter "^15.4.3"
react-text-truncate "^0.16.0"
react-use "^15.3.3"
remark-gfm "^1.0.0"
zen-observable "^0.8.15"
"@bcoe/v8-coverage@^0.2.3":
version "0.2.3"
resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
@@ -21631,6 +21587,13 @@ qs@6.7.0:
resolved "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
qs@^6.10.1:
version "6.10.1"
resolved "https://artifactory.spotify.net/artifactory/api/npm/virtual-npm/qs/-/qs-6.10.1.tgz#4931482fa8d647a5aab799c5271d2133b981fb6a"
integrity sha1-STFIL6jWR6Wqt5nFJx0hM7mB+2o=
dependencies:
side-channel "^1.0.4"
qs@^6.5.2, qs@^6.6.0, qs@^6.7.0, qs@^6.9.1, qs@^6.9.4, qs@^6.9.6:
version "6.9.6"
resolved "https://registry.npmjs.org/qs/-/qs-6.9.6.tgz#26ed3c8243a431b2924aca84cc90471f35d5a0ee"
@@ -23680,6 +23643,15 @@ side-channel@^1.0.2:
es-abstract "^1.17.0-next.1"
object-inspect "^1.7.0"
side-channel@^1.0.4:
version "1.0.4"
resolved "https://artifactory.spotify.net/artifactory/api/npm/virtual-npm/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
integrity sha1-785cj9wQTudRslxY1CkAEfpeos8=
dependencies:
call-bind "^1.0.0"
get-intrinsic "^1.0.2"
object-inspect "^1.9.0"
sigmund@~1.0.0:
version "1.0.1"
resolved "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590"