From f93d678daebcf4c1424d6c309d969cf8150eb4de Mon Sep 17 00:00:00 2001 From: Yi Cai Date: Fri, 19 Aug 2022 13:07:51 -0400 Subject: [PATCH] feat: make https repo credentials editable in the UI (#9108) (#9782) * fix: missing actions (#10327) (#10359) Signed-off-by: CI Signed-off-by: CI Signed-off-by: Yi Cai * chore: infer managed resources health from redis instead of storing it in CRD (#10191) * chore: infer managed resources health from redis instead of storing it in CRD Signed-off-by: Alexander Matyushentsev * apply reviewer notes Signed-off-by: Alexander Matyushentsev Signed-off-by: Alexander Matyushentsev Signed-off-by: Yi Cai * fix: Add logic to handle for fileHandle.Close() (#9963) (#10361) Signed-off-by: xin.li Signed-off-by: xin.li Signed-off-by: Yi Cai * docs: fix typo in upgrade notes (#10377) Signed-off-by: Xijun Dai Signed-off-by: Xijun Dai Signed-off-by: Yi Cai * fix: add space before prompt in CLI (#10362) Signed-off-by: xin.li Signed-off-by: xin.li Signed-off-by: Yi Cai * docs: fix indentation of example AppProject in 'Sync Windows' documentation (#10388) Signed-off-by: Yi Cai * fix: Correctly assume cluster-scoped resources to be self-referenced (#10390) Signed-off-by: jannfis Signed-off-by: jannfis Signed-off-by: Yi Cai * ui-make-https-repo-credential-editable Signed-off-by: ciiay Signed-off-by: Yi Cai * Minor format fix Signed-off-by: ciiay Signed-off-by: Yi Cai * Minor fix for unclickable input field Signed-off-by: ciiay Signed-off-by: Yi Cai * Updates for comments Signed-off-by: ciiay Signed-off-by: Yi Cai * ui-make-https-repo-credential-editable Signed-off-by: ciiay Signed-off-by: Yi Cai * Minor format fix Signed-off-by: ciiay Signed-off-by: Yi Cai * Minor fix for unclickable input field Signed-off-by: ciiay Signed-off-by: Yi Cai * Updates for comments Signed-off-by: ciiay Signed-off-by: Yi Cai Signed-off-by: CI Signed-off-by: Yi Cai Signed-off-by: Alexander Matyushentsev Signed-off-by: xin.li Signed-off-by: Xijun Dai Signed-off-by: jannfis Signed-off-by: ciiay Co-authored-by: Michael Crenshaw Co-authored-by: Alexander Matyushentsev Co-authored-by: my-git9 Co-authored-by: Xijun Dai Co-authored-by: Jun Duan Co-authored-by: jannfis --- .../components/repo-details/repo-details.tsx | 87 ++++ .../components/repos-list/repos-list.scss | 7 + .../components/repos-list/repos-list.tsx | 465 +++++++++++------- .../editable-panel/editable-panel.scss | 6 +- ui/src/app/shared/models.ts | 9 + ui/src/app/shared/services/repo-service.ts | 31 ++ 6 files changed, 413 insertions(+), 192 deletions(-) create mode 100644 ui/src/app/settings/components/repo-details/repo-details.tsx diff --git a/ui/src/app/settings/components/repo-details/repo-details.tsx b/ui/src/app/settings/components/repo-details/repo-details.tsx new file mode 100644 index 0000000000000..df850a927e261 --- /dev/null +++ b/ui/src/app/settings/components/repo-details/repo-details.tsx @@ -0,0 +1,87 @@ +import * as React from 'react'; +import {FormField} from 'argo-ui'; +import {FormApi, Text} from 'react-form'; +import {EditablePanel, EditablePanelItem} from '../../../shared/components'; +import * as models from '../../../shared/models'; +import {NewHTTPSRepoParams} from '../repos-list/repos-list'; + +export const RepoDetails = (props: {repo: models.Repository; save?: (params: NewHTTPSRepoParams) => Promise}) => { + const {repo, save} = props; + const FormItems = (repository: models.Repository): EditablePanelItem[] => { + const items: EditablePanelItem[] = [ + { + title: 'Type', + view: repository.type + }, + { + title: 'Repository URL', + view: repository.repo + }, + { + title: 'Username (optional)', + view: repository.username || '', + edit: (formApi: FormApi) => + }, + { + title: 'Password (optional)', + view: repository.username ? '******' : '', + edit: (formApi: FormApi) => + } + ]; + + if (repository.name) { + items.splice(1, 0, { + title: 'NAME', + view: repository.name + }); + } + + if (repository.project) { + items.splice(repository.name ? 2 : 1, 0, { + title: 'Project', + view: repository.project + }); + } + + if (repository.proxy) { + items.push({ + title: 'Proxy (optional)', + view: repository.proxy + }); + } + + return items; + }; + + const newRepo = { + type: repo.type, + name: repo.name || '', + url: repo.repo, + username: repo.username || '', + password: repo.password || '', + tlsClientCertData: repo.tlsClientCertData || '', + tlsClientCertKey: repo.tlsClientCertKey || '', + insecure: repo.insecure || false, + enableLfs: repo.enableLfs || false, + proxy: repo.proxy || '', + project: repo.project || '' + }; + + return ( + ({ + username: !input.username && input.password && 'Username is required if password is given.', + password: !input.password && input.username && 'Password is required if username is given.' + })} + save={async input => { + const params: NewHTTPSRepoParams = {...newRepo}; + params.username = input.username || ''; + params.password = input.password || ''; + save(params); + }} + title='CONNECTED REPOSITORY' + items={FormItems(repo)} + /> + ); +}; diff --git a/ui/src/app/settings/components/repos-list/repos-list.scss b/ui/src/app/settings/components/repos-list/repos-list.scss index 9b5b30b38e02b..0994a8c97a523 100644 --- a/ui/src/app/settings/components/repos-list/repos-list.scss +++ b/ui/src/app/settings/components/repos-list/repos-list.scss @@ -28,6 +28,13 @@ } .argo-table-list { + .item-clickable { + cursor: pointer; + + &:hover { + box-shadow: 1px 2px 3px rgba($argo-color-gray-9, 0.1), 0 0 0 1px rgba($argo-color-teal-5, 0.5); + } + } .argo-dropdown { float: right; } diff --git a/ui/src/app/settings/components/repos-list/repos-list.tsx b/ui/src/app/settings/components/repos-list/repos-list.tsx index 7feec8daa027b..9005728f8d3af 100644 --- a/ui/src/app/settings/components/repos-list/repos-list.tsx +++ b/ui/src/app/settings/components/repos-list/repos-list.tsx @@ -8,6 +8,7 @@ import {CheckboxField, ConnectionStateIcon, DataLoader, EmptyState, ErrorNotific import {AppContext} from '../../../shared/context'; import * as models from '../../../shared/models'; import {services} from '../../../shared/services'; +import {RepoDetails} from '../repo-details/repo-details'; require('./repos-list.scss'); @@ -22,7 +23,7 @@ interface NewSSHRepoParams { project?: string; } -interface NewHTTPSRepoParams { +export interface NewHTTPSRepoParams { type: string; name: string; url: string; @@ -86,6 +87,8 @@ export class ReposList extends React.Component< { connecting: boolean; method: string; + currentRepo: models.Repository; + displayEditPanel: boolean; } > { public static contextTypes = { @@ -103,7 +106,9 @@ export class ReposList extends React.Component< super(props); this.state = { connecting: false, - method: ConnectionMethod.SSH + method: ConnectionMethod.SSH, + currentRepo: null, + displayEditPanel: false }; } @@ -118,10 +123,10 @@ export class ReposList extends React.Component<

)} items={[ConnectionMethod.SSH, ConnectionMethod.HTTPS, ConnectionMethod.GITHUBAPP].map( - (type: ConnectionMethod.SSH | ConnectionMethod.HTTPS | ConnectionMethod.GITHUBAPP) => ({ - title: type.toUpperCase(), + (connectMethod: ConnectionMethod.SSH | ConnectionMethod.HTTPS | ConnectionMethod.GITHUBAPP) => ({ + title: connectMethod.toUpperCase(), action: () => { - onSelection(type); + onSelection(connectMethod); const formState = this.formApi.getFormState(); this.formApi.setFormState({ ...formState, @@ -151,6 +156,7 @@ export class ReposList extends React.Component< return { url: (!httpsValues.url && 'Repository URL is required') || (this.credsTemplate && !this.isHTTPSUrl(httpsValues.url) && 'Not a valid HTTPS URL'), name: httpsValues.type === 'helm' && !httpsValues.name && 'Name is required', + username: !httpsValues.username && httpsValues.password && 'Username is required if password is given.', password: !httpsValues.password && httpsValues.username && 'Password is required if username is given.', tlsClientCertKey: !httpsValues.tlsClientCertKey && httpsValues.tlsClientCertData && 'TLS client cert key is required if TLS client cert is given.' }; @@ -165,6 +171,42 @@ export class ReposList extends React.Component< } } + private SlidingPanelHeader() { + return ( + <> + {this.showConnectRepo && ( + <> + {' '} + {' '} + + + )} + {this.state.displayEditPanel && ( + + )} + + ); + } + private onSubmitForm() { switch (this.state.method) { case ConnectionMethod.SSH: @@ -215,7 +257,10 @@ export class ReposList extends React.Component< {repos.map(repo => ( -
+
(this.isRepoUpdatable(repo) ? this.displayEditSliding(repo) : null)}>
@@ -299,201 +344,217 @@ export class ReposList extends React.Component<
(this.showConnectRepo = false)} - header={ - <> - {' '} - {' '} - - - }> - {this.ConnectRepoFormButton(this.state.method, method => { - this.setState({method}); - })} - services.projects.list('items.metadata.name').then(projects => projects.map(proj => proj.metadata.name).sort())}> - {projects => ( -
(this.formApi = api)} - defaultValues={this.onChooseDefaultValues()} - validateError={(values: FormValues) => this.onValidateErrors(values)}> - {formApi => ( - - {this.state.method === ConnectionMethod.SSH && ( -
-

CONNECT REPO USING SSH

-
- -
-
- -
-
- -
-
- -
-
- - -
-
- - -
-
- -
-
- )} - {this.state.method === ConnectionMethod.HTTPS && ( -
-

CONNECT REPO USING HTTPS

-
- -
- {formApi.getFormState().values.type === 'helm' && ( + isShown={this.showConnectRepo || this.state.displayEditPanel} + onClose={() => { + if (!this.state.displayEditPanel && this.showConnectRepo) { + this.showConnectRepo = false; + } + if (this.state.displayEditPanel) { + this.setState({displayEditPanel: false}); + } + }} + header={this.SlidingPanelHeader()}> + {this.showConnectRepo && + this.ConnectRepoFormButton(this.state.method, method => { + this.setState({method}); + })} + {this.state.displayEditPanel && this.updateHTTPSRepo(params)} />} + {!this.state.displayEditPanel && ( + services.projects.list('items.metadata.name').then(projects => projects.map(proj => proj.metadata.name).sort())}> + {projects => ( + (this.formApi = api)} + defaultValues={this.onChooseDefaultValues()} + validateError={(values: FormValues) => this.onValidateErrors(values)}> + {formApi => ( + + {this.state.method === ConnectionMethod.SSH && ( +
+

CONNECT REPO USING SSH

- + +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+ + +
+
+
- )} -
- -
-
- -
-
- -
-
- -
-
- -
-
- -
- {formApi.getFormState().values.type === 'git' && ( - -
- - -
-
- - -
-
- )} -
- -
-
- )} - {this.state.method === ConnectionMethod.GITHUBAPP && ( -
-

CONNECT REPO USING GITHUB APP

-
-
- {formApi.getFormState().values.ghType === 'GitHub Enterprise' && ( - + )} + {this.state.method === ConnectionMethod.HTTPS && ( +
+

CONNECT REPO USING HTTPS

+
+ +
+ {formApi.getFormState().values.type === 'helm' && (
- +
- - )} -
- -
-
- -
-
- -
-
- -
-
- -
-
- - -
-
- - + )} +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ {formApi.getFormState().values.type === 'git' && ( + +
+ + +
+
+ + +
+
+ )} +
+ +
- {formApi.getFormState().values.ghType === 'GitHub Enterprise' && ( - -
- -
-
- -
-
- )} -
- + )} + {this.state.method === ConnectionMethod.GITHUBAPP && ( +
+

CONNECT REPO USING GITHUB APP

+
+ +
+ {formApi.getFormState().values.ghType === 'GitHub Enterprise' && ( + +
+ +
+
+ )} +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+ + +
+ {formApi.getFormState().values.ghType === 'GitHub Enterprise' && ( + +
+ +
+
+ +
+
+ )} +
+ +
-
- )} - - )} - - )} - + )} + + )} + + )} + + )} ); } + private displayEditSliding(repo: models.Repository) { + this.setState({currentRepo: repo}); + this.setState({displayEditPanel: true}); + } + // Whether url is a https url (simple version) private isHTTPSUrl(url: string) { if (url.match(/^https:\/\/.*$/gi)) { @@ -503,14 +564,19 @@ export class ReposList extends React.Component< } } + // only connections of git type which is not via GitHub App are updatable + private isRepoUpdatable(repo: models.Repository) { + return this.isHTTPSUrl(repo.repo) && repo.type === 'git' && !repo.githubAppId; + } + // Forces a reload of configured repositories, circumventing the cache - private async refreshRepoList() { + private async refreshRepoList(updatedRepo?: string) { try { await services.repos.listNoCache(); await services.repocreds.list(); this.repoLoader.reload(); this.appContext.apis.notifications.show({ - content: 'Successfully reloaded list of repositories', + content: updatedRepo ? `Successfully updated ${updatedRepo} repository` : 'Successfully reloaded list of repositories', type: NotificationType.Success }); } catch (e) { @@ -575,6 +641,23 @@ export class ReposList extends React.Component< } } + // Update an existing repository for HTTPS repositories + private async updateHTTPSRepo(params: NewHTTPSRepoParams) { + try { + await services.repos.updateHTTPS(params); + this.repoLoader.reload(); + this.setState({displayEditPanel: false}); + this.refreshRepoList(params.url); + } catch (e) { + this.appContext.apis.notifications.show({ + content: , + type: NotificationType.Error + }); + } finally { + this.setState({connecting: false}); + } + } + // Connect a new repository or create a repository credentials for GitHub App repositories private async connectGitHubAppRepo(params: NewGitHubAppRepoParams) { if (this.credsTemplate) { diff --git a/ui/src/app/shared/components/editable-panel/editable-panel.scss b/ui/src/app/shared/components/editable-panel/editable-panel.scss index 8df08c8bb5d43..7da3f2c3fc55c 100644 --- a/ui/src/app/shared/components/editable-panel/editable-panel.scss +++ b/ui/src/app/shared/components/editable-panel/editable-panel.scss @@ -3,6 +3,10 @@ .editable-panel { position: relative; + input { + z-index: 10; + } + &__buttons { position: absolute; top: 30px; @@ -28,7 +32,7 @@ border-radius: 4px; position: absolute; display: block; - background: rgba($argo-color-gray-6, .2); + background: rgba($argo-color-gray-6, 0.2); left: 0; right: 0; top: 0; diff --git a/ui/src/app/shared/models.ts b/ui/src/app/shared/models.ts index 41537598b82e0..91a1257c4d00d 100644 --- a/ui/src/app/shared/models.ts +++ b/ui/src/app/shared/models.ts @@ -491,6 +491,15 @@ export interface Repository { type?: string; name?: string; connectionState: ConnectionState; + project?: string; + username?: string; + password?: string; + tlsClientCertData?: string; + tlsClientCertKey?: string; + proxy?: string; + insecure?: boolean; + enableLfs?: boolean; + githubAppId?: string; } export interface RepositoryList extends ItemsList {} diff --git a/ui/src/app/shared/services/repo-service.ts b/ui/src/app/shared/services/repo-service.ts index c7f91ff55f757..0e180e9707831 100644 --- a/ui/src/app/shared/services/repo-service.ts +++ b/ui/src/app/shared/services/repo-service.ts @@ -47,6 +47,37 @@ export class RepositoriesService { .then(res => res.body as models.Repository); } + public updateHTTPS({ + type, + name, + url, + username, + password, + tlsClientCertData, + tlsClientCertKey, + insecure, + enableLfs, + proxy, + project + }: { + type: string; + name: string; + url: string; + username: string; + password: string; + tlsClientCertData: string; + tlsClientCertKey: string; + insecure: boolean; + enableLfs: boolean; + proxy: string; + project?: string; + }): Promise { + return requests + .put(`/repositories/${encodeURIComponent(url)}`) + .send({type, name, repo: url, username, password, tlsClientCertData, tlsClientCertKey, insecure, enableLfs, proxy, project}) + .then(res => res.body as models.Repository); + } + public createSSH({ type, name,