From 0aaf020359d48d57074ca259e59d1c47ee96de17 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Fri, 7 Jun 2024 13:15:14 +0200 Subject: [PATCH] feat: add auto-completion to workspaces and repositories Signed-off-by: Benjamin Janssens --- .../src/BitbucketCloudClient.ts | 10 ++ .../src/models/index.ts | 11 ++ .../RepoUrlPicker/BitbucketRepoPicker.tsx | 133 +++++++++++------- .../fields/RepoUrlPicker/RepoUrlPicker.tsx | 1 + .../RepoUrlPicker/RepoUrlPickerRepoName.tsx | 26 ++-- .../components/fields/RepoUrlPicker/types.ts | 1 + 6 files changed, 124 insertions(+), 58 deletions(-) diff --git a/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.ts b/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.ts index 97d3d0e52e..a06f7425d0 100644 --- a/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.ts +++ b/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.ts @@ -85,6 +85,16 @@ export class BitbucketCloudClient { ); } + listWorkspaces( + options?: FilterAndSortOptions & PartialResponseOptions, + ): WithPagination { + return new WithPagination( + paginationOptions => + this.createUrl('/workspaces', { ...paginationOptions, ...options }), + url => this.getTypeMapped(url), + ); + } + private createUrl(endpoint: string, options?: RequestOptions): URL { const request = new URL(this.config.apiBaseUrl + endpoint); for (const key in options) { diff --git a/plugins/bitbucket-cloud-common/src/models/index.ts b/plugins/bitbucket-cloud-common/src/models/index.ts index ace8ee4939..2cdc143d06 100644 --- a/plugins/bitbucket-cloud-common/src/models/index.ts +++ b/plugins/bitbucket-cloud-common/src/models/index.ts @@ -264,6 +264,17 @@ export namespace Models { values?: Set; } + /** + * A paginated list of workspaces. + * @public + */ + export interface PaginatedWorkspaces extends Paginated { + /** + * The values of the current page. + */ + values?: Set; + } + /** * Object describing a user's role on resources like commits or pull requests. * @public diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx index 75ae840407..a7c2efc75e 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx @@ -17,8 +17,6 @@ import React, { useEffect, useState } from 'react'; import FormControl from '@material-ui/core/FormControl'; import FormHelperText from '@material-ui/core/FormHelperText'; -import Input from '@material-ui/core/Input'; -import InputLabel from '@material-ui/core/InputLabel'; import { Select, SelectItem } from '@backstage/core-components'; import { RepoUrlPickerState } from './types'; import { BitbucketCloudClient } from '@backstage/plugin-bitbucket-cloud-common'; @@ -65,10 +63,8 @@ export const BitbucketRepoPicker = (props: { }, [allowedOwners, host, onChange]); const [client, setClient] = useState(); + const [availableWorkspaces, setAvailableWorkspaces] = useState([]); const [availableProjects, setAvailableProjects] = useState([]); - const [availableRepositories, setAvailableRepositories] = useState( - [], - ); useEffect(() => { if (accessToken) @@ -81,39 +77,78 @@ export const BitbucketRepoPicker = (props: { ); }, [accessToken]); - const onChangeWorkspace = async () => { - if (client) - if (!workspace) { - setAvailableProjects([]); - } else { - try { - for await (const page of client - .listProjectsByWorkspace(workspace) - .iteratePages()) { - const keys = [...page.values!].map(p => p.key!); - setAvailableProjects([...availableProjects, ...keys]); - } - } catch { - setAvailableProjects([]); - } - } - }; + // Update available workspaces when host changes + useEffect(() => { + const updateAvailableWorkspaces = async () => { + if (client) + if (!host) { + setAvailableWorkspaces([]); + } else { + const result: string[] = []; - const onChangeProject = async () => { - if (client && workspace) - if (!project) { - setAvailableRepositories([]); - } else { - for await (const page of client - .listRepositoriesByWorkspace(workspace, { - q: `project.key="${project}"`, - }) - .iteratePages()) { - const keys = [...page.values!].map(p => p.slug!); - setAvailableRepositories([...availableRepositories, ...keys]); + for await (const page of client.listWorkspaces().iteratePages()) { + const keys = [...page.values!].map(p => p.slug!); + result.push(...keys); + } + + setAvailableWorkspaces(result); } - } - }; + }; + + updateAvailableWorkspaces(); + }, [client, host]); + + // Update available repositories when workspace or project changes + useEffect(() => { + const updateAvailableRepositories = async () => { + if (client && workspace) + if (!project) { + onChange({ availableRepos: [] }); + } else { + const availableRepos: string[] = []; + + for await (const page of client + .listRepositoriesByWorkspace(workspace, { + q: `project.key="${project}"`, + }) + .iteratePages()) { + const keys = [...page.values!].map(p => p.slug!); + availableRepos.push(...keys); + } + + onChange({ availableRepos }); + } + }; + + updateAvailableRepositories(); + }, [client, workspace, project, onChange]); + + // Update available projects when workspace changes + useEffect(() => { + const updateAvailableProjects = async () => { + if (client) + if (!workspace) { + setAvailableProjects([]); + } else { + try { + const result: string[] = []; + + for await (const page of client + .listProjectsByWorkspace(workspace) + .iteratePages()) { + const keys = [...page.values!].map(p => p.key!); + result.push(...keys); + } + + setAvailableProjects(result); + } catch { + setAvailableProjects([]); + } + } + }; + + updateAvailableProjects(); + }, [client, workspace]); return ( <> @@ -135,15 +170,18 @@ export const BitbucketRepoPicker = (props: { items={ownerItems} /> ) : ( - <> - Workspace - onChange({ workspace: e.target.value })} - value={workspace} - onBlur={() => onChangeWorkspace()} - /> - + { + onChange({ workspace: newValue || '' }); + }} + options={availableWorkspaces} + renderInput={params => ( + + )} + freeSolo + autoSelect + /> )} The Workspace that this repo will belong to @@ -173,9 +211,10 @@ export const BitbucketRepoPicker = (props: { onChange({ project: newValue || '' }); }} options={availableProjects} - renderInput={params => } + renderInput={params => ( + + )} freeSolo - onBlur={() => onChangeProject()} autoSelect /> )} diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx index cd749a08a1..e0d714ffda 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx @@ -232,6 +232,7 @@ export const RepoUrlPicker = (props: RepoUrlPickerProps) => { setState(prevState => ({ ...prevState, repoName: repo })) } rawErrors={rawErrors} + availableRepos={state.availableRepos} /> ); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerRepoName.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerRepoName.tsx index f2d68b08f3..80851b0cbb 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerRepoName.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerRepoName.tsx @@ -17,16 +17,17 @@ import React, { useEffect } from 'react'; import { Select, SelectItem } from '@backstage/core-components'; import FormControl from '@material-ui/core/FormControl'; import FormHelperText from '@material-ui/core/FormHelperText'; -import Input from '@material-ui/core/Input'; -import InputLabel from '@material-ui/core/InputLabel'; +import Autocomplete from '@material-ui/lab/Autocomplete'; +import TextField from '@material-ui/core/TextField'; export const RepoUrlPickerRepoName = (props: { repoName?: string; allowedRepos?: string[]; onChange: (host: string) => void; rawErrors: string[]; + availableRepos?: string[]; }) => { - const { repoName, allowedRepos, onChange, rawErrors } = props; + const { repoName, allowedRepos, onChange, rawErrors, availableRepos } = props; useEffect(() => { // If there is no repoName chosen currently @@ -61,14 +62,17 @@ export const RepoUrlPickerRepoName = (props: { items={repoItems} /> ) : ( - <> - Repository - onChange(String(e.target.value))} - value={repoName} - /> - + { + onChange(newValue || ''); + }} + options={availableRepos || []} + renderInput={params => ( + + )} + freeSolo + /> )} The name of the repository diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/types.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/types.ts index 5e979e789e..1430b10b84 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/types.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/types.ts @@ -20,4 +20,5 @@ export interface RepoUrlPickerState { organization?: string; workspace?: string; project?: string; + availableRepos?: string[]; }