From 929f30c58be271714b41f92403df07076f5b3aae Mon Sep 17 00:00:00 2001 From: Alexander Matyushentsev Date: Mon, 4 Jun 2018 11:57:28 -0700 Subject: [PATCH] Issue #241 - Repositories list page --- src/app/app.tsx | 11 +- .../application-details.tsx | 2 +- src/app/applications/components/utils.tsx | 6 +- .../components/repos-list/repos-list.scss | 35 ++++ .../components/repos-list/repos-list.tsx | 157 ++++++++++++++++++ src/app/repos/index.ts | 5 + src/app/shared/components/colors.ts | 3 + .../components/connection-state-icon.tsx | 25 +++ src/app/shared/components/index.ts | 2 + src/app/shared/models.ts | 34 +++- src/app/shared/services/index.ts | 3 + src/app/shared/services/repo-service.ts | 16 ++ src/app/webpack.config.js | 2 +- 13 files changed, 290 insertions(+), 11 deletions(-) create mode 100644 src/app/repos/components/repos-list/repos-list.scss create mode 100644 src/app/repos/components/repos-list/repos-list.tsx create mode 100644 src/app/repos/index.ts create mode 100644 src/app/shared/components/colors.ts create mode 100644 src/app/shared/components/connection-state-icon.tsx create mode 100644 src/app/shared/services/repo-service.ts diff --git a/src/app/app.tsx b/src/app/app.tsx index d143f67fb4ba..17bd2b722295 100644 --- a/src/app/app.tsx +++ b/src/app/app.tsx @@ -1,8 +1,7 @@ import { Layout, NotificationInfo, Notifications, NotificationsManager, Popup, PopupManager, PopupProps } from 'argo-ui'; +import createHistory from 'history/createBrowserHistory'; import * as PropTypes from 'prop-types'; import * as React from 'react'; - -import createHistory from 'history/createBrowserHistory'; import { Redirect, Route, RouteComponentProps, Router, Switch } from 'react-router'; import requests from './shared/services/requests'; @@ -12,10 +11,12 @@ export const history = createHistory(); import applications from './applications'; import help from './help'; import login from './login'; +import repos from './repos'; const routes: {[path: string]: { component: React.ComponentType>, noLayout?: boolean } } = { - '/applications': { component: applications.component }, '/login': { component: login.component as any, noLayout: true }, + '/applications': { component: applications.component }, + '/repositories': { component: repos.component }, '/help': { component: help.component }, }; @@ -23,6 +24,10 @@ const navItems = [{ title: 'Apps', path: '/applications', iconClassName: 'argo-icon-application', +}, { + title: 'Repositories', + path: '/repositories', + iconClassName: 'argo-icon-git', }, { title: 'Help', path: '/help', diff --git a/src/app/applications/components/application-details/application-details.tsx b/src/app/applications/components/application-details/application-details.tsx index 36b4912cecf4..e64b1eb7795c 100644 --- a/src/app/applications/components/application-details/application-details.tsx +++ b/src/app/applications/components/application-details/application-details.tsx @@ -86,7 +86,7 @@ export class ApplicationDetails extends React.Component< public render() { const kindsSet = new Set(); if (this.state.application) { - const items: (appModels.ResourceNode | appModels.ResourceState)[] = [...this.state.application.status.comparisonResult.resources]; + const items: (appModels.ResourceNode | appModels.ResourceState)[] = [...this.state.application.status.comparisonResult.resources || []]; while (items.length > 0) { const next = items.pop(); const {resourceNode} = AppUtils.getStateAndNode(next); diff --git a/src/app/applications/components/utils.tsx b/src/app/applications/components/utils.tsx index 29decda370d6..2381bc81b46c 100644 --- a/src/app/applications/components/utils.tsx +++ b/src/app/applications/components/utils.tsx @@ -1,9 +1,7 @@ import * as React from 'react'; -import * as appModels from '../../shared/models'; -const ARGO_SUCCESS_COLOR = '#18BE94'; -const ARGO_FAILED_COLOR = '#E96D76'; -const ARGO_RUNNING_COLOR = '#0DADEA'; +import { ARGO_FAILED_COLOR, ARGO_RUNNING_COLOR, ARGO_SUCCESS_COLOR } from '../../shared/components'; +import * as appModels from '../../shared/models'; export const ComparisonStatusIcon = ({status}: { status: appModels.ComparisonStatus }) => { let className = ''; diff --git a/src/app/repos/components/repos-list/repos-list.scss b/src/app/repos/components/repos-list/repos-list.scss new file mode 100644 index 000000000000..6bbec8442351 --- /dev/null +++ b/src/app/repos/components/repos-list/repos-list.scss @@ -0,0 +1,35 @@ +@import 'node_modules/argo-ui/bundle/app/shared/styles/config'; + +.repos-list { + &__top-panel { + padding: 1em; + + & > .columns:first-child { + font-size: 8em; + text-align: center; + } + + & > .columns:last-child { + text-align: center; + border-left: 2px solid $argo-color-gray-4; + + & > p { + margin-bottom: 0; + margin-top: 24px; + &:first-of-type { + font-size: 1.25em; + } + } + + & > button { + width: 15em; + } + } + } + + .argo-table-list { + .argo-dropdown { + float: right; + } + } +} diff --git a/src/app/repos/components/repos-list/repos-list.tsx b/src/app/repos/components/repos-list/repos-list.tsx new file mode 100644 index 000000000000..11cec9b8cabe --- /dev/null +++ b/src/app/repos/components/repos-list/repos-list.tsx @@ -0,0 +1,157 @@ +import { DropDownMenu, MockupList, NotificationType, SlidingPanel } from 'argo-ui'; +import * as PropTypes from 'prop-types'; +import * as React from 'react'; +import { Form, FormApi, Text } from 'react-form'; +import { RouteComponentProps } from 'react-router'; + +import { ConnectionStateIcon, FormField, Page } from '../../../shared/components'; +import { AppContext } from '../../../shared/context'; +import * as models from '../../../shared/models'; +import { services } from '../../../shared/services'; + +require('./repos-list.scss'); + +interface NewRepoParams { + url: string; + username: string; + password: string; +} + +export class ReposList extends React.Component, { repos: models.Repository[] }> { + public static contextTypes = { + router: PropTypes.object, + apis: PropTypes.object, + }; + + private formApi: FormApi; + + constructor(props: RouteComponentProps) { + super(props); + this.state = { repos: null }; + } + + public componentDidMount() { + this.reloadRepos(); + } + + public render() { + return ( + +
+
+ +
+ +
+ +
+

Connect your repo to deploy apps.

+ +

Successfully connected your repo?

+ +
+
+
+ {this.state.repos ? ( + this.state.repos.length > 0 && ( +
+
+
+
REPOSITORY
+
CONNECTION STATUS
+
+
+ {this.state.repos.map((repo) => ( +
+
+
+ {repo.repo} +
+
+ {repo.connectionState.status} + } items={[{ + title: 'Disconnect', + action: () => this.disconnectRepo(repo.repo), + }]}/> +
+
+
+ ))} +
) + ) : } +
+
+ this.showConnectRepo = false} header={( +
+ +
+ )}> +

Connect Git repo

+
this.connectRepo(params as NewRepoParams)} + getApi={(api) => this.formApi = api} + validateError={(params: NewRepoParams) => ({ + url: !params.url && 'Repo URL is required', + })}> + {(formApi) => ( + +
+ +
+
+ +
+
+ +
+
+ )} + +
+
+ ); + } + + private async connectRepo(params: NewRepoParams) { + try { + await services.reposService.create(params); + this.showConnectRepo = false; + this.reloadRepos(); + } catch (e) { + this.appContext.apis.notifications.show({ + content: e.response && e.response.text || 'Internal error', + type: NotificationType.Error, + }); + } + } + + private async reloadRepos() { + this.setState({ repos: await services.reposService.list() }); + } + + private async disconnectRepo(repo: string) { + const confirmed = await this.appContext.apis.popup.confirm( + 'Disconnect repository', `Are you sure you want to disconnect '${repo}'?`); + if (confirmed) { + await services.reposService.delete(repo); + this.reloadRepos(); + } + } + + private get showConnectRepo() { + return new URLSearchParams(this.props.location.search).get('addRepo') === 'true'; + } + + private set showConnectRepo(val: boolean) { + this.appContext.router.history.push(`${this.props.match.url}?addRepo=${val}`); + } + + private get appContext(): AppContext { + return this.context as AppContext; + } +} diff --git a/src/app/repos/index.ts b/src/app/repos/index.ts new file mode 100644 index 000000000000..16dd5d1357d2 --- /dev/null +++ b/src/app/repos/index.ts @@ -0,0 +1,5 @@ +import { ReposList } from './components/repos-list/repos-list'; + +export default { + component: ReposList, +}; diff --git a/src/app/shared/components/colors.ts b/src/app/shared/components/colors.ts new file mode 100644 index 000000000000..42225ddb0edd --- /dev/null +++ b/src/app/shared/components/colors.ts @@ -0,0 +1,3 @@ +export const ARGO_SUCCESS_COLOR = '#18BE94'; +export const ARGO_FAILED_COLOR = '#E96D76'; +export const ARGO_RUNNING_COLOR = '#0DADEA'; diff --git a/src/app/shared/components/connection-state-icon.tsx b/src/app/shared/components/connection-state-icon.tsx new file mode 100644 index 000000000000..9b229010e8f3 --- /dev/null +++ b/src/app/shared/components/connection-state-icon.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; +import * as models from '../models'; + +import { ARGO_FAILED_COLOR, ARGO_RUNNING_COLOR, ARGO_SUCCESS_COLOR } from './colors'; + +export const ConnectionStateIcon = (props: { state: models.ConnectionState }) => { + let className = ''; + let color = ''; + + switch (props.state.status) { + case models.ConnectionStatuses.Successful: + className = 'fa fa-check-circle'; + color = ARGO_SUCCESS_COLOR; + break; + case models.ConnectionStatuses.Failed: + className = 'fa fa-times'; + color = ARGO_FAILED_COLOR; + break; + case models.ConnectionStatuses.Unknown: + className = 'fa fa-exclamation-circle'; + color = ARGO_RUNNING_COLOR; + break; + } + return ; +}; diff --git a/src/app/shared/components/index.ts b/src/app/shared/components/index.ts index ba75f567e4f6..bad66960bd72 100644 --- a/src/app/shared/components/index.ts +++ b/src/app/shared/components/index.ts @@ -1,2 +1,4 @@ export * from './page'; export * from './form-field'; +export * from './colors'; +export * from './connection-state-icon'; diff --git a/src/app/shared/models.ts b/src/app/shared/models.ts index 6c1cc3acccde..4d8302995acb 100644 --- a/src/app/shared/models.ts +++ b/src/app/shared/models.ts @@ -1,12 +1,12 @@ import { models } from 'argo-ui'; -export interface ApplicationList { +interface ItemsList { /** * APIVersion defines the versioned schema of this representation of an object. * Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. */ apiVersion?: string; - items: Application[]; + items: T[]; /** * Kind is a string value representing the REST resource this object represents. * Servers may infer this from the endpoint the client submits requests to. @@ -15,6 +15,8 @@ export interface ApplicationList { metadata: models.ListMeta; } +export interface ApplicationList extends ItemsList {} + export interface SyncOperation { revision: string; prune: boolean; @@ -200,3 +202,31 @@ export interface AuthSettings { }[]; }; } + +export type ConnectionStatus = 'Unknown' | 'Successful' | 'Failed'; + +export const ConnectionStatuses = { + Unknown: 'Unknown' , + Failed: 'Failed' , + Successful: 'Successful', +}; + +export interface ConnectionState { + status: ConnectionStatus; + message: string; + attemptedAt: models.Time; +} + +export interface Repository { + repo: string; + connectionState: ConnectionState; +} + +export interface RepositoryList extends ItemsList {} + +export interface Cluster { + server: string; + connectionState: ConnectionState; +} + +export interface ClusterList extends ItemsList {} diff --git a/src/app/shared/services/index.ts b/src/app/shared/services/index.ts index 11cf4a754e57..b233813c1ac0 100644 --- a/src/app/shared/services/index.ts +++ b/src/app/shared/services/index.ts @@ -1,15 +1,18 @@ import { ApplicationsService } from './applications-service'; import { AuthService } from './auth-service'; +import { RepositoriesService } from './repo-service'; import { UserService } from './user-service'; export interface Services { applications: ApplicationsService; userService: UserService; authService: AuthService; + reposService: RepositoriesService; } export const services: Services = { applications: new ApplicationsService(), userService: new UserService(), authService: new AuthService(), + reposService: new RepositoriesService(), }; diff --git a/src/app/shared/services/repo-service.ts b/src/app/shared/services/repo-service.ts new file mode 100644 index 000000000000..0ada2a69ad83 --- /dev/null +++ b/src/app/shared/services/repo-service.ts @@ -0,0 +1,16 @@ +import * as models from '../models'; +import requests from './requests'; + +export class RepositoriesService { + public list(): Promise { + return requests.get('/repositories').then((res) => res.body as models.RepositoryList).then((list) => list.items || []); + } + + public create({url, username, password}: {url: string, username: string, password: string}): Promise { + return requests.post('/repositories').send({ repo: url, username, password }).then((res) => res.body as models.Repository); + } + + public delete(url: string): Promise { + return requests.delete(`/repositories/${encodeURIComponent(url)}`).send().then((res) => res.body as models.Repository); + } +} diff --git a/src/app/webpack.config.js b/src/app/webpack.config.js index b98d3c6781bd..54861fea30a5 100644 --- a/src/app/webpack.config.js +++ b/src/app/webpack.config.js @@ -9,7 +9,7 @@ const isProd = process.env.NODE_ENV === 'production'; const config = { entry: './src/app/index.tsx', output: { - filename: '[name].[chunkhash].js', + filename: '[name].[hash].js', path: __dirname + '/../../dist/app' },