Skip to content

Commit

Permalink
Issue #241 - Repositories list page
Browse files Browse the repository at this point in the history
  • Loading branch information
Alexander Matyushentsev authored and Alexander Matyushentsev committed Jun 4, 2018
1 parent 2ba7eb8 commit 929f30c
Show file tree
Hide file tree
Showing 13 changed files with 290 additions and 11 deletions.
11 changes: 8 additions & 3 deletions 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';
Expand All @@ -12,17 +11,23 @@ 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<RouteComponentProps<any>>, 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 },
};

const navItems = [{
title: 'Apps',
path: '/applications',
iconClassName: 'argo-icon-application',
}, {
title: 'Repositories',
path: '/repositories',
iconClassName: 'argo-icon-git',
}, {
title: 'Help',
path: '/help',
Expand Down
Expand Up @@ -86,7 +86,7 @@ export class ApplicationDetails extends React.Component<
public render() {
const kindsSet = new Set<string>();
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);
Expand Down
6 changes: 2 additions & 4 deletions 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 = '';
Expand Down
35 changes: 35 additions & 0 deletions 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;
}
}
}
157 changes: 157 additions & 0 deletions 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<RouteComponentProps<any>, { repos: models.Repository[] }> {
public static contextTypes = {
router: PropTypes.object,
apis: PropTypes.object,
};

private formApi: FormApi;

constructor(props: RouteComponentProps<any>) {
super(props);
this.state = { repos: null };
}

public componentDidMount() {
this.reloadRepos();
}

public render() {
return (
<Page title='Repositories' toolbar={{ breadcrumbs: [{title: 'Repositories'}] }}>
<div className='repos-list'>
<div className='row repos-list__top-panel argo-container'>

<div className='columns small-7'>
<i className='argo-icon-axcluster'/>
</div>

<div className='columns small-5'>
<p>Connect your repo to deploy apps.</p>
<button className='argo-button argo-button--base' onClick={() => this.showConnectRepo = true} >Connect Repo</button>
<p>Successfully connected your repo?</p>
<button className='argo-button argo-button--base'>Create Apps</button>
</div>
</div>
<div className='argo-container'>
{this.state.repos ? (
this.state.repos.length > 0 && (
<div className='argo-table-list'>
<div className='argo-table-list__head'>
<div className='row'>
<div className='columns small-9'>REPOSITORY</div>
<div className='columns small-3'>CONNECTION STATUS</div>
</div>
</div>
{this.state.repos.map((repo) => (
<div className='argo-table-list__row' key={repo.repo}>
<div className='row'>
<div className='columns small-9'>
<i className='icon argo-icon-git'/> {repo.repo}
</div>
<div className='columns small-3'>
<ConnectionStateIcon state={repo.connectionState}/> {repo.connectionState.status}
<DropDownMenu anchor={() => <button className='argo-button argo-button--light argo-button--lg argo-button--short'>
<i className='fa fa-ellipsis-v'/>
</button>} items={[{
title: 'Disconnect',
action: () => this.disconnectRepo(repo.repo),
}]}/>
</div>
</div>
</div>
))}
</div> )
) : <MockupList height={50} marginTop={30}/>}
</div>
</div>
<SlidingPanel isShown={this.showConnectRepo} onClose={() => this.showConnectRepo = false} header={(
<div>
<button className='argo-button argo-button--base' onClick={() => this.formApi.submitForm(null)}>
Create
</button> <button onClick={() => this.showConnectRepo = false} className='argo-button argo-button--base-o'>
Cancel
</button>
</div>
)}>
<h4>Connect Git repo</h4>
<Form onSubmit={(params) => this.connectRepo(params as NewRepoParams)}
getApi={(api) => this.formApi = api}
validateError={(params: NewRepoParams) => ({
url: !params.url && 'Repo URL is required',
})}>
{(formApi) => (
<form onSubmit={formApi.submitForm} role='form' className='width-control'>
<div className='argo-form-row'>
<FormField formApi={formApi} label='Repository URL' field='url' component={Text}/>
</div>
<div className='argo-form-row'>
<FormField formApi={formApi} label='Username' field='username' component={Text}/>
</div>
<div className='argo-form-row'>
<FormField formApi={formApi} label='Password' field='password' component={Text}/>
</div>
</form>
)}
</Form>
</SlidingPanel>
</Page>
);
}

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;
}
}
5 changes: 5 additions & 0 deletions src/app/repos/index.ts
@@ -0,0 +1,5 @@
import { ReposList } from './components/repos-list/repos-list';

export default {
component: ReposList,
};
3 changes: 3 additions & 0 deletions 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';
25 changes: 25 additions & 0 deletions 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 <i title={props.state.message || props.state.status} className={className} style={{ color }} />;
};
2 changes: 2 additions & 0 deletions 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';
34 changes: 32 additions & 2 deletions src/app/shared/models.ts
@@ -1,12 +1,12 @@
import { models } from 'argo-ui';

export interface ApplicationList {
interface ItemsList<T> {
/**
* 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.
Expand All @@ -15,6 +15,8 @@ export interface ApplicationList {
metadata: models.ListMeta;
}

export interface ApplicationList extends ItemsList<Application> {}

export interface SyncOperation {
revision: string;
prune: boolean;
Expand Down Expand Up @@ -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<Repository> {}

export interface Cluster {
server: string;
connectionState: ConnectionState;
}

export interface ClusterList extends ItemsList<Cluster> {}
3 changes: 3 additions & 0 deletions 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(),
};
16 changes: 16 additions & 0 deletions 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<models.Repository[]> {
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<models.Repository> {
return requests.post('/repositories').send({ repo: url, username, password }).then((res) => res.body as models.Repository);
}

public delete(url: string): Promise<models.Repository> {
return requests.delete(`/repositories/${encodeURIComponent(url)}`).send().then((res) => res.body as models.Repository);
}
}
2 changes: 1 addition & 1 deletion src/app/webpack.config.js
Expand Up @@ -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'
},

Expand Down

0 comments on commit 929f30c

Please sign in to comment.