Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui): Make UI errors recoverable. Fixes #3666 #3674

Merged
merged 12 commits into from
Aug 6, 2020

Conversation

alexec
Copy link
Contributor

@alexec alexec commented Aug 5, 2020

Checklist:

  • Either (a) I've created an enhancement proposal and discussed it with the community, (b) this is a bug fix, or (c) this is a chore.
  • The title of the PR is (a) conventional, (b) states what changed, and (c) suffixes the related issues number. E.g. "fix(controller): Updates such and such. Fixes #1234".
  • I've signed the CLA.
  • I have written unit and/or e2e tests for my change. PRs without these are unlikely to be merged. N/A
  • My builds are green. Try syncing with master if they are not.
  • My organization is added to USERS.md.

I recommend the reviewer and I sit down and go over the changes.

Goals

The goal of this PR is to make the UI usable under error scenarios. Typically if an error occurs, we throw it and a global <ErrorBoundary/> render a generic "ops!" page. The user must then reload. This looses the user's context as well as any data entered into forms - very annoying.

Errors are now more localised in the interface. E.g. an error listing workflows just shows the error and keeps the page usable. The user can then change their search parameters. If the error was a 403, this would most useful.

Changes

  • Refactored handling of managed namespace in UI. Rather than have each component enforce this, it is done in <App/>. This simplifies many component.
  • Add automatic redirection to the correct URLs for managed namespaces.
  • Add malformed-*.yaml manifests to aid testing with malformed manifests.
  • Removed throw this.state.error and replaced with <ErrorNotice/> so that the UI remains usable on error.
  • <ResourceEditor/> now displays submission error in a way so that you can edit and retry.
  • Simplified history by adding saveHistory functions and this.url =.

Testing Notes

I'm not expecting the reviewers to do this - this is for my notes.

Does the UI stay usable?

  • Try listing and deep-linking malformed resources. UI should still be usable.
  • List each type of resource.
  • Creating each type of resource (both valid and malformed). Check namespace is correct.
  • Deleting each type of resource. Check namespace is correct.
  • Submitting each type of submittable resources. Check the correct namespace is used.

Can we deal with really bad errors?

  • Run yarn start and turn off Argo Server so that you get gateway errors, try all pages.

Does login still work?

  • Run in make AUTH_MODE=client start and verify that you are redirect to login page.

Does namespace preference still work?

  • Run in make NAMESPACED=false start clear local storage and verify no redirection occurs (you should just list the at cluster scope).
  • Change namespace in each page and verify that your local preference (stored in local storage) is updated.

Known Issues

  • The namespace preference does not affect URLs unless you reload the page. Fix to this is complex vs benefit IMHO.

@@ -15,7 +15,7 @@ interface Props<T> {
value: T;
readonly?: boolean;
editing?: boolean;
onSubmit?: (value: T) => void;
onSubmit?: (value: T) => Promise<any>;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small change, big impact

import {InputFilter} from './input-filter';

interface Props {
Copy link
Contributor Author

@alexec alexec Aug 5, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this component has always been a big buggy - so now it is dumb

return Observable.create((observer: Observer<any>) => {
const eventSource = new EventSource(url);
let opened = false;
eventSource.onopen = () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is canonical way to do this, I hope is more robust, certainly surfaces errors correctly

@@ -15,7 +15,7 @@ data:
staticClients:
- id: argo-server
redirectURIs:
- https://localhost:2746/oauth2/callback
- http://localhost:2746/oauth2/callback
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix

@@ -1,2 +1,2 @@
controller: ALWAYS_OFFLOAD_NODE_STATUS=${ALWAYS_OFFLOAD_NODE_STATUS} OFFLOAD_NODE_STATUS_TTL=30s WORKFLOW_GC_PERIOD=30s UPPERIO_DB_DEBUG=1 ARCHIVED_WORKFLOW_GC_PERIOD=30s ./dist/workflow-controller --executor-image argoproj/argoexec:${VERSION} --namespaced --loglevel ${LOG_LEVEL}
argo-server: UPPERIO_DB_DEBUG=1 ./dist/argo --loglevel ${LOG_LEVEL} server --namespaced --auth-mode ${AUTH_MODE} --secure=$SECURE
controller: ALWAYS_OFFLOAD_NODE_STATUS=${ALWAYS_OFFLOAD_NODE_STATUS} OFFLOAD_NODE_STATUS_TTL=30s WORKFLOW_GC_PERIOD=30s UPPERIO_DB_DEBUG=1 ARCHIVED_WORKFLOW_GC_PERIOD=30s ./dist/workflow-controller --executor-image argoproj/argoexec:${VERSION} --namespaced=${NAMESPACED} --namespace ${NAMESPACE} --loglevel ${LOG_LEVEL}
Copy link
Contributor Author

@alexec alexec Aug 5, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

run in non-namespaced mode in dev

@@ -110,7 +98,17 @@ export class App extends React.Component<{}, {version?: Version; popupProps: Pop

public componentDidMount() {
this.popupManager.popupProps.subscribe(popupProps => this.setState({popupProps}));
services.info.getVersion().then(version => this.setState({version}));
services.info
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔑 This is one of the key changes in this PR. This allows us to know what managed namespace we're running in almost immediately. We use this to create redirects that mean that child components no longer need to know about namespaces.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great change

.then(version => this.setState({version}))
.then(() => services.info.getInfo())
.then(info => this.setState({namespace: info.managedNamespace || Utils.getCurrentNamespace() || ''}))
.catch(error => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I make an exception here - report this error in a popup. We still need to be able to get to the login page on 401.

<Redirect from={timelineUrl} to={uiUrl('workflows')} />
<Layout navItems={navItems} version={() => <>{this.state.version ? this.state.version.version : 'unknown'}</>}>
<Notifications notifications={this.notificationsManager.notifications} />

<ErrorBoundary>
Copy link
Contributor Author

@alexec alexec Aug 5, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move this inside the layout, so we can keep button even in case of a "ops!" error:

error-bondary

<Route exact={true} strict={true} path={timelineUrl}>
<Redirect to={workflowsUrl} />
</Route>
{this.state.namespace && (
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these are the redirects, it seems you should use exact and strict if you can

@@ -20,9 +20,6 @@ import {ArchivedWorkflowFilters} from '../archived-workflow-filters/archived-wor

interface State {
pagination: Pagination;
loading: boolean;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't need these anymore, infer from other fields

@@ -62,42 +56,32 @@ export class ArchivedWorkflowList extends BasePage<RouteComponentProps<any>, Sta
}

public render() {
if (this.state.loading) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move error shims lower in the dom, so more the page remains responsive, you'll see this several times in this PR

}

private saveHistory() {
this.url = uiUrl('archived-workflows/' + this.state.namespace + '?' + this.filterParams.toString());
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you'll see saveHistory several times as a new pattern for - saving history

loading: false
});
Utils.setCurrentNamespace(newNamespace);
this.saveHistory
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

part of the saveHistory pattern is to invoke it after only on successful state update, again - you'll see this as a repeating pattern

services.clusterWorkflowTemplate
.get(this.name)
.then(template => this.setState({template}))
.then(() => services.info.getInfo())
.then(info => this.setState({namespace: info.managedNamespace || Utils.getCurrentNamespace() || 'default'}))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cluster templates an the one type where we cannot infer the namespace from the URL

.then(clusterWorkflowTemplate => props.onChange(clusterWorkflowTemplate))
.catch(err => props.onError(err));
}}
onSubmit={(value: WorkflowTemplate) =>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onSubmit now requires a promise which we display in the UI

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2020-08-05 at 11 07 21

{props.error.message}
export const ErrorNotice = (props: {style?: CSSProperties; error: Error & {response?: {body: {message?: string}}}}) => (
<Notice style={props.style}>
<PhaseIcon value='Error' /> {props.error.message || 'Unknown error. Open your browser error console for more information.'}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there are rare cases where we don't get info in the error - ask the user to look in the console

@@ -135,6 +133,9 @@ export class ArchivedWorkflowDetails extends BasePage<RouteComponentProps<any>,
}

private renderArchivedWorkflowDetails() {
if (this.state.error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2020-08-05 at 10 58 13

@@ -212,12 +213,11 @@ export class ArchivedWorkflowDetails extends BasePage<RouteComponentProps<any>,
},
spec: this.state.workflow.spec
}}
onSubmit={(value: Workflow) => {
onSubmit={(value: Workflow) =>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2020-08-05 at 10 59 04

}

private renderWorkflows() {
if (this.state.error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2020-08-05 at 11 00 33

@@ -102,10 +99,13 @@ export class ClusterWorkflowTemplateDetails extends BasePage<RouteComponentProps
}

private renderClusterWorkflowTemplate() {
if (this.state.error) {
Copy link
Contributor Author

@alexec alexec Aug 5, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2020-08-05 at 11 04 43

@@ -90,12 +88,13 @@ export class CronWorkflowDetails extends BasePage<RouteComponentProps<any>, Stat
}

private renderCronWorkflow() {
if (this.state.error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2020-08-05 at 11 07 52

}

private renderCronWorkflows() {
if (this.state.error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2020-08-05 at 11 08 24

@@ -72,12 +70,11 @@ export const CronWorkflowSummaryPanel = (props: Props) => {
// magic - we get the latest from the server and then apply the changes from the rendered version to this
const original = props.cronWorkflow;
const patch = jsonMergePatch.generate(original, value) || {};
services.cronWorkflows
return services.cronWorkflows
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2020-08-05 at 11 09 31

return (
<Page title='User Info' toolbar={{breadcrumbs: [{title: 'User Info'}]}}>
<div className='argo-container'>
{this.state.error && <ErrorNotice error={this.state.error} />}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2020-08-05 at 11 10 25

@@ -101,10 +99,13 @@ export class WorkflowTemplateDetails extends BasePage<RouteComponentProps<any>,
}

private renderWorkflowTemplate() {
if (this.state.error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2020-08-05 at 11 10 59

}

private renderTemplates() {
if (this.state.error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2020-08-05 at 11 11 40

@@ -34,12 +33,11 @@ export const WorkflowTemplateSummaryPanel = (props: Props) => {
<div className='white-box__details'>
<ResourceEditor
value={props.template}
onSubmit={(value: WorkflowTemplate) => {
onSubmit={(value: WorkflowTemplate) =>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2020-08-05 at 11 13 17

@@ -290,10 +282,12 @@ export class WorkflowsList extends BasePage<RouteComponentProps<any>, State> {
}

private renderWorkflows() {
if (this.state.error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2020-08-05 at 11 15 25

@alexec alexec changed the title feat(ui): Improve error handling in UI. Fixes #3666 feat(ui): Make UI errors recoverable. Fixes #3666 Aug 5, 2020
@alexec alexec marked this pull request as ready for review August 5, 2020 18:20
Copy link
Member

@simster7 simster7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The screenshots look great!

@@ -110,7 +98,17 @@ export class App extends React.Component<{}, {version?: Version; popupProps: Pop

public componentDidMount() {
this.popupManager.popupProps.subscribe(popupProps => this.setState({popupProps}));
services.info.getVersion().then(version => this.setState({version}));
services.info
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great change

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants