Skip to content

Commit

Permalink
Issue #446 - Improve data loading errors notification
Browse files Browse the repository at this point in the history
  • Loading branch information
Alexander Matyushentsev authored and Alexander Matyushentsev committed Aug 2, 2018
1 parent a930b4f commit 7c60ff0
Show file tree
Hide file tree
Showing 9 changed files with 431 additions and 355 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"@types/react-router": "^4.0.27",
"@types/react-router-dom": "^4.2.3",
"@types/superagent": "^3.5.7",
"argo-ui": "^2.1.1-alpha9",
"argo-ui": "^2.1.1-alpha12",
"awesome-typescript-loader": "^3.4.1",
"babel-core": "^6.26.0",
"babel-loader": "^7.1.3",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
@import 'node_modules/argo-ui/bundle/app/shared/styles/config';

.application-details {
position: relative;
padding: 1em;
overflow-x: auto;
overflow-y: auto;
min-height: calc(100vh - 2 * 50px - 115px);

&__refreshing-label {
position: absolute;
top: 0;
left: 0;
background-color: $argo-color-gray-4;
border: 1px solid $argo-color-gray-5;
border-radius: 5px;
padding: 0 5px;
font-size: 0.6em;
}

&__tab-content-full-height {
height: calc(100vh - 2 * 50px);
div.row, div.columns {
Expand Down

Large diffs are not rendered by default.

222 changes: 99 additions & 123 deletions src/app/applications/components/applications-list/applications-list.tsx

Large diffs are not rendered by default.

89 changes: 35 additions & 54 deletions src/app/settings/components/clusters-list/clusters-list.tsx
Original file line number Diff line number Diff line change
@@ -1,62 +1,43 @@
import { MockupList } from 'argo-ui';
import * as React from 'react';
import { RouteComponentProps } from 'react-router';

import { ConnectionStateIcon, Page } from '../../../shared/components';
import { ConnectionStateIcon, DataLoader, Page } from '../../../shared/components';

import * as models from '../../../shared/models';
import { services } from '../../../shared/services';

export class ClustersList extends React.Component<RouteComponentProps<any>, { clusters: models.Cluster[] }> {

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

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

public render() {
return (
<Page title='Clusters' toolbar={{ breadcrumbs: [{title: 'Settings', path: '/settings' }, {title: 'Clusters'}] }}>
<div className='repos-list'>
<div className='argo-container'>
{this.state.clusters ? (
this.state.clusters.length > 0 && (
<div className='argo-table-list'>
<div className='argo-table-list__head'>
<div className='row'>
<div className='columns small-3'>NAME</div>
<div className='columns small-6'>URL</div>
<div className='columns small-3'>CONNECTION STATUS</div>
</div>
export const ClustersList = () => (
<Page title='Clusters' toolbar={{ breadcrumbs: [{title: 'Settings', path: '/settings' }, {title: 'Clusters'}] }}>
<div className='repos-list'>
<div className='argo-container'>
<DataLoader load={() => services.clustersService.list()}>
{(clusters: models.Cluster[]) => (
clusters.length > 0 && (
<div className='argo-table-list'>
<div className='argo-table-list__head'>
<div className='row'>
<div className='columns small-3'>NAME</div>
<div className='columns small-6'>URL</div>
<div className='columns small-3'>CONNECTION STATUS</div>
</div>
{this.state.clusters.map((cluster) => (
<div className='argo-table-list__row' key={cluster.server}>
<div className='row'>
<div className='columns small-3'>
<i className='icon argo-icon-hosts'/> {cluster.name}
</div>
<div className='columns small-6'>
{cluster.server}
</div>
<div className='columns small-3'>
<ConnectionStateIcon state={cluster.connectionState}/> {cluster.connectionState.status}
</div>
</div>
{clusters.map((cluster) => (
<div className='argo-table-list__row' key={cluster.server}>
<div className='row'>
<div className='columns small-3'>
<i className='icon argo-icon-hosts'/> {cluster.name}
</div>
<div className='columns small-6'>
{cluster.server}
</div>
<div className='columns small-3'>
<ConnectionStateIcon state={cluster.connectionState}/> {cluster.connectionState.status}
</div>
</div>
))}
</div> )
) : <MockupList height={50} marginTop={30}/>}
</div>
</div>
</Page>
);
}

private async reloadClusters() {
this.setState({ clusters: await services.clustersService.list() });
}
}
</div>
))}
</div> )
)}
</DataLoader>
</div>
</div>
</Page>
);
34 changes: 12 additions & 22 deletions src/app/settings/components/repos-list/repos-list.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { DropDownMenu, MockupList, NotificationType, SlidingPanel } from 'argo-ui';
import { DropDownMenu, 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, ErrorNotification, FormField, Page } from '../../../shared/components';
import { ConnectionStateIcon, DataLoader, ErrorNotification, FormField, Page } from '../../../shared/components';
import { AppContext } from '../../../shared/context';
import * as models from '../../../shared/models';
import { services } from '../../../shared/services';
Expand All @@ -17,23 +17,15 @@ interface NewRepoParams {
password: string;
}

export class ReposList extends React.Component<RouteComponentProps<any>, { repos: models.Repository[] }> {
export class ReposList extends React.Component<RouteComponentProps<any>> {
public static contextTypes = {
router: PropTypes.object,
apis: PropTypes.object,
history: PropTypes.object,
};

private formApi: FormApi;

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

public componentDidMount() {
this.reloadRepos();
}
private loader: DataLoader;

public render() {
return (
Expand All @@ -55,16 +47,17 @@ export class ReposList extends React.Component<RouteComponentProps<any>, { repos
</div>
</div>
<div className='argo-container'>
{this.state.repos ? (
this.state.repos.length > 0 && (
<DataLoader load={() => services.reposService.list()} ref={(loader) => this.loader = loader}>
{(repos: models.Repository[]) => (
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) => (
{repos.map((repo) => (
<div className='argo-table-list__row' key={repo.repo}>
<div className='row'>
<div className='columns small-9'>
Expand All @@ -83,7 +76,8 @@ export class ReposList extends React.Component<RouteComponentProps<any>, { repos
</div>
))}
</div> )
) : <MockupList height={50} marginTop={30}/>}
)}
</DataLoader>
</div>
</div>
<SlidingPanel isShown={this.showConnectRepo} onClose={() => this.showConnectRepo = false} header={(
Expand Down Expand Up @@ -124,7 +118,7 @@ export class ReposList extends React.Component<RouteComponentProps<any>, { repos
try {
await services.reposService.create(params);
this.showConnectRepo = false;
this.reloadRepos();
this.loader.reload();
} catch (e) {
this.appContext.apis.notifications.show({
content: <ErrorNotification title='Unable to connect repository' e={e}/>,
Expand All @@ -133,16 +127,12 @@ export class ReposList extends React.Component<RouteComponentProps<any>, { repos
}
}

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();
this.loader.reload();
}
}

Expand Down
113 changes: 113 additions & 0 deletions src/app/shared/components/data-loader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { NotificationType } from 'argo-ui';
import * as PropTypes from 'prop-types';
import * as React from 'react';
import { Observable, Subscription } from 'rxjs';
import { AppContext } from '../context';
import { ErrorNotification } from './error-notification';

interface LoaderProps<I, D> {
load: (input: I) => Promise<D>;
input?: I;
loadingRenderer?: React.ComponentType;
errorRenderer?: (children: React.ReactNode) => React.ReactNode;
children: (data: D) => React.ReactNode;
dataChanges?: Observable<D>;
}

export class DataLoader<D = {}, I = {}> extends React.Component<LoaderProps<I, D>, { loading: boolean; data: D; error: boolean; input: I}> {
public static contextTypes = {
router: PropTypes.object,
apis: PropTypes.object,
};

public static getDerivedStateFromProps(nextProps: LoaderProps<any, any>, prevState: { input: any }) {
if (JSON.stringify(nextProps.input) !== JSON.stringify(prevState.input)) {
return { data: null as any, input: nextProps.input };
}
return null;
}

private subscription: Subscription;

constructor(props: LoaderProps<I, D>) {
super(props);
this.state = { loading: false, error: false, data: null, input: props.input };
}

public getData() {
return this.state.data;
}

public setData(data: D) {
return this.setState({ data });
}

public componentDidMount() {
this.loadData();
this.subscribe();
}

public componentDidUpdate(prevProps: LoaderProps<I, D>) {
this.loadData();
if (this.props.dataChanges !== prevProps.dataChanges) {
this.subscribe();
}
}

public componentWillUnmount() {
this.ensureUnsubscribed();
}

public render() {
const style: React.CSSProperties = {padding: '0.5em', textAlign: 'center'};
if (this.state.error) {
const error = <p style={style}>Failed to load data, please <a onClick={() => this.reload()}>try again</a>.</p>;
if (this.props.errorRenderer) {
return this.props.errorRenderer(error);
}
return error;
}
if (this.state.data) {
return this.props.children(this.state.data);
}
return this.props.loadingRenderer ? <this.props.loadingRenderer/> : <p style={style}>Loading...</p>;
}

public reload() {
this.setState({ data: null, error: false });
}

private async loadData() {
if (!this.state.error && !this.state.loading && this.state.data == null) {
this.setState({ error: false, loading: true });
try {
const data = await this.props.load(this.props.input);
this.setState({ data, loading: false });
} catch (e) {
this.setState({ error: true, loading: false });
this.appContext.apis.notifications.show({
content: <ErrorNotification title='Unable to load data' e={e}/>,
type: NotificationType.Error,
});
}
}
}

private subscribe() {
this.ensureUnsubscribed();
if (this.props.dataChanges) {
this.subscription = this.props.dataChanges.subscribe((data: D) => this.setState({ data }));
}
}

private ensureUnsubscribed() {
if (this.subscription) {
this.subscription.unsubscribe();
}
this.subscription = null;
}

private get appContext(): AppContext {
return this.context as AppContext;
}
}
1 change: 1 addition & 0 deletions src/app/shared/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './colors';
export * from './connection-state-icon';
export * from './error-notification';
export * from './ticket';
export * from './data-loader';
6 changes: 3 additions & 3 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,9 @@ are-we-there-yet@~1.1.2:
delegates "^1.0.0"
readable-stream "^2.0.6"

argo-ui@^2.1.1-alpha9:
version "2.1.1-alpha9"
resolved "https://registry.yarnpkg.com/argo-ui/-/argo-ui-2.1.1-alpha9.tgz#c5e08cb244049a6ab18b59a16e4646179365a976"
argo-ui@^2.1.1-alpha12:
version "2.1.1-alpha12"
resolved "https://registry.yarnpkg.com/argo-ui/-/argo-ui-2.1.1-alpha12.tgz#81db977e6c5bc32344653acffe38c65c7484f7a6"
dependencies:
aws-sdk "^2.188.0"
body-parser "^1.18.2"
Expand Down

0 comments on commit 7c60ff0

Please sign in to comment.