From 416969730235593181ab88dc0d6efb80ff0e7902 Mon Sep 17 00:00:00 2001 From: Alex Collins Date: Tue, 22 Oct 2019 15:45:33 -0700 Subject: [PATCH] Enable prettier on UI source code (#2524) --- ui/.prettierrc | 8 + ui/package.json | 3 + ui/src/app/app.tsx | 172 +-- .../application-conditions.tsx | 24 +- .../application-create-panel.tsx | 559 +++++----- .../application-deployment-history.tsx | 76 +- .../revision-metadata-rows.tsx | 60 +- .../application-details.tsx | 981 ++++++++++-------- .../application-resource-list.tsx | 90 +- .../application-node-info.tsx | 106 +- .../application-operation-state.tsx | 146 ++- .../application-parameters.tsx | 363 ++++--- .../application-parameters/kustomize-image.ts | 12 +- .../application-parameters/kustomize.tsx | 79 +- .../vars-input-field.tsx | 14 +- .../application-resource-events.tsx | 17 +- .../application-resource-tree.tsx | 351 ++++--- .../node-update-animation.tsx | 12 +- .../application-resources-diff.tsx | 131 ++- .../application-status-panel.tsx | 104 +- .../revision-metadata-panel.tsx | 60 +- .../application-summary.tsx | 566 ++++++---- .../application-sync-panel.tsx | 210 ++-- .../components/application-urls.tsx | 40 +- .../components/applications-container.tsx | 10 +- .../applications-list/applications-filter.tsx | 180 ++-- .../applications-list/applications-list.tsx | 559 +++++----- .../applications-summary.tsx | 75 +- .../applications-list/applications-table.tsx | 119 ++- .../applications-list/applications-tiles.tsx | 202 ++-- .../applications-sync-panel.tsx | 149 +-- .../pod-logs-viewer/pod-logs-viewer.tsx | 24 +- .../applications/components/resource-icon.tsx | 12 +- ui/src/app/applications/components/utils.tsx | 81 +- ui/src/app/applications/index.ts | 4 +- ui/src/app/help/components/help.tsx | 60 +- ui/src/app/help/index.tsx | 4 +- ui/src/app/index.tsx | 6 +- ui/src/app/login/components/login.tsx | 86 +- ui/src/app/login/index.tsx | 4 +- .../components/certs-list/certs-list.tsx | 235 +++-- .../clusters-list/clusters-list.tsx | 57 +- .../project-details/project-details.tsx | 793 +++++++------- .../project-edit-panel/project-edit-panel.tsx | 275 ++--- .../project-events/project-events.tsx | 13 +- .../project-role-edit-panel.tsx | 56 +- .../project-role-groups-edit.tsx | 54 +- .../project-role-jwt-tokens.tsx | 123 ++- .../project-role-policies-edit.tsx | 158 +-- .../project-sync-windows-edit-panel.tsx | 84 +- .../project-sync-windows-edit.tsx | 321 ++++-- .../projects-list/projects-list.tsx | 134 +-- .../components/repos-list/repos-list.tsx | 329 +++--- .../components/settings-container.tsx | 28 +- .../settings-overview/settings-overview.tsx | 45 +- ui/src/app/settings/index.ts | 4 +- .../components/array-input/array-input.tsx | 53 +- .../autocomplete/autocomplete-field.tsx | 31 +- .../components/autocomplete/autocomplete.tsx | 28 +- .../components/checkbox/checkbox-field.tsx | 10 +- ui/src/app/shared/components/cluster.tsx | 21 +- ui/src/app/shared/components/colors.ts | 20 +- .../components/connection-state-icon.tsx | 6 +- .../editable-panel/editable-panel.tsx | 178 ++-- .../components/empty-state/empty-state.tsx | 4 +- .../components/events-list/events-list.tsx | 11 +- .../components/expandable/expandable.tsx | 8 +- ui/src/app/shared/components/index.ts | 2 +- .../app/shared/components/monaco-editor.tsx | 82 +- ui/src/app/shared/components/page.tsx | 42 +- .../shared/components/paginate/paginate.tsx | 84 +- ui/src/app/shared/components/query.tsx | 16 +- ui/src/app/shared/components/repo.tsx | 2 +- ui/src/app/shared/components/revision.tsx | 2 +- .../tags-input/tags-input-field.tsx | 13 +- .../components/tags-input/tags-input.tsx | 59 +- ui/src/app/shared/components/timestamp.tsx | 8 +- ui/src/app/shared/components/urls.ts | 3 +- .../components/yaml-editor/yaml-editor.tsx | 116 ++- ui/src/app/shared/context.ts | 14 +- ui/src/app/shared/models.ts | 51 +- .../shared/services/applications-service.ts | 260 +++-- ui/src/app/shared/services/auth-service.ts | 4 +- ui/src/app/shared/services/cert-service.ts | 18 +- .../app/shared/services/clusters-service.ts | 7 +- ui/src/app/shared/services/index.ts | 22 +- .../app/shared/services/projects-service.ts | 63 +- ui/src/app/shared/services/repo-service.ts | 82 +- .../app/shared/services/repocreds-service.ts | 39 +- ui/src/app/shared/services/requests.ts | 22 +- ui/src/app/shared/services/user-service.ts | 9 +- ui/src/app/shared/services/version-service.ts | 2 +- .../services/view-preferences-service.ts | 22 +- ui/src/app/shared/utils.ts | 2 +- .../components/user-info-container.tsx | 6 +- .../user-info-overview/user-info-overview.tsx | 39 +- ui/src/app/user-info/index.ts | 4 +- ui/tslint.json | 3 +- ui/yarn.lock | 44 +- 99 files changed, 5662 insertions(+), 4318 deletions(-) create mode 100644 ui/.prettierrc diff --git a/ui/.prettierrc b/ui/.prettierrc new file mode 100644 index 0000000000000..f1d63056591cc --- /dev/null +++ b/ui/.prettierrc @@ -0,0 +1,8 @@ +{ + "bracketSpacing": false, + "jsxSingleQuote": true, + "printWidth": 180, + "singleQuote": true, + "tabWidth": 4, + "jsxBracketSameLine": true +} diff --git a/ui/package.json b/ui/package.json index 9e06d837d8e24..0304b09890802 100644 --- a/ui/package.json +++ b/ui/package.json @@ -88,7 +88,10 @@ "@types/jest": "^24.0.13", "add": "^2.0.6", "jest": "^24.8.0", + "prettier": "^1.18.2", "ts-jest": "^24.0.2", + "tslint-config-prettier": "^1.18.0", + "tslint-plugin-prettier": "^2.0.1", "yarn": "^1.16.0" } } diff --git a/ui/src/app/app.tsx b/ui/src/app/app.tsx index e9f07be25c44d..f5496d4ec616f 100644 --- a/ui/src/app/app.tsx +++ b/ui/src/app/app.tsx @@ -1,14 +1,4 @@ -import { - DataLoader, - Layout, - NavigationManager, - Notifications, - NotificationsManager, - PageContext, - Popup, - PopupManager, - PopupProps, -} from 'argo-ui'; +import {DataLoader, Layout, NavigationManager, Notifications, NotificationsManager, PageContext, Popup, PopupManager, PopupProps} from 'argo-ui'; import {createBrowserHistory} from 'history'; import * as PropTypes from 'prop-types'; import * as React from 'react'; @@ -28,41 +18,46 @@ import userInfo from './user-info'; services.viewPreferences.init(); const bases = document.getElementsByTagName('base'); const base = bases.length > 0 ? bases[0].getAttribute('href') || '/' : '/'; -export const history = createBrowserHistory({ basename: base }); +export const history = createBrowserHistory({basename: base}); requests.setApiRoot(`${base}api/v1`); -const routes: {[path: string]: { component: React.ComponentType>, noLayout?: boolean } } = { - '/login': { component: login.component as any, noLayout: true }, - '/applications': { component: applications.component }, - '/settings': { component: settings.component }, - '/user-info': { component: userInfo.component }, - '/help': { component: help.component }, +const routes: {[path: string]: {component: React.ComponentType>; noLayout?: boolean}} = { + '/login': {component: login.component as any, noLayout: true}, + '/applications': {component: applications.component}, + '/settings': {component: settings.component}, + '/user-info': {component: userInfo.component}, + '/help': {component: help.component} }; -const navItems = [{ - title: 'Manage your applications, and diagnose health problems.', - path: '/applications', - iconClassName: 'argo-icon-application', -}, { - title: 'Manage your repositories, projects, settings', - path: '/settings', - iconClassName: 'argo-icon-settings', -}, { - title: 'User Info', - path: '/user-info', - iconClassName: 'fa fa-user-circle', -}, { - title: 'Read the documentation, and get help and assistance.', - path: '/help', - iconClassName: 'argo-icon-docs', -}]; +const navItems = [ + { + title: 'Manage your applications, and diagnose health problems.', + path: '/applications', + iconClassName: 'argo-icon-application' + }, + { + title: 'Manage your repositories, projects, settings', + path: '/settings', + iconClassName: 'argo-icon-settings' + }, + { + title: 'User Info', + path: '/user-info', + iconClassName: 'fa fa-user-circle' + }, + { + title: 'Read the documentation, and get help and assistance.', + path: '/help', + iconClassName: 'argo-icon-docs' + } +]; async function isExpiredSSO() { try { const {loggedIn, iss} = await services.users.get(); if (loggedIn && iss !== 'argocd') { const authSettings = await services.authService.settings(); - return (authSettings.dexConfig && authSettings.dexConfig.connectors || []).length > 0 || authSettings.oidcConfig; + return ((authSettings.dexConfig && authSettings.dexConfig.connectors) || []).length > 0 || authSettings.oidcConfig; } } catch { return false; @@ -70,12 +65,15 @@ async function isExpiredSSO() { return false; } -requests.onError.subscribe(async (err) => { +requests.onError.subscribe(async err => { if (err.status === 401) { if (!history.location.pathname.startsWith('/login')) { // Query for basehref and remove trailing /. // If basehref is the default `/` it will become an empty string. - const basehref = document.querySelector('head > base').getAttribute('href').replace(/\/$/, ''); + const basehref = document + .querySelector('head > base') + .getAttribute('href') + .replace(/\/$/, ''); if (await isExpiredSSO()) { window.location.href = `${basehref}/auth/login?return_url=${encodeURIComponent(location.href)}`; } else { @@ -85,14 +83,14 @@ requests.onError.subscribe(async (err) => { } }); -export class App extends React.Component<{}, { popupProps: PopupProps, error: Error }> { +export class App extends React.Component<{}, {popupProps: PopupProps; error: Error}> { public static childContextTypes = { history: PropTypes.object, - apis: PropTypes.object, + apis: PropTypes.object }; public static getDerivedStateFromError(error: Error) { - return { error }; + return {error}; } private popupManager: PopupManager; @@ -101,15 +99,15 @@ export class App extends React.Component<{}, { popupProps: PopupProps, error: Er constructor(props: {}) { super(props); - this.state = { popupProps: null, error: null }; + this.state = {popupProps: null, error: null}; this.popupManager = new PopupManager(); this.notificationsManager = new NotificationsManager(); this.navigationManager = new NavigationManager(history); } public async componentDidMount() { - this.popupManager.popupProps.subscribe((popupProps) => this.setState({ popupProps })); - const { trackingID, anonymizeUsers } = await services.authService.settings().then((item) => item.googleAnalytics || { trackingID: '', anonymizeUsers: true }); + this.popupManager.popupProps.subscribe(popupProps => this.setState({popupProps})); + const {trackingID, anonymizeUsers} = await services.authService.settings().then(item => item.googleAnalytics || {trackingID: '', anonymizeUsers: true}); const {loggedIn, username} = await services.users.get(); if (trackingID) { const ga = await import('react-ga'); @@ -134,7 +132,10 @@ export class App extends React.Component<{}, { popupProps: PopupProps, error: Er return (

Something went wrong!

-

Consider submitting an issue here.


+

+ Consider submitting an issue here. +

+

Stacktrace:

{stack}
@@ -144,47 +145,60 @@ export class App extends React.Component<{}, { popupProps: PopupProps, error: Er return ( - - + + - - {this.state.popupProps && } - - - - {Object.keys(routes).map((path) => { - const route = routes[path]; - return ( - route.noLayout ? ( -
- -
- ) : ( - services.version.version()}>{(msg) => msg.Version}}> - - - ) - )}/>; - })} - -
-
- services.authService.settings()}>{(s) => ( - s.help && s.help.chatUrl && || null - )} -
+ + {this.state.popupProps && } + + + + {Object.keys(routes).map(path => { + const route = routes[path]; + return ( + + route.noLayout ? ( +
+ +
+ ) : ( + services.version.version()}>{msg => msg.Version}}> + + + ) + } + /> + ); + })} + +
+
+ services.authService.settings()}> + {s => + (s.help && s.help.chatUrl && ( + + )) || + null + } + +
- +
); } public getChildContext() { - return { history, apis: { popup: this.popupManager, notifications: this.notificationsManager, navigation: this.navigationManager } }; + return {history, apis: {popup: this.popupManager, notifications: this.notificationsManager, navigation: this.navigationManager}}; } } diff --git a/ui/src/app/applications/components/application-conditions/application-conditions.tsx b/ui/src/app/applications/components/application-conditions/application-conditions.tsx index 04c0004d98519..c817ac03a6c99 100644 --- a/ui/src/app/applications/components/application-conditions/application-conditions.tsx +++ b/ui/src/app/applications/components/application-conditions/application-conditions.tsx @@ -1,30 +1,24 @@ import * as React from 'react'; import * as models from '../../../shared/models'; -import { getConditionCategory } from '../utils'; +import {getConditionCategory} from '../utils'; require('./application-conditions.scss'); -export const ApplicationConditions = ({conditions}: { conditions: models.ApplicationCondition[]}) => { +export const ApplicationConditions = ({conditions}: {conditions: models.ApplicationCondition[]}) => { return (

Application conditions

- {conditions.length === 0 && ( -

No conditions to report!

- ) || ( + {(conditions.length === 0 &&

No conditions to report!

) || (
- {conditions.map((condition, index) => ( -
-
-
- {condition.type} -
-
- {condition.message} + {conditions.map((condition, index) => ( +
+
+
{condition.type}
+
{condition.message}
-
- ))} + ))}
)}
diff --git a/ui/src/app/applications/components/application-create-panel/application-create-panel.tsx b/ui/src/app/applications/components/application-create-panel/application-create-panel.tsx index 425eba104dab4..ea97ea507bd47 100644 --- a/ui/src/app/applications/components/application-create-panel/application-create-panel.tsx +++ b/ui/src/app/applications/components/application-create-panel/application-create-panel.tsx @@ -5,60 +5,65 @@ import {FieldApi, Form, FormApi, FormField as ReactFormField, Text} from 'react- import {AutocompleteField, clusterTitle, YamlEditor} from '../../../shared/components'; import * as models from '../../../shared/models'; import {services} from '../../../shared/services'; -import { ApplicationParameters } from '../application-parameters/application-parameters'; +import {ApplicationParameters} from '../application-parameters/application-parameters'; const jsonMergePatch = require('json-merge-patch'); require('./application-create-panel.scss'); -const appTypes = new Array<{ field: string, type: models.AppSourceType }>( - { type: 'Helm', field: 'helm'}, - { type: 'Kustomize', field: 'kustomize'}, - { type: 'Ksonnet', field: 'ksonnet'}, - { type: 'Directory', field: 'directory'}, - { type: 'Plugin', field: 'plugin'}, +const appTypes = new Array<{field: string; type: models.AppSourceType}>( + {type: 'Helm', field: 'helm'}, + {type: 'Kustomize', field: 'kustomize'}, + {type: 'Ksonnet', field: 'ksonnet'}, + {type: 'Directory', field: 'directory'}, + {type: 'Plugin', field: 'plugin'} ); const DEFAULT_APP: Partial = { apiVersion: 'argoproj.io/v1alpha1', kind: 'Application', metadata: { - name: '', + name: '' }, spec: { destination: { namespace: '', - server: '', + server: '' }, source: { path: '', repoURL: '', - targetRevision: 'HEAD', + targetRevision: 'HEAD' }, - project: '', - }, + project: '' + } }; -const AutoSyncFormField = ReactFormField((props: {fieldApi: FieldApi, className: string }) => { +const AutoSyncFormField = ReactFormField((props: {fieldApi: FieldApi; className: string}) => { const manual = 'Manual'; const auto = 'Automatic'; - const { fieldApi: {getValue, setValue}} = props; + const { + fieldApi: {getValue, setValue} + } = props; const policy = getValue() as models.SyncPolicy; return ( - { + setValue(opt.value === auto ? {automated: {prune: false, selfHeal: false}} : null); + }} + /> {policy && policy.automated && (
- setValue({ automated: {...policy.automated, prune: val} })} - checked={policy.automated.prune} id='policyPrune' /> setValue({ automated: {...policy.automated, selfHeal: val} })} - checked={policy.automated.selfHeal} id='policySelfHeal' /> + setValue({automated: {...policy.automated, prune: val}})} checked={policy.automated.prune} id='policyPrune' />{' '} + {' '} + setValue({automated: {...policy.automated, selfHeal: val}})} checked={policy.automated.selfHeal} id='policySelfHeal' />{' '} +
)}
@@ -66,15 +71,15 @@ const AutoSyncFormField = ReactFormField((props: {fieldApi: FieldApi, className: }); function normalizeAppSource(app: models.Application, type: string): boolean { - const repoType = app.spec.source.hasOwnProperty('chart') && 'helm' || 'git'; + const repoType = (app.spec.source.hasOwnProperty('chart') && 'helm') || 'git'; if (repoType !== type) { if (type === 'git') { app.spec.source.path = app.spec.source.chart; - delete(app.spec.source.chart); + delete app.spec.source.chart; app.spec.source.targetRevision = 'HEAD'; } else { app.spec.source.chart = app.spec.source.path; - delete(app.spec.source.path); + delete app.spec.source.path; app.spec.source.targetRevision = ''; } return true; @@ -83,14 +88,13 @@ function normalizeAppSource(app: models.Application, type: string): boolean { } export const ApplicationCreatePanel = (props: { - app: models.Application, + app: models.Application; onAppChanged: (app: models.Application) => any; createApp: (app: models.Application) => any; getFormApi: (api: FormApi) => any; }) => { - const [yamlMode, setYamlMode] = React.useState(false); - const [explicitPathType, setExplicitPathType] = React.useState<{ path: string, type: models.AppSourceType }>(null); + const [explicitPathType, setExplicitPathType] = React.useState<{path: string; type: models.AppSourceType}>(null); function normalizeTypeFields(formApi: FormApi, type: models.AppSourceType) { const app = formApi.getFormState().values; @@ -104,231 +108,306 @@ export const ApplicationCreatePanel = (props: { return ( - Promise.all([ - services.projects.list().then((projects) => projects.map((proj) => proj.metadata.name).sort()), - services.clusters.list().then((clusters) => clusters - .map((cluster) => ({label: clusterTitle(cluster), value: cluster.server})) - .sort((first, second) => first.label.localeCompare(second.label)), - ), - services.repos.list(), - ]).then(([projects, clusters, reposInfo]) => ({projects, clusters, reposInfo}))}> - {({projects, clusters, reposInfo}) => { - const repos = reposInfo.map((info) => info.repo).sort(); - const app = deepMerge(DEFAULT_APP, props.app || {}); - const repoInfo = reposInfo.find((info) => info.repo === app.spec.source.repoURL); - if (repoInfo) { - normalizeAppSource(app, repoInfo.type || 'git'); - } - return ( -
- {yamlMode && ( - setYamlMode(false)} onSave={async (patch) => { - props.onAppChanged(jsonMergePatch.apply(app, JSON.parse(patch))); - setYamlMode(false); - return true; - }}/> - ) || ( -
({ - 'metadata.name': !a.metadata.name && 'Application name is required', - 'spec.project': !a.spec.project && 'Project name is required', - 'spec.source.repoURL': !a.spec.source.repoURL && 'Repository URL is required', - 'spec.source.targetRevision': !a.spec.source.targetRevision && a.spec.source.hasOwnProperty('chart') && 'Version is required', - 'spec.source.path': !a.spec.source.path && !a.spec.source.chart && 'Path is required', - 'spec.source.chart': !a.spec.source.path && !a.spec.source.chart && 'Chart is required', - 'spec.destination.server': !a.spec.destination.server && 'Cluster is required', - 'spec.destination.namespace': !a.spec.destination.namespace && 'Namespace is required', - })} defaultValues={app} formDidUpdate={(state) => props.onAppChanged(state.values as any)} onSubmit={props.createApp} getApi={props.getFormApi}> - {(api) => { - const generalPanel = () => ( -
-

GENERAL

- {!yamlMode && ( - - )} -
- -
-
- -
-
- -
-
- ); - - const repoType = api.getFormState().values.spec.source.hasOwnProperty('chart') && 'helm' || 'git'; - const sourcePanel = () => ( -
-

SOURCE

-
-
- -
-
-
- {repoInfo && ( - - {(repoInfo.type || 'git').toUpperCase()} - - ) || ( - (

{repoType.toUpperCase()}

)} items={['git', 'helm'].map( - (type: 'git' | 'helm') => ({ title: type.toUpperCase(), action: () => { - if (repoType !== type) { - const updatedApp = api.getFormState().values as models.Application; - if (normalizeAppSource(updatedApp, type)) { - api.setAllValues(updatedApp); - } - } - }}))} - /> + + Promise.all([ + services.projects.list().then(projects => projects.map(proj => proj.metadata.name).sort()), + services.clusters + .list() + .then(clusters => + clusters.map(cluster => ({label: clusterTitle(cluster), value: cluster.server})).sort((first, second) => first.label.localeCompare(second.label)) + ), + services.repos.list() + ]).then(([projects, clusters, reposInfo]) => ({projects, clusters, reposInfo})) + }> + {({projects, clusters, reposInfo}) => { + const repos = reposInfo.map(info => info.repo).sort(); + const app = deepMerge(DEFAULT_APP, props.app || {}); + const repoInfo = reposInfo.find(info => info.repo === app.spec.source.repoURL); + if (repoInfo) { + normalizeAppSource(app, repoInfo.type || 'git'); + } + return ( +
+ {(yamlMode && ( + setYamlMode(false)} + onSave={async patch => { + props.onAppChanged(jsonMergePatch.apply(app, JSON.parse(patch))); + setYamlMode(false); + return true; + }} + /> + )) || ( + ({ + 'metadata.name': !a.metadata.name && 'Application name is required', + 'spec.project': !a.spec.project && 'Project name is required', + 'spec.source.repoURL': !a.spec.source.repoURL && 'Repository URL is required', + 'spec.source.targetRevision': !a.spec.source.targetRevision && a.spec.source.hasOwnProperty('chart') && 'Version is required', + 'spec.source.path': !a.spec.source.path && !a.spec.source.chart && 'Path is required', + 'spec.source.chart': !a.spec.source.path && !a.spec.source.chart && 'Chart is required', + 'spec.destination.server': !a.spec.destination.server && 'Cluster is required', + 'spec.destination.namespace': !a.spec.destination.namespace && 'Namespace is required' + })} + defaultValues={app} + formDidUpdate={state => props.onAppChanged(state.values as any)} + onSubmit={props.createApp} + getApi={props.getFormApi}> + {api => { + const generalPanel = () => ( +
+

GENERAL

+ {!yamlMode && ( + )} +
+ +
+
+ +
+
+
-
- {repoType === 'git' && ( - -
- -
-
- src.repoURL && - services.repos.apps(src.repoURL, src.revision) - .then((apps) => Array.from(new Set(apps.map((item) => item.path))).sort()) - .catch(() => new Array()) || - new Array() + ); + + const repoType = (api.getFormState().values.spec.source.hasOwnProperty('chart') && 'helm') || 'git'; + const sourcePanel = () => ( +
+

SOURCE

+
+
+ +
+
+
+ {(repoInfo && ( + + {(repoInfo.type || 'git').toUpperCase()} + + )) || ( + ( +

+ {repoType.toUpperCase()} +

+ )} + items={['git', 'helm'].map((type: 'git' | 'helm') => ({ + title: type.toUpperCase(), + action: () => { + if (repoType !== type) { + const updatedApp = api.getFormState().values as models.Application; + if (normalizeAppSource(updatedApp, type)) { + api.setAllValues(updatedApp); + } + } + } + }))} + /> + )} +
+
+
+ {(repoType === 'git' && ( + +
+ +
+
+ + (src.repoURL && + services.repos + .apps(src.repoURL, src.revision) + .then(apps => Array.from(new Set(apps.map(item => item.path))).sort()) + .catch(() => new Array())) || + new Array() + }> + {(apps: string[]) => ( + + )} + +
+
+ )) || ( + + (src.repoURL && services.repos.charts(src.repoURL).catch(() => new Array())) || + new Array() }> - {(apps: string[]) => ( - + {(charts: models.HelmChart[]) => { + const selectedChart = charts.find(chart => chart.name === api.getFormState().values.spec.source.chart); + return ( +
+
+ chart.name), + filterSuggestions: true + }} + /> +
+
+ +
+
+ ); + }} +
)} -
- - ) || ( - src.repoURL && - services.repos.charts(src.repoURL).catch(() => new Array()) || - new Array() - }> - {(charts: models.HelmChart[]) => { - const selectedChart = charts.find((chart) => chart.name === api.getFormState().values.spec.source.chart); - return ( -
-
- chart.name), filterSuggestions: true, - }}/> -
-
- -
-
- ); - }} -
- )} -
- ); + ); - const destinationPanel = () => ( -
-

DESTINATION

-
- -
-
- -
-
- ); + const destinationPanel = () => ( +
+

DESTINATION

+
+ +
+
+ +
+
+ ); - const typePanel = () => ( - { - if (src.repoURL && src.targetRevision && (src.path || src.chart)) { - return services.repos.appDetails(src).catch(() => ({ - type: 'Directory', - details: {}, - })); - } else { - return { - type: 'Directory', - details: {}, - }; - } - }}> - {(details: models.RepoAppDetails) => { - const type = explicitPathType && explicitPathType.path === app.spec.source.path && explicitPathType.type || details.type; - if (details.type !== type) { - switch (type) { - case 'Helm': - details = {type, path: details.path, helm: { name: '', valueFiles: [], path: '', parameters: [] } }; - break; - case 'Kustomize': - details = {type, path: details.path, kustomize: { path: '' } }; - break; - case 'Ksonnet': - details = {type, path: details.path, ksonnet: { name: '', path: '', environments: {}, parameters: []} }; - break; - case 'Plugin': - details = {type, path: details.path, plugin: { name: '', env: []} }; - break; - // Directory - default: - details = {type, path: details.path, directory: {} }; - break; - } - } - return ( - - (

{type}

)} items={appTypes.map( - (item) => ({ title: item.type, action: () => { - setExplicitPathType({ type: item.type, path: app.spec.source.path }); - normalizeTypeFields(api, item.type); - }}))} - /> - { - api.setAllValues(updatedApp); - }} /> -
+ const typePanel = () => ( + { + if (src.repoURL && src.targetRevision && (src.path || src.chart)) { + return services.repos.appDetails(src).catch(() => ({ + type: 'Directory', + details: {} + })); + } else { + return { + type: 'Directory', + details: {} + }; + } + }}> + {(details: models.RepoAppDetails) => { + const type = (explicitPathType && explicitPathType.path === app.spec.source.path && explicitPathType.type) || details.type; + if (details.type !== type) { + switch (type) { + case 'Helm': + details = {type, path: details.path, helm: {name: '', valueFiles: [], path: '', parameters: []}}; + break; + case 'Kustomize': + details = {type, path: details.path, kustomize: {path: ''}}; + break; + case 'Ksonnet': + details = {type, path: details.path, ksonnet: {name: '', path: '', environments: {}, parameters: []}}; + break; + case 'Plugin': + details = {type, path: details.path, plugin: {name: '', env: []}}; + break; + // Directory + default: + details = {type, path: details.path, directory: {}}; + break; + } + } + return ( + + ( +

+ {type} +

+ )} + items={appTypes.map(item => ({ + title: item.type, + action: () => { + setExplicitPathType({type: item.type, path: app.spec.source.path}); + normalizeTypeFields(api, item.type); + } + }))} + /> + { + api.setAllValues(updatedApp); + }} + /> +
+ ); + }} +
); - }} -
- ); - return ( - - {generalPanel()} + return ( + + {generalPanel()} - {sourcePanel()} + {sourcePanel()} - {destinationPanel()} + {destinationPanel()} - {typePanel()} - - ); - }} - - )} -
- ); - }} + {typePanel()} + + ); + }} + + )} +
+ ); + }} ); diff --git a/ui/src/app/applications/components/application-deployment-history/application-deployment-history.tsx b/ui/src/app/applications/components/application-deployment-history/application-deployment-history.tsx index e63344b08ce71..22f47982de715 100644 --- a/ui/src/app/applications/components/application-deployment-history/application-deployment-history.tsx +++ b/ui/src/app/applications/components/application-deployment-history/application-deployment-history.tsx @@ -14,12 +14,12 @@ export const ApplicationDeploymentHistory = ({ app, rollbackApp, selectedRollbackDeploymentIndex, - selectDeployment, + selectDeployment }: { - app: models.Application, - selectedRollbackDeploymentIndex: number, - rollbackApp: (info: models.RevisionHistory) => any, - selectDeployment: (index: number) => any, + app: models.Application; + selectedRollbackDeploymentIndex: number; + rollbackApp: (info: models.RevisionHistory) => any; + selectDeployment: (index: number) => any; }) => { const deployments = (app.status.history || []).slice().reverse(); const recentDeployments = deployments.map((info, i) => { @@ -31,48 +31,56 @@ export const ApplicationDeploymentHistory = ({ return (
{recentDeployments.map((info, index) => ( -
selectDeployment(index)}> +
selectDeployment(index)}>
- + +
+
+
-
-
- Revision: -
+
Revision:
- +
- } items={[{ - title: info.nextDeployedAt && 'Rollback' || 'Redeploy', - action: () => rollbackApp(info), - }]}/> + ( + + )} + items={[ + { + title: (info.nextDeployedAt && 'Rollback') || 'Redeploy', + action: () => rollbackApp(info) + } + ]} + />
- + {selectedRollbackDeploymentIndex === index ? ( - services.repos.appDetails(src)}> - {(details: models.RepoAppDetails) => ( -
- services.repos.appDetails(src)}> + {(details: models.RepoAppDetails) => ( +
+ -
- ) - } - - ) - : null} + spec: {...app.spec, source: recentDeployments[index].source} + }} + details={details} + /> +
+ )} +
+ ) : null}
))} diff --git a/ui/src/app/applications/components/application-deployment-history/revision-metadata-rows.tsx b/ui/src/app/applications/components/application-deployment-history/revision-metadata-rows.tsx index 046c767b9a421..04d7dc0515af1 100644 --- a/ui/src/app/applications/components/application-deployment-history/revision-metadata-rows.tsx +++ b/ui/src/app/applications/components/application-deployment-history/revision-metadata-rows.tsx @@ -4,53 +4,47 @@ import {Timestamp} from '../../../shared/components/timestamp'; import {ApplicationSource, RevisionMetadata} from '../../../shared/models'; import {services} from '../../../shared/services'; -export const RevisionMetadataRows = (props: { - applicationName: string; - source: ApplicationSource; -}) => { +export const RevisionMetadataRows = (props: {applicationName: string; source: ApplicationSource}) => { if (props.source.chart) { return (
Helm Chart
-
- {props.source.chart} -
+
{props.source.chart}
Version
-
- v{props.source.targetRevision} -
+
v{props.source.targetRevision}
); } return ( - services.applications.revisionMetadata(input.applicationName, input.source.targetRevision)} - >{(m: RevisionMetadata) => ( -
-
-
Authored by
-
- {m.author || 'unknown'}
- {m.date && } -
-
- {m.tags && ( -
-
Tagged
-
{m.tags.join(', ')}
-
- )} - {m.message && ( + services.applications.revisionMetadata(input.applicationName, input.source.targetRevision)}> + {(m: RevisionMetadata) => ( +
-
-
{m.message}
+
Authored by
+
+ {m.author || 'unknown'} +
+ {m.date && } +
- )} -
- )} + {m.tags && ( +
+
Tagged
+
{m.tags.join(', ')}
+
+ )} + {m.message && ( +
+
+
{m.message}
+
+ )} +
+ )} + ); }; diff --git a/ui/src/app/applications/components/application-details/application-details.tsx b/ui/src/app/applications/components/application-details/application-details.tsx index f6120ef76f38a..627a2663cda05 100644 --- a/ui/src/app/applications/components/application-details/application-details.tsx +++ b/ui/src/app/applications/components/application-details/application-details.tsx @@ -5,16 +5,7 @@ import * as React from 'react'; import {Checkbox} from 'react-form'; import {RouteComponentProps} from 'react-router'; import {BehaviorSubject, Observable} from 'rxjs'; -import { - DataLoader, - EmptyState, - ErrorNotification, - EventsList, - ObservableQuery, - Page, - Paginate, - YamlEditor, -} from '../../../shared/components'; +import {DataLoader, EmptyState, ErrorNotification, EventsList, ObservableQuery, Page, Paginate, YamlEditor} from '../../../shared/components'; import {AppContext} from '../../../shared/context'; import * as appModels from '../../../shared/models'; import {AppDetailsPreferences, AppsDetailsViewType, services} from '../../../shared/services'; @@ -40,19 +31,18 @@ const jsonMergePatch = require('json-merge-patch'); require('./application-details.scss'); -type ActionMenuItem = MenuItem & { disabled?: boolean }; - -export class ApplicationDetails extends React.Component, {page: number}> { +type ActionMenuItem = MenuItem & {disabled?: boolean}; +export class ApplicationDetails extends React.Component, {page: number}> { public static contextTypes = { - apis: PropTypes.object, + apis: PropTypes.object }; private refreshRequested = new BehaviorSubject(null); - constructor(props: RouteComponentProps<{ name: string; }>) { + constructor(props: RouteComponentProps<{name: string}>) { super(props); - this.state = { page: 0 }; + this.state = {page: 0}; } private get showOperationState() { @@ -68,7 +58,7 @@ export class ApplicationDetails extends React.Component - {(q) => ( - {error}} - loadingRenderer={() => Loading...} - input={this.props.match.params.name} - load={(name) => Observable.combineLatest(this.loadAppInfo(name), services.viewPreferences.getPreferences(), q).map((items) => { - const pref = items[1].appDetails; - const params = items[2]; - if (params.get('resource') != null) { - pref.resourceFilter = params.get('resource').split(',').filter((item) => !!item); - } - if (params.get('view') != null) { - pref.view = params.get('view') as AppsDetailsViewType; - } - if (params.get('orphaned') != null) { - pref.orphanedResources = params.get('orphaned') === 'true'; - } - return {...items[0], pref}; - })}> - - {({application, tree, pref}: {application: appModels.Application, tree: appModels.ApplicationTree, pref: AppDetailsPreferences}) => { - tree.nodes = tree.nodes || []; - const kindsSet = new Set(tree.nodes.map((item) => item.kind)); - const treeFilter = this.getTreeFilter(pref.resourceFilter); - treeFilter.kind.forEach((kind) => { kindsSet.add(kind); }); - const kinds = Array.from(kindsSet); - const noKindsFilter = pref.resourceFilter.filter((item) => item.indexOf('kind:') !== 0); - const refreshing = application.metadata.annotations && application.metadata.annotations[appModels.AnnotationRefreshKey]; - - const filter: TopBarFilter = { - items: [ - { content: () => Sync }, - { value: 'sync:Synced', label: 'Synced' }, - // Unhealthy includes 'Unknown' and 'OutOfSync' - { value: 'sync:OutOfSync', label: 'OutOfSync' }, - { content: () => Health }, - { value: 'health:Healthy', label: 'Healthy' }, - { value: 'health:Progressing', label: 'Progressing' }, - { value: 'health:Degraded', label: 'Degraded' }, - { value: 'health:Missing', label: 'Missing' }, - { value: 'health:Unknown', label: 'Unknown' }, - { content: (setSelection) => ( - - ) }, - ...kinds.sort().map((kind) => ({ value: `kind:${kind}`, label: kind })), - ], - selectedValues: pref.resourceFilter, - selectionChanged: (items) => { - this.appContext.apis.navigation.goto('.', { resource: `${items.join(',')}`}); - services.viewPreferences.updatePreferences({ appDetails: { ...pref, resourceFilter: items } }); - }, - }; - - const appNodesByName = this.groupAppNodesByKey(application, tree); - const selectedItem = this.selectedNodeKey && appNodesByName.get(this.selectedNodeKey) || null; - const isAppSelected = selectedItem === application; - const selectedNode = !isAppSelected && selectedItem as appModels.ResourceNode; - const operationState = application.status.operationState; - const conditions = application.status.conditions || []; - const syncResourceKey = new URLSearchParams(this.props.history.location.search).get('deploy'); - const tab = new URLSearchParams(this.props.history.location.search).get('tab'); - const filteredRes = application.status.resources.filter((res) => { - const resNode: ResourceTreeNode = {...res, root: null, info: null, parentRefs: [], resourceVersion: '', uid: ''}; - resNode.root = resNode; - return this.filterTreeNode(resNode, treeFilter); - }); - return ( -
- -
- { - this.appContext.apis.navigation.goto('.', { view: 'tree' }); - services.viewPreferences.updatePreferences({ appDetails: {...pref, view: 'tree'} }); - }} /> - { - this.appContext.apis.navigation.goto('.', { view: 'network' }); - services.viewPreferences.updatePreferences({ appDetails: {...pref, view: 'network'} }); - }} /> - { - this.appContext.apis.navigation.goto('.', { view: 'list' }); - services.viewPreferences.updatePreferences({ appDetails: {...pref, view: 'list'} }); - }} /> + {q => ( + {error}} + loadingRenderer={() => Loading...} + input={this.props.match.params.name} + load={name => + Observable.combineLatest(this.loadAppInfo(name), services.viewPreferences.getPreferences(), q).map(items => { + const pref = items[1].appDetails; + const params = items[2]; + if (params.get('resource') != null) { + pref.resourceFilter = params + .get('resource') + .split(',') + .filter(item => !!item); + } + if (params.get('view') != null) { + pref.view = params.get('view') as AppsDetailsViewType; + } + if (params.get('orphaned') != null) { + pref.orphanedResources = params.get('orphaned') === 'true'; + } + return {...items[0], pref}; + }) + }> + {({application, tree, pref}: {application: appModels.Application; tree: appModels.ApplicationTree; pref: AppDetailsPreferences}) => { + tree.nodes = tree.nodes || []; + const kindsSet = new Set(tree.nodes.map(item => item.kind)); + const treeFilter = this.getTreeFilter(pref.resourceFilter); + treeFilter.kind.forEach(kind => { + kindsSet.add(kind); + }); + const kinds = Array.from(kindsSet); + const noKindsFilter = pref.resourceFilter.filter(item => item.indexOf('kind:') !== 0); + const refreshing = application.metadata.annotations && application.metadata.annotations[appModels.AnnotationRefreshKey]; + + const filter: TopBarFilter = { + items: [ + {content: () => Sync}, + {value: 'sync:Synced', label: 'Synced'}, + // Unhealthy includes 'Unknown' and 'OutOfSync' + {value: 'sync:OutOfSync', label: 'OutOfSync'}, + {content: () => Health}, + {value: 'health:Healthy', label: 'Healthy'}, + {value: 'health:Progressing', label: 'Progressing'}, + {value: 'health:Degraded', label: 'Degraded'}, + {value: 'health:Missing', label: 'Missing'}, + {value: 'health:Unknown', label: 'Unknown'}, + { + content: setSelection => ( + + ) + }, + ...kinds.sort().map(kind => ({value: `kind:${kind}`, label: kind})) + ], + selectedValues: pref.resourceFilter, + selectionChanged: items => { + this.appContext.apis.navigation.goto('.', {resource: `${items.join(',')}`}); + services.viewPreferences.updatePreferences({appDetails: {...pref, resourceFilter: items}}); + } + }; + + const appNodesByName = this.groupAppNodesByKey(application, tree); + const selectedItem = (this.selectedNodeKey && appNodesByName.get(this.selectedNodeKey)) || null; + const isAppSelected = selectedItem === application; + const selectedNode = !isAppSelected && (selectedItem as appModels.ResourceNode); + const operationState = application.status.operationState; + const conditions = application.status.conditions || []; + const syncResourceKey = new URLSearchParams(this.props.history.location.search).get('deploy'); + const tab = new URLSearchParams(this.props.history.location.search).get('tab'); + const filteredRes = application.status.resources.filter(res => { + const resNode: ResourceTreeNode = {...res, root: null, info: null, parentRefs: [], resourceVersion: '', uid: ''}; + resNode.root = resNode; + return this.filterTreeNode(resNode, treeFilter); + }); + return ( +
+ +
+ { + this.appContext.apis.navigation.goto('.', {view: 'tree'}); + services.viewPreferences.updatePreferences({appDetails: {...pref, view: 'tree'}}); + }} + /> + { + this.appContext.apis.navigation.goto('.', {view: 'network'}); + services.viewPreferences.updatePreferences({appDetails: {...pref, view: 'network'}}); + }} + /> + { + this.appContext.apis.navigation.goto('.', {view: 'list'}); + services.viewPreferences.updatePreferences({appDetails: {...pref, view: 'list'}}); + }} + /> +
+ + ) + }}> +
+ this.setOperationStatusVisible(true)} + showConditions={() => this.setConditionsStatusVisible(true)} + />
- - ), - }}> -
- this.setOperationStatusVisible(true)} - showConditions={() => this.setConditionsStatusVisible(true)} - /> -
-
- {refreshing &&

Refreshing

} - {(tree.orphanedNodes || []).length > 0 && ( -
- { - this.appContext.apis.navigation.goto('.', { orphaned: val }); - services.viewPreferences.updatePreferences({ appDetails: {...pref, orphanedResources: val} }); - }}/> -
- )} - {(pref.view === 'tree' || pref.view === 'network') && ( - this.filterTreeNode(node, treeFilter)} - selectedNodeFullName={this.selectedNodeKey} - onNodeClick={(fullName) => this.selectNode(fullName)} - nodeMenu={(node) => this.renderResourceMenu(node, application)} - tree={tree} - app={application} - showOrphanedResources={pref.orphanedResources} - useNetworkingHierarchy={pref.view === 'network'} - onClearFilter={() => { - this.appContext.apis.navigation.goto('.', { resource: '' } ); - services.viewPreferences.updatePreferences({ appDetails: { ...pref, resourceFilter: [] } }); - }} - /> - ) || ( -
- {filteredRes.length > 0 && ( - this.setState({page})} preferencesKey='application-details'> - {(data) => ( - this.selectNode(fullName)} - resources={data} - nodeMenu={(node) => this.renderResourceMenu({...node, root: node}, application)} +
+ {refreshing &&

Refreshing

} + {(tree.orphanedNodes || []).length > 0 && ( +
+ { + this.appContext.apis.navigation.goto('.', {orphaned: val}); + services.viewPreferences.updatePreferences({appDetails: {...pref, orphanedResources: val}}); + }} + />{' '} + +
+ )} + {((pref.view === 'tree' || pref.view === 'network') && ( + this.filterTreeNode(node, treeFilter)} + selectedNodeFullName={this.selectedNodeKey} + onNodeClick={fullName => this.selectNode(fullName)} + nodeMenu={node => this.renderResourceMenu(node, application)} + tree={tree} + app={application} + showOrphanedResources={pref.orphanedResources} + useNetworkingHierarchy={pref.view === 'network'} + onClearFilter={() => { + this.appContext.apis.navigation.goto('.', {resource: ''}); + services.viewPreferences.updatePreferences({appDetails: {...pref, resourceFilter: []}}); + }} + /> + )) || ( +
+ {(filteredRes.length > 0 && ( + this.setState({page})} + preferencesKey='application-details'> + {data => ( + this.selectNode(fullName)} + resources={data} + nodeMenu={node => this.renderResourceMenu({...node, root: node}, application)} + /> + )} + + )) || ( + +

No resources found

+
Try to change filter criteria
+
+ )} +
+ )} +
+ this.selectNode('')}> +
+ {selectedNode && ( + { + const managedResources = await services.applications.managedResources(application.metadata.name); + const controlled = managedResources.find(item => isSameNode(selectedNode, item)); + const summary = application.status.resources.find(item => isSameNode(selectedNode, item)); + const controlledState = (controlled && summary && {summary, state: controlled}) || null; + const liveState = await services.applications.getResource(application.metadata.name, selectedNode).catch(() => null); + const events = + (liveState && + (await services.applications.resourceEvents(application.metadata.name, { + name: liveState.metadata.name, + namespace: liveState.metadata.namespace, + uid: liveState.metadata.uid + }))) || + []; + + return {controlledState, liveState, events}; + }}> + {data => ( + + ) + } + ])} + selectedTabKey={tab} + onTabSelected={selected => this.appContext.apis.navigation.goto('.', {tab: selected})} + /> + )} + + )} + {isAppSelected && ( + this.updateApp(app)} /> + }, + { + title: 'PARAMETERS', + key: 'parameters', + content: ( + + services.repos + .appDetails(src) + .catch(() => ({type: 'Directory' as appModels.AppSourceType, path: application.spec.source.path})) + }> + {(details: appModels.RepoAppDetails) => ( + this.updateApp(app)} application={application} details={details} /> + )} + + ) + }, + { + title: 'MANIFEST', + key: 'manifest', + content: ( + { + const spec = JSON.parse(JSON.stringify(application.spec)); + return services.applications.updateSpec( + application.metadata.name, + jsonMergePatch.apply(spec, JSON.parse(patch)) + ); + }} + /> + ) + }, + { + icon: 'fa fa-file-medical', + title: 'DIFF', + key: 'diff', + content: ( + await services.applications.managedResources(application.metadata.name)}> + {managedResources => } + + ) + }, + { + title: 'EVENTS', + key: 'event', + content: + } + ]} + selectedTabKey={tab} + onTabSelected={selected => this.appContext.apis.navigation.goto('.', {tab: selected})} /> )} - - ) || ( - -

No resources found

-
Try to change filter criteria
-
- )} -
- )} -
- this.selectNode('')}> -
- {selectedNode && ( - { - const managedResources = await services.applications.managedResources(application.metadata.name); - const controlled = managedResources.find((item) => isSameNode(selectedNode, item)); - const summary = application.status.resources.find((item) => isSameNode(selectedNode, item)); - const controlledState = controlled && summary && { summary, state: controlled } || null; - const liveState = await services.applications.getResource(application.metadata.name, selectedNode).catch(() => null); - const events = liveState && await services.applications.resourceEvents(application.metadata.name, { - name: liveState.metadata.name, - namespace: liveState.metadata.namespace, - uid: liveState.metadata.uid, - }) || []; - - return { controlledState, liveState, events }; - - }}>{(data) => - - ), - }])} selectedTabKey={tab} onTabSelected={(selected) => this.appContext.apis.navigation.goto('.', {tab: selected})}/> - } - )} - {isAppSelected && ( - this.updateApp(app)}/>, - }, { - title: 'PARAMETERS', key: 'parameters', content: ( - services.repos.appDetails(src) - .catch(() => ({ type: 'Directory' as appModels.AppSourceType, path: application.spec.source.path }))}> - {(details: appModels.RepoAppDetails) => this.updateApp(app)} application={application} details={details} />} - - ), - }, { - title: 'MANIFEST', key: 'manifest', content: ( - { - const spec = JSON.parse(JSON.stringify(application.spec)); - return services.applications.updateSpec(application.metadata.name, jsonMergePatch.apply(spec, JSON.parse(patch))); - }}/> - ), - }, { - icon: 'fa fa-file-medical', title: 'DIFF', key: 'diff', content: ( - await services.applications.managedResources(application.metadata.name)}>{(managedResources) => - } - ), - }, { - title: 'EVENTS', key: 'event', content: , - }]} selectedTabKey={tab} onTabSelected={(selected) => this.appContext.apis.navigation.goto('.', {tab: selected})}/> - )} +
+
+ this.showDeploy(null)} selectedResource={syncResourceKey} /> + -1} onClose={() => this.setRollbackPanelVisible(-1)}> + {this.selectedRollbackDeploymentIndex > -1 && ( + this.rollbackApplication(info)} + selectDeployment={i => this.setRollbackPanelVisible(i)} + /> + )} + + this.setOperationStatusVisible(false)}> + {operationState && } + + this.setConditionsStatusVisible(false)}> + {conditions && } + +
- - this.showDeploy(null)} - selectedResource={syncResourceKey} - /> - -1} onClose={() => this.setRollbackPanelVisible(-1)}> - {this.selectedRollbackDeploymentIndex > -1 && this.rollbackApplication(info)} - selectDeployment={(i) => this.setRollbackPanelVisible(i)} - />} - - this.setOperationStatusVisible(false)}> - {operationState && } - - this.setConditionsStatusVisible(false)}> - {conditions && } - -
-
- ); - }} -
)} + ); + }} + + )} ); } private getApplicationActionMenu(app: appModels.Application) { const refreshing = app.metadata.annotations && app.metadata.annotations[appModels.AnnotationRefreshKey]; - const fullName = nodeKey({group: 'argoproj.io', kind: app.kind, name: app.metadata.name, namespace: app.metadata.namespace }); - return [{ - iconClassName: 'fa fa-info-circle', - title: App Details, - action: () => this.selectNode(fullName), - }, { - iconClassName: 'fa fa-file-medical', - title: App Diff, - action: () => this.selectNode(fullName, 0, 'diff'), - disabled: app.status.sync.status === SyncStatuses.Synced, - }, { - iconClassName: 'fa fa-sync', - title: Sync, - action: () => this.showDeploy('all'), - }, { - iconClassName: 'fa fa-info-circle', - title: Sync Status, - action: () => this.setOperationStatusVisible(true), - disabled: !app.status.operationState, - }, { - iconClassName: 'fa fa-history', - title: History and rollback, - action: () => this.setRollbackPanelVisible(0), - disabled: !app.status.operationState, - }, { - iconClassName: 'fa fa-times-circle', - title: Delete, - action: () => this.deleteApplication(), - }, { - iconClassName: classNames('fa fa-redo', { 'status-icon--spin': !!refreshing }), - title: ( - Refresh !refreshing && services.applications.get(app.metadata.name, 'hard'), - }]} anchor={() => } /> - ), - disabled: !!refreshing, - action: () => !refreshing && services.applications.get(app.metadata.name, 'normal'), - }]; + const fullName = nodeKey({group: 'argoproj.io', kind: app.kind, name: app.metadata.name, namespace: app.metadata.namespace}); + return [ + { + iconClassName: 'fa fa-info-circle', + title: App Details, + action: () => this.selectNode(fullName) + }, + { + iconClassName: 'fa fa-file-medical', + title: App Diff, + action: () => this.selectNode(fullName, 0, 'diff'), + disabled: app.status.sync.status === SyncStatuses.Synced + }, + { + iconClassName: 'fa fa-sync', + title: Sync, + action: () => this.showDeploy('all') + }, + { + iconClassName: 'fa fa-info-circle', + title: Sync Status, + action: () => this.setOperationStatusVisible(true), + disabled: !app.status.operationState + }, + { + iconClassName: 'fa fa-history', + title: History and rollback, + action: () => this.setRollbackPanelVisible(0), + disabled: !app.status.operationState + }, + { + iconClassName: 'fa fa-times-circle', + title: Delete, + action: () => this.deleteApplication() + }, + { + iconClassName: classNames('fa fa-redo', {'status-icon--spin': !!refreshing}), + title: ( + + Refresh{' '} + !refreshing && services.applications.get(app.metadata.name, 'hard') + } + ]} + anchor={() => } + /> + + ), + disabled: !!refreshing, + action: () => !refreshing && services.applications.get(app.metadata.name, 'normal') + } + ]; } - private filterTreeNode(node: ResourceTreeNode, filter: {kind: string[], health: string[], sync: string[]}): boolean { - const syncStatuses = filter.sync.map((item) => item === 'OutOfSync' ? ['OutOfSync', 'Unknown'] : [item] ).reduce( - (first, second) => first.concat(second), []); + private filterTreeNode(node: ResourceTreeNode, filter: {kind: string[]; health: string[]; sync: string[]}): boolean { + const syncStatuses = filter.sync.map(item => (item === 'OutOfSync' ? ['OutOfSync', 'Unknown'] : [item])).reduce((first, second) => first.concat(second), []); - return (filter.kind.length === 0 || filter.kind.indexOf(node.kind) > -1) && - (syncStatuses.length === 0 || node.root.hook || node.root.status && syncStatuses.indexOf(node.root.status) > -1) && - (filter.health.length === 0 || node.root.hook || node.root.health && filter.health.indexOf(node.root.health.status) > -1); + return ( + (filter.kind.length === 0 || filter.kind.indexOf(node.kind) > -1) && + (syncStatuses.length === 0 || node.root.hook || (node.root.status && syncStatuses.indexOf(node.root.status) > -1)) && + (filter.health.length === 0 || node.root.hook || (node.root.health && filter.health.indexOf(node.root.health.status) > -1)) + ); } - private loadAppInfo(name: string): Observable<{application: appModels.Application, tree: appModels.ApplicationTree}> { + private loadAppInfo(name: string): Observable<{application: appModels.Application; tree: appModels.ApplicationTree}> { return Observable.merge( - Observable.fromPromise(services.applications.get(name).then((app) => ({ app, watchEvent: false }))), - services.applications.watch({name}).map((watchEvent) => { - if (watchEvent.type === 'DELETED') { - this.onAppDeleted(); - } - return { app: watchEvent.application, watchEvent: true }; - }).repeat().retryWhen((errors) => errors.delay(500)), - this.refreshRequested.filter((e) => e !== null).flatMap(() => services.applications.get(name).then((app) => ({ app, watchEvent: true }))), - ).flatMap((appInfo) => { - const app = appInfo.app; - const fallbackTree: appModels.ApplicationTree = { - nodes: app.status.resources.map((res) => ({...res, parentRefs: [], info: [], resourceVersion: '', uid: ''})), - orphanedNodes: [], - }; - const treeSource = new Observable<{ application: appModels.Application, tree: appModels.ApplicationTree }>((observer) => { - services.applications.resourceTree(app.metadata.name) - .then((tree) => observer.next({ application: app, tree })) - .catch((e) => { - observer.next({ application: app, tree: fallbackTree }); - observer.error(e); - }); - }).repeat().retryWhen((errors) => errors.delay(1000)); - if (appInfo.watchEvent) { - return treeSource; - } else { - return Observable.merge(Observable.from([{ application: app, tree: fallbackTree }]), treeSource); - } - }); + Observable.fromPromise(services.applications.get(name).then(app => ({app, watchEvent: false}))), + services.applications + .watch({name}) + .map(watchEvent => { + if (watchEvent.type === 'DELETED') { + this.onAppDeleted(); + } + return {app: watchEvent.application, watchEvent: true}; + }) + .repeat() + .retryWhen(errors => errors.delay(500)), + this.refreshRequested.filter(e => e !== null).flatMap(() => services.applications.get(name).then(app => ({app, watchEvent: true}))) + ).flatMap(appInfo => { + const app = appInfo.app; + const fallbackTree: appModels.ApplicationTree = { + nodes: app.status.resources.map(res => ({...res, parentRefs: [], info: [], resourceVersion: '', uid: ''})), + orphanedNodes: [] + }; + const treeSource = new Observable<{application: appModels.Application; tree: appModels.ApplicationTree}>(observer => { + services.applications + .resourceTree(app.metadata.name) + .then(tree => observer.next({application: app, tree})) + .catch(e => { + observer.next({application: app, tree: fallbackTree}); + observer.error(e); + }); + }) + .repeat() + .retryWhen(errors => errors.delay(1000)); + if (appInfo.watchEvent) { + return treeSource; + } else { + return Observable.merge(Observable.from([{application: app, tree: fallbackTree}]), treeSource); + } + }); } private onAppDeleted() { - this.appContext.apis.notifications.show({ type: NotificationType.Success, content: `Application '${this.props.match.params.name}' was deleted` }); + this.appContext.apis.notifications.show({type: NotificationType.Success, content: `Application '${this.props.match.params.name}' was deleted`}); this.appContext.apis.navigation.goto('/applications'); } @@ -415,12 +515,12 @@ export class ApplicationDetails extends React.Component(); - tree.nodes.concat(tree.orphanedNodes || []).forEach((node) => nodeByKey.set(nodeKey(node), node)); + tree.nodes.concat(tree.orphanedNodes || []).forEach(node => nodeByKey.set(nodeKey(node), node)); nodeByKey.set(nodeKey({group: 'argoproj.io', kind: application.kind, name: application.metadata.name, namespace: application.metadata.namespace}), application); return nodeByKey; } - private getTreeFilter(filter: string[]): {kind: string[], health: string[], sync: string[]} { + private getTreeFilter(filter: string[]): {kind: string[]; health: string[]; sync: string[]} { const kind = new Array(); const health = new Array(); const sync = new Array(); @@ -442,24 +542,24 @@ export class ApplicationDetails extends React.Component, - type: NotificationType.Error, + content: , + type: NotificationType.Error }); } } @@ -488,75 +588,96 @@ export class ApplicationDetails extends React.Component this.showDeploy(nodeKey(resource)), - }] || []), { - title: 'Delete', - action: async () => { - this.appContext.apis.popup.prompt('Delete resource', - () => ( -
-

Are your sure you want to delete {resource.kind} '{resource.name}'?`

-
- -
-
- ), + const items: MenuItem[] = [ + ...((isRoot && [ { - submit: async (vals, _, close) => { - try { - await services.applications.deleteResource(this.props.match.params.name, resource, !!vals.force); - this.refreshRequested.next({}); - close(); - } catch (e) { - this.appContext.apis.notifications.show({ - content: , - type: NotificationType.Error, - }); - } - }, - }); - }, - }]; - const resourceActions = services.applications.getResourceActions(application.metadata.name, resource) - .then((actions) => items.concat(actions.map((action) => ({ - title: action.name, - disabled: !action.available, + title: 'Sync', + action: () => this.showDeploy(nodeKey(resource)) + } + ]) || + []), + { + title: 'Delete', action: async () => { - try { - const confirmed = await this.appContext.apis.popup.confirm( - `Execute '${action.name}' action?`, `Are you sure you want to execute '${action.name}' action?`); - if (confirmed) { - await services.applications.runResourceAction(application.metadata.name, resource, action.name); + this.appContext.apis.popup.prompt( + 'Delete resource', + () => ( +
+

+ Are your sure you want to delete {resource.kind} '{resource.name}'?` +

+
+ +
+
+ ), + { + submit: async (vals, _, close) => { + try { + await services.applications.deleteResource(this.props.match.params.name, resource, !!vals.force); + this.refreshRequested.next({}); + close(); + } catch (e) { + this.appContext.apis.notifications.show({ + content: , + type: NotificationType.Error + }); + } + } } - } catch (e) { - this.appContext.apis.notifications.show({ - content: , - type: NotificationType.Error, - }); - } - }, - })))).catch(() => items); - menuItems = Observable.merge( - Observable.from([items]), - Observable.fromPromise(resourceActions)); + ); + } + } + ]; + const resourceActions = services.applications + .getResourceActions(application.metadata.name, resource) + .then(actions => + items.concat( + actions.map(action => ({ + title: action.name, + disabled: !action.available, + action: async () => { + try { + const confirmed = await this.appContext.apis.popup.confirm( + `Execute '${action.name}' action?`, + `Are you sure you want to execute '${action.name}' action?` + ); + if (confirmed) { + await services.applications.runResourceAction(application.metadata.name, resource, action.name); + } + } catch (e) { + this.appContext.apis.notifications.show({ + content: , + type: NotificationType.Error + }); + } + } + })) + ) + ) + .catch(() => items); + menuItems = Observable.merge(Observable.from([items]), Observable.fromPromise(resourceActions)); } return ( menuItems}> - {(items) => ( -
    - {items.map((item, i) => ( -
  • { - e.stopPropagation(); - if (!item.disabled) { - item.action(); - document.body.click(); // hack, trigger body click to make sure that dropdown closed - } - }}>{item.iconClassName && } {item.title}
  • - ))} -
- )} + {items => ( +
    + {items.map((item, i) => ( +
  • { + e.stopPropagation(); + if (!item.disabled) { + item.action(); + document.body.click(); + } + }}> + {item.iconClassName && } {item.title} +
  • + ))} +
+ )}
); } @@ -567,54 +688,62 @@ export class ApplicationDetails extends React.Component event.type !== 'Normal').reduce((total, event) => total + event.count, 0); + const numErrors = events.filter(event => event.type !== 'Normal').reduce((total, event) => total + event.count, 0); tabs.push({ title: 'EVENTS', - badge: numErrors > 0 && numErrors || null, - key: 'events', content: ( + badge: (numErrors > 0 && numErrors) || null, + key: 'events', + content: (
- -
), + +
+ ) }); } if (node.kind === 'Pod' && state) { - const containerGroups = [{ - offset: 0, - title: 'INIT CONTAINERS', - containers: state.spec.initContainers || [], - }, { - offset: (state.spec.initContainers || []).length, - title: 'CONTAINERS', - containers: state.spec.containers || [], - }]; - tabs = tabs.concat([{ - key: 'logs', - title: 'LOGS', - content: ( -
-
-
- {containerGroups.map((group) => ( -
- {group.containers.length > 0 &&

{group.title}:

} - {group.containers.map((container: any, i: number) => ( -
this.selectNode( - this.selectedNodeKey, group.offset + i, 'logs')}> - {(group.offset + i) === this.selectedNodeInfo.container && } - {container.name} -
- ))} -
- ))} -
-
- + const containerGroups = [ + { + offset: 0, + title: 'INIT CONTAINERS', + containers: state.spec.initContainers || [] + }, + { + offset: (state.spec.initContainers || []).length, + title: 'CONTAINERS', + containers: state.spec.containers || [] + } + ]; + tabs = tabs.concat([ + { + key: 'logs', + title: 'LOGS', + content: ( +
+
+
+ {containerGroups.map(group => ( +
+ {group.containers.length > 0 &&

{group.title}:

} + {group.containers.map((container: any, i: number) => ( +
this.selectNode(this.selectedNodeKey, group.offset + i, 'logs')}> + {group.offset + i === this.selectedNodeInfo.container && } + {container.name} +
+ ))} +
+ ))} +
+
+ +
-
- ), - }]); + ) + } + ]); } return tabs; } diff --git a/ui/src/app/applications/components/application-details/application-resource-list.tsx b/ui/src/app/applications/components/application-details/application-resource-list.tsx index 67dc722adb4ab..b0d3c469b3eb6 100644 --- a/ui/src/app/applications/components/application-details/application-resource-list.tsx +++ b/ui/src/app/applications/components/application-details/application-resource-list.tsx @@ -1,56 +1,74 @@ -import { DropDown } from 'argo-ui'; +import {DropDown} from 'argo-ui'; import * as React from 'react'; import * as models from '../../../shared/models'; import {ResourceIcon} from '../resource-icon'; -import { ComparisonStatusIcon, HealthStatusIcon, nodeKey } from '../utils'; +import {ComparisonStatusIcon, HealthStatusIcon, nodeKey} from '../utils'; -export const ApplicationResourceList = ({ resources, onNodeClick, nodeMenu }: { - resources: models.ResourceStatus[], - onNodeClick?: (fullName: string) => any, - nodeMenu?: (node: models.ResourceNode) => React.ReactNode, +export const ApplicationResourceList = ({ + resources, + onNodeClick, + nodeMenu +}: { + resources: models.ResourceStatus[]; + onNodeClick?: (fullName: string) => any; + nodeMenu?: (node: models.ResourceNode) => React.ReactNode; }) => (
-
+
NAME
GROUP/KIND
NAMESPACE
STATUS
- {resources.sort((first, second) => nodeKey(first).localeCompare(nodeKey(second))).map((res) => ( -
onNodeClick(nodeKey(res))}> -
-
-
{res.name}
-
{[res.group, res.kind].filter((item) => !!item).join('/')}
-
{res.namespace}
-
- {res.health && {res.health.status}  } - {res.status && } - {res.hook && ()} -
- }> - {() => nodeMenu({ - name: res.name, - version: res.version, - kind: res.kind, - namespace: res.namespace, - group: res.group, - info: null, - uid: '', - resourceVersion: null, - parentRefs: [], - })} - + {resources + .sort((first, second) => nodeKey(first).localeCompare(nodeKey(second))) + .map(res => ( +
onNodeClick(nodeKey(res))}> +
+
+ +
+
{res.name}
+
{[res.group, res.kind].filter(item => !!item).join('/')}
+
{res.namespace}
+
+ {res.health && ( + + {res.health.status}   + + )} + {res.status && } + {res.hook && } +
+ ( + + )}> + {() => + nodeMenu({ + name: res.name, + version: res.version, + kind: res.kind, + namespace: res.namespace, + group: res.group, + info: null, + uid: '', + resourceVersion: null, + parentRefs: [] + }) + } + +
-
- ))} + ))}
); diff --git a/ui/src/app/applications/components/application-node-info/application-node-info.tsx b/ui/src/app/applications/components/application-node-info/application-node-info.tsx index d48111180fa09..aa6ed02060664 100644 --- a/ui/src/app/applications/components/application-node-info/application-node-info.tsx +++ b/ui/src/app/applications/components/application-node-info/application-node-info.tsx @@ -1,38 +1,45 @@ -import { DataLoader, Tab, Tabs } from 'argo-ui'; +import {DataLoader, Tab, Tabs} from 'argo-ui'; import * as React from 'react'; -import { YamlEditor } from '../../../shared/components'; +import {YamlEditor} from '../../../shared/components'; import * as models from '../../../shared/models'; -import { services } from '../../../shared/services'; -import { ApplicationResourcesDiff } from '../application-resources-diff/application-resources-diff'; -import { ComparisonStatusIcon, getPodStateReason, HealthStatusIcon } from '../utils'; +import {services} from '../../../shared/services'; +import {ApplicationResourcesDiff} from '../application-resources-diff/application-resources-diff'; +import {ComparisonStatusIcon, getPodStateReason, HealthStatusIcon} from '../utils'; require('./application-node-info.scss'); export const ApplicationNodeInfo = (props: { - application: models.Application, - node: models.ResourceNode, - live: models.State, - controlled: { summary: models.ResourceStatus, state: models.ResourceDiff }, + application: models.Application; + node: models.ResourceNode; + live: models.State; + controlled: {summary: models.ResourceStatus; state: models.ResourceDiff}; }) => { - const attributes: {title: string, value: any}[] = [ + const attributes: {title: string; value: any}[] = [ {title: 'KIND', value: props.node.kind}, {title: 'NAME', value: props.node.name}, - {title: 'NAMESPACE', value: props.node.namespace}, + {title: 'NAMESPACE', value: props.node.namespace} ]; if ((props.node.images || []).length) { - attributes.push({title: 'IMAGES', value: ( -
- {(props.node.images || []).sort().map((image) => ({image}))} -
- ) }); + attributes.push({ + title: 'IMAGES', + value: ( +
+ {(props.node.images || []).sort().map(image => ( + + {image} + + ))} +
+ ) + }); } if (props.live) { if (props.node.kind === 'Pod') { const {reason, message} = getPodStateReason(props.live); - attributes.push({title: 'STATE', value: reason }); + attributes.push({title: 'STATE', value: reason}); if (message) { - attributes.push({title: 'STATE DETAILS', value: message }); + attributes.push({title: 'STATE DETAILS', value: message}); } } else if (props.node.kind === 'Service') { attributes.push({title: 'TYPE', value: props.live.spec.type}); @@ -47,34 +54,49 @@ export const ApplicationNodeInfo = (props: { if (props.controlled) { if (!props.controlled.summary.hook) { - attributes.push({title: 'STATUS', value: ( - - )} as any); + attributes.push({ + title: 'STATUS', + value: ( + + + + ) + } as any); } if (props.controlled.summary.health !== undefined) { - attributes.push({title: 'HEALTH', value: ( - {props.controlled.summary.health.status} - )} as any); + attributes.push({ + title: 'HEALTH', + value: ( + + {props.controlled.summary.health.status} + + ) + } as any); if (props.controlled.summary.health.message) { attributes.push({title: 'HEALTH DETAILS', value: props.controlled.summary.health.message}); } } } - const tabs: Tab[] = [{ - key: 'manifest', - title: 'Manifest', - content: ( - - services.applications.patchResource(props.application.metadata.name, props.node, patch, patchType) - }/>), - }]; + const tabs: Tab[] = [ + { + key: 'manifest', + title: 'Manifest', + content: ( + services.applications.patchResource(props.application.metadata.name, props.node, patch, patchType)} + /> + ) + } + ]; if (props.controlled && !props.controlled.summary.hook) { tabs.push({ key: 'diff', icon: 'fa fa-file-medical', title: 'Diff', - content: , + content: }); } @@ -82,11 +104,9 @@ export const ApplicationNodeInfo = (props: {
- {attributes.map((attr) => ( + {attributes.map(attr => (
-
- {attr.title} -
+
{attr.title}
{attr.value}
))} @@ -95,9 +115,15 @@ export const ApplicationNodeInfo = (props: {
services.viewPreferences.getPreferences()}> - {(pref) => 1 && pref.appDetails.resourceView || 'manifest'} tabs={tabs} onTabSelected={(selected) => { - services.viewPreferences.updatePreferences({ appDetails: { ...pref.appDetails, resourceView: selected as any } }); - }} />} + {pref => ( + 1 && pref.appDetails.resourceView) || 'manifest'} + tabs={tabs} + onTabSelected={selected => { + services.viewPreferences.updatePreferences({appDetails: {...pref.appDetails, resourceView: selected as any}}); + }} + /> + )}
diff --git a/ui/src/app/applications/components/application-operation-state/application-operation-state.tsx b/ui/src/app/applications/components/application-operation-state/application-operation-state.tsx index df28ae820a48d..c68a216b36e43 100644 --- a/ui/src/app/applications/components/application-operation-state/application-operation-state.tsx +++ b/ui/src/app/applications/components/application-operation-state/application-operation-state.tsx @@ -1,70 +1,77 @@ -import { Duration, NotificationType, Ticker } from 'argo-ui'; +import {Duration, NotificationType, Ticker} from 'argo-ui'; import * as moment from 'moment'; import * as PropTypes from 'prop-types'; import * as React from 'react'; -import { ErrorNotification } from '../../../shared/components'; +import {ErrorNotification} from '../../../shared/components'; import {Revision} from '../../../shared/components/revision'; import {Timestamp} from '../../../shared/components/timestamp'; -import { AppContext } from '../../../shared/context'; +import {AppContext} from '../../../shared/context'; import * as models from '../../../shared/models'; import {services} from '../../../shared/services'; import * as utils from '../utils'; require('./application-operation-state.scss'); -interface Props { application: models.Application; operationState: models.OperationState; } +interface Props { + application: models.Application; + operationState: models.OperationState; +} export const ApplicationOperationState: React.StatelessComponent = ({application, operationState}, ctx: AppContext) => { - const operationAttributes = [ {title: 'OPERATION', value: utils.getOperationType(application)}, {title: 'PHASE', value: operationState.phase}, ...(operationState.message ? [{title: 'MESSAGE', value: operationState.message}] : []), - {title: 'STARTED AT', value: }, - {title: 'DURATION', value: ( - - {(time) => } - - )}, + {title: 'STARTED AT', value: }, + { + title: 'DURATION', + value: ( + + {time => } + + ) + } ]; if (operationState.finishedAt) { - operationAttributes.push({ title: 'FINISHED AT', value: }); + operationAttributes.push({title: 'FINISHED AT', value: }); } else if (operationState.phase !== 'Terminating') { operationAttributes.push({ title: '', value: ( - - ), + }}> + Terminate + + ) }); } if (operationState.syncResult) { - operationAttributes.push( - {title: 'REVISION', value: }, - ); + operationAttributes.push({title: 'REVISION', value: }); } - const resultAttributes: {title: string, value: string}[] = []; + const resultAttributes: {title: string; value: string}[] = []; const syncResult = operationState.syncResult; if (operationState.finishedAt) { if (syncResult) { - (syncResult.resources || []).forEach((res) => { + (syncResult.resources || []).forEach(res => { resultAttributes.push({ title: `${res.namespace}/${res.kind}:${res.name}`, - value: res.message, + value: res.message }); }); } @@ -74,11 +81,9 @@ export const ApplicationOperationState: React.StatelessComponent = ({appl
- {operationAttributes.map((attr) => ( + {operationAttributes.map(attr => (
-
- {attr.title} -
+
{attr.title}
{attr.value}
))} @@ -90,56 +95,35 @@ export const ApplicationOperationState: React.StatelessComponent = ({appl
-
- KIND -
-
- NAMESPACE -
-
- NAME -
-
- STATUS -
-
- HOOK -
-
- MESSAGE -
+
KIND
+
NAMESPACE
+
NAME
+
STATUS
+
HOOK
+
MESSAGE
- {syncResult.resources.map((resource, i) => ( -
-
-
-
- {resource.hookType && ()} -
- {resource.group ? resource.group + '/' + resource.version : resource.version}/{resource.kind} -
-
- {resource.namespace} -
-
- {resource.name} -
-
- {resource.hookType ? resource.hookPhase : resource.status} -
-
- {resource.hookType} -
-
-
- {resource.message} + {syncResult.resources.map((resource, i) => ( +
+
+
+
+ {resource.hookType && } +
+ {resource.group ? resource.group + '/' + resource.version : resource.version}/{resource.kind} +
+
{resource.namespace}
+
{resource.name}
+
+ {resource.hookType ? resource.hookPhase : resource.status} +
+
{resource.hookType}
+
+
{resource.message}
-
- ))} + ))}
)} @@ -148,5 +132,5 @@ export const ApplicationOperationState: React.StatelessComponent = ({appl }; ApplicationOperationState.contextTypes = { - apis: PropTypes.object, + apis: PropTypes.object }; diff --git a/ui/src/app/applications/components/application-parameters/application-parameters.tsx b/ui/src/app/applications/components/application-parameters/application-parameters.tsx index d6f3dcf5fe6d4..ae25a601a8b24 100644 --- a/ui/src/app/applications/components/application-parameters/application-parameters.tsx +++ b/ui/src/app/applications/components/application-parameters/application-parameters.tsx @@ -8,20 +8,22 @@ import {ApplicationSourceDirectory, AuthSettings} from '../../../shared/models'; import {services} from '../../../shared/services'; import {ImageTagFieldEditor} from './kustomize'; import * as kustomize from './kustomize-image'; -import { VarsInputField } from './vars-input-field'; +import {VarsInputField} from './vars-input-field'; -const TextWithMetadataField = ReactFormField((props: {metadata: { value: string }, fieldApi: FieldApi, className: string }) => { - const { fieldApi: {getValue, setValue}} = props; - const metadata = (getValue() || props.metadata); +const TextWithMetadataField = ReactFormField((props: {metadata: {value: string}; fieldApi: FieldApi; className: string}) => { + const { + fieldApi: {getValue, setValue} + } = props; + const metadata = getValue() || props.metadata; - return setValue({...metadata, value: el.target.value})}/>; + return setValue({...metadata, value: el.target.value})} />; }); function distinct(first: IterableIterator, second: IterableIterator) { return Array.from(new Set(Array.from(first).concat(Array.from(second)))); } -function overridesFirst(first: { overrideIndex: number}, second: { overrideIndex: number }) { +function overridesFirst(first: {overrideIndex: number}, second: {overrideIndex: number}) { if (first.overrideIndex < 0) { return 1; } else if (second.overrideIndex < 0) { @@ -39,61 +41,77 @@ function getParamsEditableItems( params: { key?: string; overrideIndex: number; - original: string, - metadata: { name: string; value: string; } + original: string; + metadata: {name: string; value: string}; }[], - component: React.ComponentType = TextWithMetadataField, + component: React.ComponentType = TextWithMetadataField ) { - return params.sort(overridesFirst).map((param, i) => ({ - key: param.key, - title: param.metadata.name, - view: ( - - {param.overrideIndex > -1 && } {param.metadata.value} - - ), - edit: (formApi: FormApi) => { - const labelStyle = {position: 'absolute', right: 0, top: 0, zIndex: 1} as any; - const overrideRemoved = removedOverrides[i]; - const fieldItemPath = `${fieldsPath}[${i}]`; - return ( - - {overrideRemoved && ( - {param.original} - ) || ( - - )} - {param.metadata.value !== param.original && !overrideRemoved && { - formApi.setValue(fieldItemPath, null); - removedOverrides[i] = true; - setRemovedOverrides(removedOverrides); - }} style={labelStyle}> - Remove override} - {overrideRemoved && { - formApi.setValue(fieldItemPath, getNestedField(app, fieldsPath)[i]); - removedOverrides[i] = false; - setRemovedOverrides(removedOverrides); - }} style={labelStyle}> - Keep override} - - ); - }, - })).sort((first, second) => { - const firstSortBy = first.key || first.title; - const secondSortBy = second.key || second.title; - return firstSortBy.localeCompare(secondSortBy); - }).map((item, i) => ({...item, before: i === 0 &&

{title}

|| null })); + return params + .sort(overridesFirst) + .map((param, i) => ({ + key: param.key, + title: param.metadata.name, + view: ( + + {param.overrideIndex > -1 && } {param.metadata.value} + + ), + edit: (formApi: FormApi) => { + const labelStyle = {position: 'absolute', right: 0, top: 0, zIndex: 1} as any; + const overrideRemoved = removedOverrides[i]; + const fieldItemPath = `${fieldsPath}[${i}]`; + return ( + + {(overrideRemoved && {param.original}) || ( + + )} + {param.metadata.value !== param.original && !overrideRemoved && ( + { + formApi.setValue(fieldItemPath, null); + removedOverrides[i] = true; + setRemovedOverrides(removedOverrides); + }} + style={labelStyle}> + Remove override + + )} + {overrideRemoved && ( + { + formApi.setValue(fieldItemPath, getNestedField(app, fieldsPath)[i]); + removedOverrides[i] = false; + setRemovedOverrides(removedOverrides); + }} + style={labelStyle}> + Keep override + + )} + + ); + } + })) + .sort((first, second) => { + const firstSortBy = first.key || first.title; + const secondSortBy = second.key || second.title; + return firstSortBy.localeCompare(secondSortBy); + }) + .map((item, i) => ({...item, before: (i === 0 &&

{title}

) || null})); } export const ApplicationParameters = (props: { - application: models.Application, - details: models.RepoAppDetails, - save?: (application: models.Application) => Promise, - noReadonlyMode?: boolean, + application: models.Application; + details: models.RepoAppDetails; + save?: (application: models.Application) => Promise; + noReadonlyMode?: boolean; }) => { - const app = props.application; const source = props.application.spec.source; const [removedOverrides, setRemovedOverrides] = React.useState(new Array()); @@ -109,79 +127,108 @@ export const ApplicationParameters = (props: { formApi={formApi} field='spec.source.ksonnet.environment' component={FormSelect} - componentProps={{ options: Object.keys(props.details.ksonnet.environments) }}/> - ), + componentProps={{options: Object.keys(props.details.ksonnet.environments)}} + /> + ) }); const paramsByComponentName = new Map(); - (props.details.ksonnet && props.details.ksonnet.parameters || []).forEach((param) => paramsByComponentName.set(`${param.component}-${param.name}` , param)); + ((props.details.ksonnet && props.details.ksonnet.parameters) || []).forEach(param => paramsByComponentName.set(`${param.component}-${param.name}`, param)); const overridesByComponentName = new Map(); - (source.ksonnet && source.ksonnet.parameters || []).forEach((override, i) => overridesByComponentName.set(`${override.component}-${override.name}`, i)); - attributes = attributes.concat(getParamsEditableItems(app, 'PARAMETERS', 'spec.source.ksonnet.parameters', removedOverrides, setRemovedOverrides, - distinct(paramsByComponentName.keys(), overridesByComponentName.keys()).map((componentName) => { - let param = paramsByComponentName.get(componentName); - const original = param && param.value || ''; - let overrideIndex = overridesByComponentName.get(componentName); - if (overrideIndex === undefined) { - overrideIndex = -1; - } - if (!param && overrideIndex > -1) { - param = {...source.ksonnet.parameters[overrideIndex]}; - } - const value = overrideIndex > -1 && source.ksonnet.parameters[overrideIndex].value || original; - return { key: componentName, overrideIndex, original, metadata: { name: param.name, component: param.component, value } }; - }))); + ((source.ksonnet && source.ksonnet.parameters) || []).forEach((override, i) => overridesByComponentName.set(`${override.component}-${override.name}`, i)); + attributes = attributes.concat( + getParamsEditableItems( + app, + 'PARAMETERS', + 'spec.source.ksonnet.parameters', + removedOverrides, + setRemovedOverrides, + distinct(paramsByComponentName.keys(), overridesByComponentName.keys()).map(componentName => { + let param = paramsByComponentName.get(componentName); + const original = (param && param.value) || ''; + let overrideIndex = overridesByComponentName.get(componentName); + if (overrideIndex === undefined) { + overrideIndex = -1; + } + if (!param && overrideIndex > -1) { + param = {...source.ksonnet.parameters[overrideIndex]}; + } + const value = (overrideIndex > -1 && source.ksonnet.parameters[overrideIndex].value) || original; + return {key: componentName, overrideIndex, original, metadata: {name: param.name, component: param.component, value}}; + }) + ) + ); } else if (props.details.type === 'Kustomize' && props.details.kustomize) { attributes.push({ title: 'NAME PREFIX', view: app.spec.source.kustomize && app.spec.source.kustomize.namePrefix, - edit: (formApi: FormApi) => , + edit: (formApi: FormApi) => }); attributes.push({ title: 'NAME SUFFIX', view: app.spec.source.kustomize && app.spec.source.kustomize.nameSuffix, - edit: (formApi: FormApi) => , + edit: (formApi: FormApi) => }); - const srcImages = (props.details && props.details.kustomize && props.details.kustomize.images || []).map((val) => kustomize.parse(val)); - const images = (source.kustomize && source.kustomize.images || []).map((val) => kustomize.parse(val)); + const srcImages = ((props.details && props.details.kustomize && props.details.kustomize.images) || []).map(val => kustomize.parse(val)); + const images = ((source.kustomize && source.kustomize.images) || []).map(val => kustomize.parse(val)); if (srcImages.length > 0) { const imagesByName = new Map(); - srcImages.forEach((img) => imagesByName.set(img.name, img)); + srcImages.forEach(img => imagesByName.set(img.name, img)); const overridesByName = new Map(); images.forEach((override, i) => overridesByName.set(override.name, i)); - attributes = attributes.concat(getParamsEditableItems(app, 'IMAGES', 'spec.source.kustomize.images', removedOverrides, setRemovedOverrides, - distinct(imagesByName.keys(), overridesByName.keys()).map((name) => { - const param = imagesByName.get(name); - const original = param && kustomize.format(param); - let overrideIndex = overridesByName.get(name); - if (overrideIndex === undefined) { - overrideIndex = -1; - } - const value = overrideIndex > -1 && kustomize.format(images[overrideIndex]) || original; - return { overrideIndex, original, metadata: { name, value } }; - }), ImageTagFieldEditor)); + attributes = attributes.concat( + getParamsEditableItems( + app, + 'IMAGES', + 'spec.source.kustomize.images', + removedOverrides, + setRemovedOverrides, + distinct(imagesByName.keys(), overridesByName.keys()).map(name => { + const param = imagesByName.get(name); + const original = param && kustomize.format(param); + let overrideIndex = overridesByName.get(name); + if (overrideIndex === undefined) { + overrideIndex = -1; + } + const value = (overrideIndex > -1 && kustomize.format(images[overrideIndex])) || original; + return {overrideIndex, original, metadata: {name, value}}; + }), + ImageTagFieldEditor + ) + ); } } else if (props.details.type === 'Helm' && props.details.helm) { attributes.push({ title: 'VALUES FILES', - view: app.spec.source.helm && (app.spec.source.helm.valueFiles || []).join(', ') || 'No values files selected', + view: (app.spec.source.helm && (app.spec.source.helm.valueFiles || []).join(', ')) || 'No values files selected', edit: (formApi: FormApi) => ( - - ), + + ) }); attributes.push({ title: 'VALUES', - view: app.spec.source.helm && (
{app.spec.source.helm.values}
), + view: app.spec.source.helm && ( + +
{app.spec.source.helm.values}
+
+ ), edit: (formApi: FormApi) => (
-
+
+                        
+                    
{props.details.helm.values && (
@@ -191,87 +238,101 @@ export const ApplicationParameters = (props: {
)}
- ), + ) }); const paramsByName = new Map(); - (props.details.helm.parameters || []).forEach((param) => paramsByName.set(param.name, param)); + (props.details.helm.parameters || []).forEach(param => paramsByName.set(param.name, param)); const overridesByName = new Map(); - (source.helm && source.helm.parameters || []).forEach((override, i) => overridesByName.set(override.name, i)); - attributes = attributes.concat(getParamsEditableItems( - app, - 'PARAMETERS', - 'spec.source.helm.parameters', removedOverrides, setRemovedOverrides, distinct(paramsByName.keys(), overridesByName.keys()).map((name) => { - const param = paramsByName.get(name); - const original = param && param.value || ''; - let overrideIndex = overridesByName.get(name); - if (overrideIndex === undefined) { - overrideIndex = -1; - } - const value = overrideIndex > -1 && source.helm.parameters[overrideIndex].value || original; - return { overrideIndex, original, metadata: { name, value } }; - }))); + ((source.helm && source.helm.parameters) || []).forEach((override, i) => overridesByName.set(override.name, i)); + attributes = attributes.concat( + getParamsEditableItems( + app, + 'PARAMETERS', + 'spec.source.helm.parameters', + removedOverrides, + setRemovedOverrides, + distinct(paramsByName.keys(), overridesByName.keys()).map(name => { + const param = paramsByName.get(name); + const original = (param && param.value) || ''; + let overrideIndex = overridesByName.get(name); + if (overrideIndex === undefined) { + overrideIndex = -1; + } + const value = (overrideIndex > -1 && source.helm.parameters[overrideIndex].value) || original; + return {overrideIndex, original, metadata: {name, value}}; + }) + ) + ); } else if (props.details.type === 'Plugin') { attributes.push({ title: 'NAME', view: app.spec.source.plugin && app.spec.source.plugin.name, edit: (formApi: FormApi) => ( - services.authService.settings()}>{(settings: AuthSettings) => ( - p.name)}}/> - )} - ), + services.authService.settings()}> + {(settings: AuthSettings) => ( + p.name)}} /> + )} + + ) }); attributes.push({ title: 'ENV', - view: app.spec.source.plugin && (app.spec.source.plugin.env || []).map((i) => `${i.name}='${i.value}'`).join(' '), - edit: (formApi: FormApi) => ( - - ), + view: app.spec.source.plugin && (app.spec.source.plugin.env || []).map(i => `${i.name}='${i.value}'`).join(' '), + edit: (formApi: FormApi) => }); } else if (props.details.type === 'Directory') { - const directory = app.spec.source.directory || {} as ApplicationSourceDirectory; + const directory = app.spec.source.directory || ({} as ApplicationSourceDirectory); attributes.push({ title: 'DIRECTORY RECURSE', view: (!!directory.recurse).toString(), - edit: (formApi: FormApi) => ( - - ), + edit: (formApi: FormApi) => }); attributes.push({ title: 'TOP-LEVEL ARGUMENTS', - view: (directory.jsonnet && directory.jsonnet.tlas || []).map((i, j) =>

{i.name}='{i.value}' {i.code && 'code'}

), - edit: (formApi: FormApi) => ( - - ), + view: ((directory.jsonnet && directory.jsonnet.tlas) || []).map((i, j) => ( +

+ {i.name}='{i.value}' {i.code && 'code'} +

+ )), + edit: (formApi: FormApi) => }); attributes.push({ title: 'EXTERNAL VARIABLES', - view: (directory.jsonnet && directory.jsonnet.extVars || []).map((i, j) =>

{i.name}='{i.value}' {i.code && 'code'}

), - edit: (formApi: FormApi) => ( - - ), + view: ((directory.jsonnet && directory.jsonnet.extVars) || []).map((i, j) => ( +

+ {i.name}='{i.value}' {i.code && 'code'} +

+ )), + edit: (formApi: FormApi) => }); } return ( { - function isDefined(item: any) { - return item !== null && item !== undefined; - } + save={ + props.save && + (async (input: models.Application) => { + function isDefined(item: any) { + return item !== null && item !== undefined; + } - if (input.spec.source.helm && input.spec.source.helm.parameters) { - input.spec.source.helm.parameters = input.spec.source.helm.parameters.filter(isDefined); - } - if (input.spec.source.ksonnet && input.spec.source.ksonnet.parameters) { - input.spec.source.ksonnet.parameters = input.spec.source.ksonnet.parameters.filter(isDefined); - } - if (input.spec.source.kustomize && input.spec.source.kustomize.images) { - input.spec.source.kustomize.images = input.spec.source.kustomize.images.filter(isDefined); - } - await props.save(input); - setRemovedOverrides(new Array()); - })} - values={app} title={props.details.type.toLocaleUpperCase()} items={attributes} noReadonlyMode={props.noReadonlyMode} /> + if (input.spec.source.helm && input.spec.source.helm.parameters) { + input.spec.source.helm.parameters = input.spec.source.helm.parameters.filter(isDefined); + } + if (input.spec.source.ksonnet && input.spec.source.ksonnet.parameters) { + input.spec.source.ksonnet.parameters = input.spec.source.ksonnet.parameters.filter(isDefined); + } + if (input.spec.source.kustomize && input.spec.source.kustomize.images) { + input.spec.source.kustomize.images = input.spec.source.kustomize.images.filter(isDefined); + } + await props.save(input); + setRemovedOverrides(new Array()); + }) + } + values={app} + title={props.details.type.toLocaleUpperCase()} + items={attributes} + noReadonlyMode={props.noReadonlyMode} + /> ); }; diff --git a/ui/src/app/applications/components/application-parameters/kustomize-image.ts b/ui/src/app/applications/components/application-parameters/kustomize-image.ts index fb103ae20416c..9a3fc5fb3b5ed 100644 --- a/ui/src/app/applications/components/application-parameters/kustomize-image.ts +++ b/ui/src/app/applications/components/application-parameters/kustomize-image.ts @@ -7,24 +7,24 @@ export interface Image { digest?: string; } -function parseOverwrite(arg: string, overwriteImage: boolean): { name: string; digest?: string, tag?: string } { +function parseOverwrite(arg: string, overwriteImage: boolean): {name: string; digest?: string; tag?: string} { // match @ const parts = arg.split('@'); if (parts.length > 1) { - return { name: parts[0], digest: parts[1]}; + return {name: parts[0], digest: parts[1]}; } // match : const groups = pattern.exec(arg); if (groups && groups.length === 3) { - return { name: groups[1], tag: groups[2]}; + return {name: groups[1], tag: groups[2]}; } // match if (arg.length > 0 && overwriteImage) { - return { name: arg }; + return {name: arg}; } - return { name: arg }; + return {name: arg}; } export function parse(arg: string): Image { @@ -37,7 +37,7 @@ export function parse(arg: string): Image { name: parts[0], newName: overwrite.name, newTag: overwrite.tag, - digest: overwrite.digest, + digest: overwrite.digest }; } diff --git a/ui/src/app/applications/components/application-parameters/kustomize.tsx b/ui/src/app/applications/components/application-parameters/kustomize.tsx index 2a4e11afba78e..79628e371f2a4 100644 --- a/ui/src/app/applications/components/application-parameters/kustomize.tsx +++ b/ui/src/app/applications/components/application-parameters/kustomize.tsx @@ -1,43 +1,62 @@ -import { Checkbox } from 'argo-ui'; +import {Checkbox} from 'argo-ui'; import * as React from 'react'; -import { FieldApi, FormField as ReactFormField } from 'react-form'; +import {FieldApi, FormField as ReactFormField} from 'react-form'; -import { format, parse } from './kustomize-image'; +import {format, parse} from './kustomize-image'; -export const ImageTagFieldEditor = ReactFormField((props: {metadata: { value: string }, fieldApi: FieldApi, className: string }) => { - const { fieldApi: {getValue, setValue}} = props; +export const ImageTagFieldEditor = ReactFormField((props: {metadata: {value: string}; fieldApi: FieldApi; className: string}) => { + const { + fieldApi: {getValue, setValue} + } = props; const origImage = parse(props.metadata.value); const val = getValue(); - const image = val ? parse(val) : { name: origImage.name }; + const image = val ? parse(val) : {name: origImage.name}; const mustBeDigest = (image.digest || '').indexOf(':') > -1; return (
- { - setValue(format({...image, newName: el.target.value})); - }}/> - { - const forceDigest = el.target.value.indexOf(':') > -1; - if (image.digest || forceDigest) { - setValue(format({...image, newTag: null, digest: el.target.value})); - } else { - setValue(format({...image, newTag: el.target.value, digest: null})); - } - }} placeholder={origImage.newTag || origImage.digest} value={image.newTag || image.digest || ''}/> -
- { - const nextImg = {...image}; - if (mustBeDigest) { - return; - } - if (nextImg.digest) { - nextImg.newTag = nextImg.digest; - nextImg.digest = null; + { + setValue(format({...image, newName: el.target.value})); + }} + /> + { + const forceDigest = el.target.value.indexOf(':') > -1; + if (image.digest || forceDigest) { + setValue(format({...image, newTag: null, digest: el.target.value})); } else { - nextImg.digest = nextImg.newTag; - nextImg.newTag = null; + setValue(format({...image, newTag: el.target.value, digest: null})); } - setValue(format(nextImg)); - }}/> + }} + placeholder={origImage.newTag || origImage.digest} + value={image.newTag || image.digest || ''} + /> +
+ { + const nextImg = {...image}; + if (mustBeDigest) { + return; + } + if (nextImg.digest) { + nextImg.newTag = nextImg.digest; + nextImg.digest = null; + } else { + nextImg.digest = nextImg.newTag; + nextImg.newTag = null; + } + setValue(format(nextImg)); + }} + />{' '} +
); diff --git a/ui/src/app/applications/components/application-parameters/vars-input-field.tsx b/ui/src/app/applications/components/application-parameters/vars-input-field.tsx index e4d4942010d15..ffe91d5dc98fd 100644 --- a/ui/src/app/applications/components/application-parameters/vars-input-field.tsx +++ b/ui/src/app/applications/components/application-parameters/vars-input-field.tsx @@ -1,8 +1,8 @@ -import { Checkbox } from 'argo-ui'; +import {Checkbox} from 'argo-ui'; import * as React from 'react'; import * as ReactForm from 'react-form'; -import { ArrayInput, hasNameAndValue, NameValueEditor } from '../../../shared/components'; +import {ArrayInput, hasNameAndValue, NameValueEditor} from '../../../shared/components'; export interface Var { name: string; @@ -14,13 +14,15 @@ const VarInputEditor = (item: Var, onChange: (item: Var) => any) => ( {NameValueEditor(item, onChange)}   - onChange({...item, code: val})} /> + onChange({...item, code: val})} />   ); -export const VarsInputField = ReactForm.FormField((props: { fieldApi: ReactForm.FieldApi }) => { - const {fieldApi: {getValue, setValue}} = props; +export const VarsInputField = ReactForm.FormField((props: {fieldApi: ReactForm.FieldApi}) => { + const { + fieldApi: {getValue, setValue} + } = props; const val = getValue() || []; - return ; + return ; }); diff --git a/ui/src/app/applications/components/application-resource-events/application-resource-events.tsx b/ui/src/app/applications/components/application-resource-events/application-resource-events.tsx index f02b147711af0..e5cf025d11424 100644 --- a/ui/src/app/applications/components/application-resource-events/application-resource-events.tsx +++ b/ui/src/app/applications/components/application-resource-events/application-resource-events.tsx @@ -1,16 +1,15 @@ -import { MockupList } from 'argo-ui'; +import {MockupList} from 'argo-ui'; import * as React from 'react'; -import { DataLoader, EventsList } from '../../../shared/components'; -import { services } from '../../../shared/services'; +import {DataLoader, EventsList} from '../../../shared/components'; +import {services} from '../../../shared/services'; -export const ApplicationResourceEvents = (props: { applicationName: string, resource?: {namespace: string, name: string, uid: string} }) => ( +export const ApplicationResourceEvents = (props: {applicationName: string; resource?: {namespace: string; name: string; uid: string}}) => (
- props.resource ? - services.applications.resourceEvents(props.applicationName, props.resource) : - services.applications.events(props.applicationName)} - loadingRenderer={() => }> - {(events) => } + (props.resource ? services.applications.resourceEvents(props.applicationName, props.resource) : services.applications.events(props.applicationName))} + loadingRenderer={() => }> + {events => }
); diff --git a/ui/src/app/applications/components/application-resource-tree/application-resource-tree.tsx b/ui/src/app/applications/components/application-resource-tree/application-resource-tree.tsx index 71e491b0fb972..9f5b8633cb096 100644 --- a/ui/src/app/applications/components/application-resource-tree/application-resource-tree.tsx +++ b/ui/src/app/applications/components/application-resource-tree/application-resource-tree.tsx @@ -11,7 +11,7 @@ import {ResourceIcon} from '../resource-icon'; import {ComparisonStatusIcon, getAppOverridesCount, HealthStatusIcon, isAppNode, NodeId, nodeKey} from '../utils'; import {NodeUpdateAnimation} from './node-update-animation'; -function treeNodeKey(node: NodeId & { uid?: string }) { +function treeNodeKey(node: NodeId & {uid?: string}) { return node.uid || nodeKey(node); } @@ -40,7 +40,12 @@ export interface ApplicationResourceTreeProps { showOrphanedResources: boolean; } -interface Line { x1: number; y1: number; x2: number; y2: number; } +interface Line { + x1: number; + y1: number; + x2: number; + y2: number; +} const NODE_WIDTH = 282; const NODE_HEIGHT = 52; @@ -51,7 +56,7 @@ const NODE_TYPES = { filteredIndicator: 'filtered_indicator', externalTraffic: 'external_traffic', externalLoadBalancer: 'external_load_balancer', - internalTraffic: 'internal_traffic', + internalTraffic: 'internal_traffic' }; const BASE_COLORS = [ @@ -60,16 +65,24 @@ const BASE_COLORS = [ '#F4C030', // orange '#FF6262', // red '#4B0082', // purple - '#964B00', // brown + '#964B00' // brown ]; // generate lots of colors with different darkness -const TRAFFIC_COLORS = [0, 0.25, 0.4, 0.6].map((darken) => BASE_COLORS.map((item) => color(item).darken(darken).hex())).reduce((first, second) => first.concat(second), []); +const TRAFFIC_COLORS = [0, 0.25, 0.4, 0.6] + .map(darken => + BASE_COLORS.map(item => + color(item) + .darken(darken) + .hex() + ) + ) + .reduce((first, second) => first.concat(second), []); -function getGraphSize(nodes: dagre.Node[]): { width: number, height: number} { +function getGraphSize(nodes: dagre.Node[]): {width: number; height: number} { let width = 0; let height = 0; - nodes.forEach((node) => { + nodes.forEach(node => { width = Math.max(node.x + node.width, width); height = Math.max(node.y + node.height, height); }); @@ -79,7 +92,7 @@ function getGraphSize(nodes: dagre.Node[]): { width: number, height: number} { function filterGraph(app: models.Application, filteredIndicatorParent: string, graph: dagre.graphlib.Graph, predicate: (node: ResourceTreeNode) => boolean) { const appKey = appNodeKey(app); let filtered = 0; - graph.nodes().forEach((nodeId) => { + graph.nodes().forEach(nodeId => { const node: ResourceTreeNode = graph.node(nodeId) as any; const parentIds = graph.predecessors(nodeId); if (node.root != null && !predicate(node) && appKey !== nodeId) { @@ -94,20 +107,20 @@ function filterGraph(app: models.Application, filteredIndicatorParent: string, g } }); if (filtered) { - graph.setNode(FILTERED_INDICATOR_NODE, { height: NODE_HEIGHT, width: NODE_WIDTH, count: filtered, type: NODE_TYPES.filteredIndicator }); + graph.setNode(FILTERED_INDICATOR_NODE, {height: NODE_HEIGHT, width: NODE_WIDTH, count: filtered, type: NODE_TYPES.filteredIndicator}); graph.setEdge(filteredIndicatorParent, FILTERED_INDICATOR_NODE); } } function compareNodes(first: ResourceTreeNode, second: ResourceTreeNode) { - return `${first.orphaned && '1' || '0'}/${nodeKey(first)}`.localeCompare(`${second.orphaned && '1' || '0'}/${nodeKey(second)}`); + return `${(first.orphaned && '1') || '0'}/${nodeKey(first)}`.localeCompare(`${(second.orphaned && '1') || '0'}/${nodeKey(second)}`); } function appNodeKey(app: models.Application) { - return nodeKey({group: 'argoproj.io', kind: app.kind, name: app.metadata.name, namespace: app.metadata.namespace }); + return nodeKey({group: 'argoproj.io', kind: app.kind, name: app.metadata.name, namespace: app.metadata.namespace}); } -function renderFilteredNode(node: { count: number } & dagre.Node, onClearFilter: () => any) { +function renderFilteredNode(node: {count: number} & dagre.Node, onClearFilter: () => any) { const indicators = new Array(); let count = Math.min(node.count - 1, 3); while (count > 0) { @@ -117,15 +130,20 @@ function renderFilteredNode(node: { count: number } & dagre.Node, onClearFilter: - {indicators.map((i) => ( -
+ {indicators.map(i => ( +
))} ); @@ -135,17 +153,22 @@ function renderTrafficNode(node: dagre.Node) { return (
- +
); } -function renderLoadBalancerNode(node: dagre.Node & { label: string, color: string }) { +function renderLoadBalancerNode(node: dagre.Node & {label: string; color: string}) { return ( -
+
@@ -167,40 +190,55 @@ function renderResourceNode(props: ApplicationResourceTreeProps, id: string, nod const appNode = isAppNode(node); const rootNode = !node.root; return ( -
props.onNodeClick && props.onNodeClick(fullName)} className={classNames('application-resource-tree__node', { - 'active': fullName === props.selectedNodeFullName, - 'application-resource-tree__node--orphaned': node.orphaned, - })} style={{left: node.x, top: node.y, width: node.width, height: node.height}}> +
props.onNodeClick && props.onNodeClick(fullName)} + className={classNames('application-resource-tree__node', { + active: fullName === props.selectedNodeFullName, + 'application-resource-tree__node--orphaned': node.orphaned + })} + style={{left: node.x, top: node.y, width: node.width, height: node.height}}> {!appNode && } -
- +
+
{node.name} -
- {node.hook && ()} - {healthState != null && } +
+ {node.hook && } + {healthState != null && } {comparisonStatus != null && } - {(appNode && !rootNode) && ( - + {appNode && !rootNode && ( + + + )} - +
- {(node.info || []).map((tag, i) => {tag.value})} + {(node.info || []).map((tag, i) => ( + + {tag.value} + + ))} {node.kind}
{props.nodeMenu && (
- }> - {() => props.nodeMenu(node)} + ( + + )}> + {() => props.nodeMenu(node)}
)} @@ -211,20 +249,22 @@ function renderResourceNode(props: ApplicationResourceTreeProps, id: string, nod function findNetworkTargets(nodes: ResourceTreeNode[], networkingInfo: models.ResourceNetworkingInfo): ResourceTreeNode[] { let result = new Array(); const refs = new Set((networkingInfo.targetRefs || []).map(nodeKey)); - result = result.concat(nodes.filter((target) => refs.has(nodeKey(target)))); + result = result.concat(nodes.filter(target => refs.has(nodeKey(target)))); if (networkingInfo.targetLabels) { - result = result.concat(nodes.filter((target) => { - if (target.networkingInfo && target.networkingInfo.labels) { - return Object.keys(networkingInfo.targetLabels).every((key) => networkingInfo.targetLabels[key] === target.networkingInfo.labels[key]); - } - return false; - })); + result = result.concat( + nodes.filter(target => { + if (target.networkingInfo && target.networkingInfo.labels) { + return Object.keys(networkingInfo.targetLabels).every(key => networkingInfo.targetLabels[key] === target.networkingInfo.labels[key]); + } + return false; + }) + ); } return result; } export const ApplicationResourceTree = (props: ApplicationResourceTreeProps) => { const graph = new dagre.graphlib.Graph(); - graph.setGraph({ rankdir: 'LR', marginx: -100 }); + graph.setGraph({rankdir: 'LR', marginx: -100}); graph.setDefaultEdgeLabel(() => ({})); const overridesCount = getAppOverridesCount(props.app); const appNode = { @@ -237,91 +277,108 @@ export const ApplicationResourceTree = (props: ApplicationResourceTreeProps) => children: Array(), status: props.app.status.sync.status, health: props.app.status.health, - info: overridesCount > 0 ? [{ - name: 'Parameter overrides', - value: `${overridesCount} parameter override(s)`, - }] : [], + info: + overridesCount > 0 + ? [ + { + name: 'Parameter overrides', + value: `${overridesCount} parameter override(s)` + } + ] + : [] }; const statusByKey = new Map(); - props.app.status.resources.forEach((res) => statusByKey.set(nodeKey(res), res)); + props.app.status.resources.forEach(res => statusByKey.set(nodeKey(res), res)); const nodeByKey = new Map(); - props.tree.nodes.map((node) => ({...node, orphaned: false})) - .concat((props.showOrphanedResources && props.tree.orphanedNodes || []) - .map((node) => ({...node, orphaned: true}))).forEach((node) => { - const status = statusByKey.get(nodeKey(node)); - const resourceNode: ResourceTreeNode = {...node}; - if (status) { - resourceNode.health = status.health; - resourceNode.status = status.status; - resourceNode.hook = status.hook; - resourceNode.requiresPruning = status.requiresPruning; - } - nodeByKey.set(treeNodeKey(node), resourceNode); - }); + props.tree.nodes + .map(node => ({...node, orphaned: false})) + .concat(((props.showOrphanedResources && props.tree.orphanedNodes) || []).map(node => ({...node, orphaned: true}))) + .forEach(node => { + const status = statusByKey.get(nodeKey(node)); + const resourceNode: ResourceTreeNode = {...node}; + if (status) { + resourceNode.health = status.health; + resourceNode.status = status.status; + resourceNode.hook = status.hook; + resourceNode.requiresPruning = status.requiresPruning; + } + nodeByKey.set(treeNodeKey(node), resourceNode); + }); const nodes = Array.from(nodeByKey.values()); let roots: ResourceTreeNode[] = null; const childrenByParentKey = new Map(); if (props.useNetworkingHierarchy) { const hasParents = new Set(); - const networkNodes = nodes.filter((node) => node.networkingInfo); - networkNodes.forEach((parent) => { - findNetworkTargets(networkNodes, parent.networkingInfo).forEach((child) => { + const networkNodes = nodes.filter(node => node.networkingInfo); + networkNodes.forEach(parent => { + findNetworkTargets(networkNodes, parent.networkingInfo).forEach(child => { const children = childrenByParentKey.get(treeNodeKey(parent)) || []; hasParents.add(treeNodeKey(child)); children.push(child); childrenByParentKey.set(treeNodeKey(parent), children); }); }); - roots = networkNodes.filter((node) => !hasParents.has(treeNodeKey(node))); + roots = networkNodes.filter(node => !hasParents.has(treeNodeKey(node))); } else { - nodes.forEach((child) => { - (child.parentRefs || []).forEach((parent) => { + nodes.forEach(child => { + (child.parentRefs || []).forEach(parent => { const children = childrenByParentKey.get(treeNodeKey(parent)) || []; children.push(child); childrenByParentKey.set(treeNodeKey(parent), children); }); }); - roots = nodes.filter((node) => (node.parentRefs || []).length === 0).sort(compareNodes); + roots = nodes.filter(node => (node.parentRefs || []).length === 0).sort(compareNodes); } function processNode(node: ResourceTreeNode, root: ResourceTreeNode, colors?: string[]) { graph.setNode(treeNodeKey(node), {...node, width: NODE_WIDTH, height: NODE_HEIGHT, root}); - (childrenByParentKey.get(treeNodeKey(node)) || []).sort(compareNodes).forEach((child) => { + (childrenByParentKey.get(treeNodeKey(node)) || []).sort(compareNodes).forEach(child => { graph.setEdge(treeNodeKey(node), treeNodeKey(child), {colors}); processNode(child, root, colors); }); } if (props.useNetworkingHierarchy) { - const externalRoots = roots.filter((root) => (root.networkingInfo.ingress || []).length > 0).sort(compareNodes); - const internalRoots = roots.filter((root) => (root.networkingInfo.ingress || []).length === 0).sort(compareNodes); + const externalRoots = roots.filter(root => (root.networkingInfo.ingress || []).length > 0).sort(compareNodes); + const internalRoots = roots.filter(root => (root.networkingInfo.ingress || []).length === 0).sort(compareNodes); const colorsBySource = new Map(); // sources are root internal services and external ingress/service IPs - const sources = Array.from(new Set(internalRoots.map((root) => treeNodeKey(root)).concat( - externalRoots.map((root) => root.networkingInfo.ingress.map((ingress) => ingress.hostname || ingress.ip)).reduce((first, second) => first.concat(second), []), - ))); + const sources = Array.from( + new Set( + internalRoots + .map(root => treeNodeKey(root)) + .concat( + externalRoots.map(root => root.networkingInfo.ingress.map(ingress => ingress.hostname || ingress.ip)).reduce((first, second) => first.concat(second), []) + ) + ) + ); // assign unique color to each traffic source sources.forEach((key, i) => colorsBySource.set(key, TRAFFIC_COLORS[i % TRAFFIC_COLORS.length])); if (externalRoots.length > 0) { - graph.setNode(EXTERNAL_TRAFFIC_NODE, { height: NODE_HEIGHT, width: 30, type: NODE_TYPES.externalTraffic }); - externalRoots.sort(compareNodes).forEach((root) => { - const loadBalancers = root.networkingInfo.ingress.map((ingress) => ingress.hostname || ingress.ip); - processNode(root, root, loadBalancers.map((lb) => colorsBySource.get(lb))); - loadBalancers.forEach((key) => { + graph.setNode(EXTERNAL_TRAFFIC_NODE, {height: NODE_HEIGHT, width: 30, type: NODE_TYPES.externalTraffic}); + externalRoots.sort(compareNodes).forEach(root => { + const loadBalancers = root.networkingInfo.ingress.map(ingress => ingress.hostname || ingress.ip); + processNode(root, root, loadBalancers.map(lb => colorsBySource.get(lb))); + loadBalancers.forEach(key => { const loadBalancerNodeKey = `${EXTERNAL_TRAFFIC_NODE}:${key}`; graph.setNode(loadBalancerNodeKey, { - height: NODE_HEIGHT, width: NODE_WIDTH, type: NODE_TYPES.externalLoadBalancer, label: key, color: colorsBySource.get(key)}); - graph.setEdge(loadBalancerNodeKey, treeNodeKey(root), { colors: [colorsBySource.get(key)]}); - graph.setEdge(EXTERNAL_TRAFFIC_NODE, loadBalancerNodeKey, { colors: [colorsBySource.get(key)]}); + height: NODE_HEIGHT, + width: NODE_WIDTH, + type: NODE_TYPES.externalLoadBalancer, + label: key, + color: colorsBySource.get(key) + }); + graph.setEdge(loadBalancerNodeKey, treeNodeKey(root), {colors: [colorsBySource.get(key)]}); + graph.setEdge(EXTERNAL_TRAFFIC_NODE, loadBalancerNodeKey, {colors: [colorsBySource.get(key)]}); }); }); } if (internalRoots.length > 0) { - graph.setNode(INTERNAL_TRAFFIC_NODE, { height: NODE_HEIGHT, width: 30, type: NODE_TYPES.internalTraffic }); - internalRoots.forEach((root) => { + graph.setNode(INTERNAL_TRAFFIC_NODE, {height: NODE_HEIGHT, width: 30, type: NODE_TYPES.internalTraffic}); + internalRoots.forEach(root => { processNode(root, root, [colorsBySource.get(treeNodeKey(root))]); graph.setEdge(INTERNAL_TRAFFIC_NODE, treeNodeKey(root)); }); @@ -331,9 +388,9 @@ export const ApplicationResourceTree = (props: ApplicationResourceTreeProps) => filterGraph(props.app, externalRoots.length > 0 ? EXTERNAL_TRAFFIC_NODE : INTERNAL_TRAFFIC_NODE, graph, props.nodeFilter); } } else { - roots.sort(compareNodes).forEach((node) => processNode(node, node)); - graph.setNode(appNodeKey(props.app), { ...appNode, width: NODE_WIDTH, height: NODE_HEIGHT }); - roots.forEach((root) => graph.setEdge(appNodeKey(props.app), treeNodeKey(root))); + roots.sort(compareNodes).forEach(node => processNode(node, node)); + graph.setNode(appNodeKey(props.app), {...appNode, width: NODE_WIDTH, height: NODE_HEIGHT}); + roots.forEach(root => graph.setEdge(appNodeKey(props.app), treeNodeKey(root))); if (props.nodeFilter) { filterGraph(props.app, appNodeKey(props.app), graph, props.nodeFilter); } @@ -341,10 +398,10 @@ export const ApplicationResourceTree = (props: ApplicationResourceTreeProps) => dagre.layout(graph); - const edges: {from: string, to: string, lines: Line[], backgroundImage?: string}[] = []; - graph.edges().forEach((edgeInfo) => { + const edges: {from: string; to: string; lines: Line[]; backgroundImage?: string}[] = []; + graph.edges().forEach(edgeInfo => { const edge = graph.edge(edgeInfo); - const colors = edge.colors as string[] || []; + const colors = (edge.colors as string[]) || []; let backgroundImage: string; if (colors.length > 0) { const step = 100 / colors.length; @@ -361,55 +418,63 @@ export const ApplicationResourceTree = (props: ApplicationResourceTreeProps) => } if (edge.points.length > 1) { for (let i = 1; i < edge.points.length; i++) { - lines.push({ x1: edge.points[i - 1].x, y1: edge.points[i - 1].y, x2: edge.points[i].x, y2: edge.points[i].y }); + lines.push({x1: edge.points[i - 1].x, y1: edge.points[i - 1].y, x2: edge.points[i].x, y2: edge.points[i].y}); } } - edges.push({ from: edgeInfo.v, to: edgeInfo.w, lines, backgroundImage }); + edges.push({from: edgeInfo.v, to: edgeInfo.w, lines, backgroundImage}); }); const graphNodes = graph.nodes(); - const size = getGraphSize(graphNodes.map((id) => graph.node(id))); - return graphNodes.length === 0 && ( - -

Your application has no network resources

-
Try switching to tree or list view
-
- ) || ( -
graph.node(id))); + return ( + (graphNodes.length === 0 && ( + +

Your application has no network resources

+
Try switching to tree or list view
+
+ )) || ( +
- {graphNodes.map((key) => { - const node = graph.node(key); - const nodeType = node.type; - switch (nodeType) { - case NODE_TYPES.filteredIndicator: - return {renderFilteredNode(node as any, props.onClearFilter)}; - case NODE_TYPES.externalTraffic: - return {renderTrafficNode(node)}; - case NODE_TYPES.internalTraffic: - return null; - case NODE_TYPES.externalLoadBalancer: - return {renderLoadBalancerNode(node as any)}; - default: - return {renderResourceNode(props, key, node as (ResourceTreeNode) & dagre.Node)}; - } - })} - {edges.map((edge) => ( -
- {edge.lines.map((line, i) => { - const distance = Math.sqrt(Math.pow(line.x1 - line.x2, 2) + Math.pow(line.y1 - line.y2, 2)); - const xMid = (line.x1 + line.x2) / 2; - const yMid = (line.y1 + line.y2) / 2; - const angle = Math.atan2(line.y1 - line.y2, line.x1 - line.x2) * 180 / Math.PI; - return ( -
- ); - })}
- ))} -
+ {graphNodes.map(key => { + const node = graph.node(key); + const nodeType = node.type; + switch (nodeType) { + case NODE_TYPES.filteredIndicator: + return {renderFilteredNode(node as any, props.onClearFilter)}; + case NODE_TYPES.externalTraffic: + return {renderTrafficNode(node)}; + case NODE_TYPES.internalTraffic: + return null; + case NODE_TYPES.externalLoadBalancer: + return {renderLoadBalancerNode(node as any)}; + default: + return {renderResourceNode(props, key, node as (ResourceTreeNode) & dagre.Node)}; + } + })} + {edges.map(edge => ( +
+ {edge.lines.map((line, i) => { + const distance = Math.sqrt(Math.pow(line.x1 - line.x2, 2) + Math.pow(line.y1 - line.y2, 2)); + const xMid = (line.x1 + line.x2) / 2; + const yMid = (line.y1 + line.y2) / 2; + const angle = (Math.atan2(line.y1 - line.y2, line.x1 - line.x2) * 180) / Math.PI; + return ( +
+ ); + })} +
+ ))} +
+ ) ); }; diff --git a/ui/src/app/applications/components/application-resource-tree/node-update-animation.tsx b/ui/src/app/applications/components/application-resource-tree/node-update-animation.tsx index 4da3ec1931c66..6e868c30723a8 100644 --- a/ui/src/app/applications/components/application-resource-tree/node-update-animation.tsx +++ b/ui/src/app/applications/components/application-resource-tree/node-update-animation.tsx @@ -1,18 +1,18 @@ import * as React from 'react'; -export class NodeUpdateAnimation extends React.PureComponent<{ resourceVersion: string; }, { ready: boolean }> { - constructor(props: { resourceVersion: string; }) { +export class NodeUpdateAnimation extends React.PureComponent<{resourceVersion: string}, {ready: boolean}> { + constructor(props: {resourceVersion: string}) { super(props); - this.state = { ready: false }; + this.state = {ready: false}; } public render() { - return this.state.ready &&
; + return this.state.ready &&
; } - public componentDidUpdate(prevProps: { resourceVersion: string; }) { + public componentDidUpdate(prevProps: {resourceVersion: string}) { if (prevProps.resourceVersion && this.props.resourceVersion !== prevProps.resourceVersion) { - this.setState({ ready: true }); + this.setState({ready: true}); } } } diff --git a/ui/src/app/applications/components/application-resources-diff/application-resources-diff.tsx b/ui/src/app/applications/components/application-resources-diff/application-resources-diff.tsx index b53db6aee14fe..1952ae0607cda 100644 --- a/ui/src/app/applications/components/application-resources-diff/application-resources-diff.tsx +++ b/ui/src/app/applications/components/application-resources-diff/application-resources-diff.tsx @@ -17,36 +17,38 @@ export interface ApplicationResourcesDiffProps { export const ApplicationResourcesDiff = (props: ApplicationResourcesDiffProps) => ( services.viewPreferences.getPreferences()}> - {(pref) => { - const diffText = props.states.map((state) => { - let live = state.liveState; - if (pref.appDetails.hideDefaultedFields && live) { - live = removeDefaultedFields(state.targetState, live); - } + {pref => { + const diffText = props.states + .map(state => { + let live = state.liveState; + if (pref.appDetails.hideDefaultedFields && live) { + live = removeDefaultedFields(state.targetState, live); + } - const liveCopy = JSON.parse(JSON.stringify(live || {})); - let target: any = null; - if (state.targetState) { - target = state.diff ? jsonDiffPatch.patch(liveCopy, JSON.parse(state.diff)) : liveCopy; - } + const liveCopy = JSON.parse(JSON.stringify(live || {})); + let target: any = null; + if (state.targetState) { + target = state.diff ? jsonDiffPatch.patch(liveCopy, JSON.parse(state.diff)) : liveCopy; + } - return { - a: live ? jsYaml.safeDump(live, {indent: 2}) : '', - b: target ? jsYaml.safeDump(target, {indent: 2}) : '', - hook: state.hook, - // doubles as sort order - name: (state.group || '') + '/' + state.kind + '/' + state.namespace + '/' + state.name, - }; - }) - .filter((i) => !i.hook) - .filter((i) => i.a !== i.b) - .map((i) => { - const context = pref.appDetails.compactDiff ? 2 : Number.MAX_SAFE_INTEGER; - // react-diff-view, awesome as it is, does not accept unidiff format, you must add a git header section - return `diff --git a/${i.name} b/${i.name} + return { + a: live ? jsYaml.safeDump(live, {indent: 2}) : '', + b: target ? jsYaml.safeDump(target, {indent: 2}) : '', + hook: state.hook, + // doubles as sort order + name: (state.group || '') + '/' + state.kind + '/' + state.namespace + '/' + state.name + }; + }) + .filter(i => !i.hook) + .filter(i => i.a !== i.b) + .map(i => { + const context = pref.appDetails.compactDiff ? 2 : Number.MAX_SAFE_INTEGER; + // react-diff-view, awesome as it is, does not accept unidiff format, you must add a git header section + return `diff --git a/${i.name} b/${i.name} index 6829b8a2..4c565f1b 100644 ${formatLines(diffLines(i.a, i.b), {context, aname: `a/${name}}`, bname: `b/${i.name}`})}`; - }).join('\n'); + }) + .join('\n'); // assume that if you only have one file, we don't need the file path const whiteBox = props.states.length > 1 ? 'white-box' : ''; const showPath = props.states.length > 1; @@ -55,41 +57,56 @@ ${formatLines(diffLines(i.a, i.b), {context, aname: `a/${name}}`, bname: `b/${i. return (
- services.viewPreferences.updatePreferences({ - appDetails: { - ...pref.appDetails, - compactDiff: !pref.appDetails.compactDiff, - }, - })}/> + + services.viewPreferences.updatePreferences({ + appDetails: { + ...pref.appDetails, + compactDiff: !pref.appDetails.compactDiff + } + }) + } + /> - services.viewPreferences.updatePreferences({ - appDetails: { - ...pref.appDetails, - inlineDiff: !pref.appDetails.inlineDiff, - }, - })}/> + + services.viewPreferences.updatePreferences({ + appDetails: { + ...pref.appDetails, + inlineDiff: !pref.appDetails.inlineDiff + } + }) + } + /> - services.viewPreferences.updatePreferences({ - appDetails: { - ...pref.appDetails, - hideDefaultedFields: !pref.appDetails.hideDefaultedFields, - }, - })}/> + + services.viewPreferences.updatePreferences({ + appDetails: { + ...pref.appDetails, + hideDefaultedFields: !pref.appDetails.hideDefaultedFields + } + }) + } + />
- {files.sort((a: any, b: any) => a.newPath.localeCompare(b.newPath)).map((file: any) => ( -
- {showPath && ( -

{file.newPath}

- )} - - {(hunks: any) => hunks.map((hunk: any) => ())} - -
- ))} + {files + .sort((a: any, b: any) => a.newPath.localeCompare(b.newPath)) + .map((file: any) => ( +
+ {showPath &&

{file.newPath}

} + + {(hunks: any) => hunks.map((hunk: any) => )} + +
+ ))}
); }} diff --git a/ui/src/app/applications/components/application-status-panel/application-status-panel.tsx b/ui/src/app/applications/components/application-status-panel/application-status-panel.tsx index 326a29df66d1e..f666c57818f9d 100644 --- a/ui/src/app/applications/components/application-status-panel/application-status-panel.tsx +++ b/ui/src/app/applications/components/application-status-panel/application-status-panel.tsx @@ -28,12 +28,13 @@ export const ApplicationStatusPanel = ({application, showOperation, showConditio } const cntByCategory = (application.status.conditions || []).reduce( (map, next) => map.set(utils.getConditionCategory(next), (map.get(utils.getConditionCategory(next)) || 0) + 1), - new Map()); + new Map() + ); let appOperationState = application.status.operationState; if (application.metadata.deletionTimestamp) { appOperationState = { phase: models.OperationPhases.Running, - startedAt: application.metadata.deletionTimestamp, + startedAt: application.metadata.deletionTimestamp } as models.OperationState; showOperation = null; } else if (application.operation) { @@ -41,14 +42,17 @@ export const ApplicationStatusPanel = ({application, showOperation, showConditio phase: models.OperationPhases.Running, startedAt: new Date().toISOString(), operation: { - sync: {}, - } as models.Operation, + sync: {} + } as models.Operation } as models.OperationState; } const tooltip = (title: string) => ( - + + {' '} + + ); @@ -56,7 +60,8 @@ export const ApplicationStatusPanel = ({application, showOperation, showConditio
-   + +   {application.status.health.status} {tooltip('The health status of your app')}
@@ -64,72 +69,85 @@ export const ApplicationStatusPanel = ({application, showOperation, showConditio
-   + +   {application.status.sync.status} {tooltip('Whether or not the version of your app is up to date with your repo. You may wish to sync your app if it is out-of-sync.')}
{syncStatusMessage(application)}
- { application.status && application.status.sync && application.status.sync.revision && ( - + {application.status && application.status.sync && application.status.sync.revision && ( + )}
{appOperationState && (
-
- showOperation && showOperation()}>{utils.getOperationType(application)} - +
+ showOperation && showOperation()}> + {utils.getOperationType(application)} + - {tooltip('Whether or not your last app sync was successful. It has been ' + daysSinceLastSynchronized + - ' days since last sync. Click for the status of that sync.')} + {tooltip( + 'Whether or not your last app sync was successful. It has been ' + + daysSinceLastSynchronized + + ' days since last sync. Click for the status of that sync.' + )}
{appOperationState.syncResult && ( -
To +
+ To
)}
- {appOperationState.phase} + {appOperationState.phase}
{appOperationState.syncResult && ( - + )}
)} {application.status.conditions && (
-
showConditions && showConditions()}> +
showConditions && showConditions()}> {cntByCategory.get('info') && {cntByCategory.get('info')} Info} - {cntByCategory.get('warning') && - {cntByCategory.get('warning')} Warnings} + {cntByCategory.get('warning') && {cntByCategory.get('warning')} Warnings} {cntByCategory.get('error') && {cntByCategory.get('error')} Errors}
)} - { - return await services.applications.getApplicationSyncWindowState(application.metadata.name); - }}>{(data) => - -
-
- {data.assignedWindows && ( - - - {tooltip('The aggregate state of sync windows for this app. ' + - 'Red: no syncs allowed. ' + - 'Yellow: manual syncs allowed. ' + - 'Green: all syncs allowed')} - - )} + { + return await services.applications.getApplicationSyncWindowState(application.metadata.name); + }}> + {data => ( + +
+
+ {data.assignedWindows && ( + + + {tooltip( + 'The aggregate state of sync windows for this app. ' + + 'Red: no syncs allowed. ' + + 'Yellow: manual syncs allowed. ' + + 'Green: all syncs allowed' + )} + + )} +
-
- - } + + )} +
); }; diff --git a/ui/src/app/applications/components/application-status-panel/revision-metadata-panel.tsx b/ui/src/app/applications/components/application-status-panel/revision-metadata-panel.tsx index 4245ad313ae97..a141e3b3b9327 100644 --- a/ui/src/app/applications/components/application-status-panel/revision-metadata-panel.tsx +++ b/ui/src/app/applications/components/application-status-panel/revision-metadata-panel.tsx @@ -4,10 +4,7 @@ import {Timestamp} from '../../../shared/components/timestamp'; import {ApplicationSource, RevisionMetadata} from '../../../shared/models'; import {services} from '../../../shared/services'; -export const RevisionMetadataPanel = (props: { - applicationName: string; - source: ApplicationSource; -}) => { +export const RevisionMetadataPanel = (props: {applicationName: string; source: ApplicationSource}) => { if (props.source.chart) { return (
@@ -16,23 +13,42 @@ export const RevisionMetadataPanel = (props: { ); } return ( - services.applications.revisionMetadata(input.applicationName, props.source.targetRevision || '')} - >{(m: RevisionMetadata) => ( - - {m.author && Authored by {m.author}} - {m.date && }
- {m.tags && (Tags: {m.tags}
)} - (m.message} - - )} placement='bottom' allowHTML={true}> -
- {m.author && Authored by {m.author}
} - {m.tags && Tagged {m.tags.join(', ')}
} - {m.message} -
-
- )}
+ services.applications.revisionMetadata(input.applicationName, props.source.targetRevision || '')}> + {(m: RevisionMetadata) => ( + + {m.author && Authored by {m.author}} + {m.date && } +
+ {m.tags && ( + + Tags: {m.tags} +
+
+ )} + (m.message} + + } + placement='bottom' + allowHTML={true}> +
+ {m.author && ( + + Authored by {m.author} +
+
+ )} + {m.tags && ( + + Tagged {m.tags.join(', ')} +
+
+ )} + {m.message} +
+
+ )} +
); }; diff --git a/ui/src/app/applications/components/application-summary/application-summary.tsx b/ui/src/app/applications/components/application-summary/application-summary.tsx index d460c897d34f4..56555ce60fb55 100644 --- a/ui/src/app/applications/components/application-summary/application-summary.tsx +++ b/ui/src/app/applications/components/application-summary/application-summary.tsx @@ -1,19 +1,21 @@ -import { DropDownMenu, FormField, FormSelect, PopupApi } from 'argo-ui'; +import {DropDownMenu, FormField, FormSelect, PopupApi} from 'argo-ui'; import * as React from 'react'; -import { FormApi, Text } from 'react-form'; +import {FormApi, Text} from 'react-form'; require('./application-summary.scss'); -import { AutocompleteField, Cluster, clusterTitle, DataLoader, EditablePanel, EditablePanelItem } from '../../../shared/components'; -import { MapInputField, Repo, Revision } from '../../../shared/components'; -import { Consumer } from '../../../shared/context'; +import {AutocompleteField, Cluster, clusterTitle, DataLoader, EditablePanel, EditablePanelItem} from '../../../shared/components'; +import {MapInputField, Repo, Revision} from '../../../shared/components'; +import {Consumer} from '../../../shared/context'; import * as models from '../../../shared/models'; -import { services } from '../../../shared/services'; +import {services} from '../../../shared/services'; -import { ComparisonStatusIcon, HealthStatusIcon, syncStatusMessage } from '../utils'; +import {ComparisonStatusIcon, HealthStatusIcon, syncStatusMessage} from '../utils'; -const urlPattern = new RegExp('^(https?:\\/\\/)?((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|((\\d{1,3}\\.){3}\\d{1,3}))' - + '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*(\\?[;&a-z\\d%_.~+=-]*)?(\\#[-a-z\\d_]*)?$', 'i'); +const urlPattern = new RegExp( + '^(https?:\\/\\/)?((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|((\\d{1,3}\\.){3}\\d{1,3}))' + '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*(\\?[;&a-z\\d%_.~+=-]*)?(\\#[-a-z\\d_]*)?$', + 'i' +); function swap(array: any[], a: number, b: number) { array = array.slice(); @@ -21,185 +23,256 @@ function swap(array: any[], a: number, b: number) { return array; } -export const ApplicationSummary = (props: { - app: models.Application, - updateApp: (app: models.Application) => Promise, -}) => { +export const ApplicationSummary = (props: {app: models.Application; updateApp: (app: models.Application) => Promise}) => { const app = JSON.parse(JSON.stringify(props.app)) as models.Application; const isHelm = app.spec.source.hasOwnProperty('chart'); const attributes = [ { title: 'PROJECT', - view: ( - {app.spec.project} - ), + view: {app.spec.project}, edit: (formApi: FormApi) => ( - services.projects.list().then((projs) => projs.map((item) => item.metadata.name))}> - {(projects) => } + services.projects.list().then(projs => projs.map(item => item.metadata.name))}> + {projects => } - ), + ) }, { title: 'LABELS', - view: Object.keys(app.metadata.labels || {}).map((label) => `${label}=${app.metadata.labels[label]}`).join(' '), - edit: (formApi: FormApi) => ( - - ), + view: Object.keys(app.metadata.labels || {}) + .map(label => `${label}=${app.metadata.labels[label]}`) + .join(' '), + edit: (formApi: FormApi) => }, { title: 'CLUSTER', - view: , + view: , edit: (formApi: FormApi) => ( - services.clusters.list().then((clusters) => clusters.map((cluster) => ({ - title: clusterTitle(cluster), - value: cluster.server, - })))}> - {(clusters) => ( - - )} + + services.clusters.list().then(clusters => + clusters.map(cluster => ({ + title: clusterTitle(cluster), + value: cluster.server + })) + ) + }> + {clusters => } - ), + ) }, { title: 'NAMESPACE', view: app.spec.destination.namespace, - edit: (formApi: FormApi) => , + edit: (formApi: FormApi) => }, { title: 'REPO URL', - view: , - edit: (formApi: FormApi) => , + view: , + edit: (formApi: FormApi) => }, - ...(isHelm ? [{ - title: 'CHART', - view: {app.spec.source.chart}:{app.spec.source.targetRevision}, - edit: (formApi: FormApi) => ( - services.repos.charts(src.repoURL).catch(() => new Array())}> - {(charts: models.HelmChart[]) => ( -
-
- chart.name), filterSuggestions: true, - }}/> -
- { - const chartInfo = data.charts.find((chart) => chart.name === data.chart); - return chartInfo && chartInfo.versions || new Array(); - }}> - {(versions: string[]) => ( -
- -
- )} -
-
- )} -
- ), - }] : [{ - title: 'TARGET REVISION', - view: , - edit: (formApi: FormApi) => , - }, { - title: 'PATH', - view: app.spec.source.path, - edit: (formApi: FormApi) => , - }]), - {title: 'STATUS', view: ( - {app.status.sync.status} {syncStatusMessage(app)} - - )}, - {title: 'HEALTH', view: ( - {app.status.health.status} - )}, + ...(isHelm + ? [ + { + title: 'CHART', + view: ( + + {app.spec.source.chart}:{app.spec.source.targetRevision} + + ), + edit: (formApi: FormApi) => ( + services.repos.charts(src.repoURL).catch(() => new Array())}> + {(charts: models.HelmChart[]) => ( +
+
+ chart.name), + filterSuggestions: true + }} + /> +
+ { + const chartInfo = data.charts.find(chart => chart.name === data.chart); + return (chartInfo && chartInfo.versions) || new Array(); + }}> + {(versions: string[]) => ( +
+ +
+ )} +
+
+ )} +
+ ) + } + ] + : [ + { + title: 'TARGET REVISION', + view: , + edit: (formApi: FormApi) => + }, + { + title: 'PATH', + view: app.spec.source.path, + edit: (formApi: FormApi) => + } + ]), + { + title: 'STATUS', + view: ( + + {app.status.sync.status} {syncStatusMessage(app)} + + ) + }, + { + title: 'HEALTH', + view: ( + + {app.status.health.status} + + ) + } ]; const urls = app.status.summary.externalURLs || []; if (urls.length > 0) { - attributes.push({title: 'URLs', view: ( - - {urls.map((item) => {item}  )} - - )}); + attributes.push({ + title: 'URLs', + view: ( + + {urls.map(item => ( + + {item}   + + ))} + + ) + }); } if ((app.status.summary.images || []).length) { - attributes.push({title: 'IMAGES', view: ( -
- {(app.status.summary.images || []).sort().map((image) => ({image}))} -
- ) }); + attributes.push({ + title: 'IMAGES', + view: ( +
+ {(app.status.summary.images || []).sort().map(image => ( + + {image} + + ))} +
+ ) + }); } - async function setAutoSync(ctx: { popup: PopupApi }, confirmationTitle: string, confirmationText: string, prune: boolean, selfHeal: boolean) { + async function setAutoSync(ctx: {popup: PopupApi}, confirmationTitle: string, confirmationText: string, prune: boolean, selfHeal: boolean) { const confirmed = await ctx.popup.confirm(confirmationTitle, confirmationText); if (confirmed) { const updatedApp = JSON.parse(JSON.stringify(props.app)) as models.Application; - updatedApp.spec.syncPolicy = { automated: { prune, selfHeal } }; + updatedApp.spec.syncPolicy = {automated: {prune, selfHeal}}; props.updateApp(updatedApp); } } - async function unsetAutoSync(ctx: { popup: PopupApi }) { + async function unsetAutoSync(ctx: {popup: PopupApi}) { const confirmed = await ctx.popup.confirm('Disable Auto-Sync?', 'Are you sure you want to disable automated application synchronization'); if (confirmed) { const updatedApp = JSON.parse(JSON.stringify(props.app)) as models.Application; - updatedApp.spec.syncPolicy = { automated: null }; + updatedApp.spec.syncPolicy = {automated: null}; props.updateApp(updatedApp); } } - const items = (app.spec.info || []); + const items = app.spec.info || []; const [adjustedCount, setAdjustedCount] = React.useState(0); - const added = new Array<{name: string, value: string, key: string}>(); + const added = new Array<{name: string; value: string; key: string}>(); for (let i = 0; i < adjustedCount; i++) { - added.push({ name: '', value: '', key: (items.length + i).toString()}); + added.push({name: '', value: '', key: (items.length + i).toString()}); } for (let i = 0; i > adjustedCount; i--) { items.pop(); } const allItems = items.concat(added); - const infoItems: EditablePanelItem[] = allItems.map((info, i) => ({ - key: i.toString(), - title: info.name, - view: info.value.match(urlPattern) ? {info.value} : info.value, - titleEdit: (formApi: FormApi) => ( - - {i > 0 && { - formApi.setValue('spec.info', swap((formApi.getFormState().values.spec.info || []), i, i - 1)); - }}/>} - - {i < allItems.length - 1 && { - formApi.setValue('spec.info', swap((formApi.getFormState().values.spec.info || []), i, i + 1)); - }}/> } - - ), - edit: (formApi: FormApi) => ( - - - { - const values = (formApi.getFormState().values.spec.info || []) as Array; - formApi.setValue('spec.info', [...values.slice(0, i), ...values.slice(i + 1, values.length)]); - setAdjustedCount(adjustedCount - 1); - }}/> - - ), - })).concat({ - key: '-1', - title: '', - titleEdit: () => ( - - ), - view: null as any, - edit: null, - }); + const infoItems: EditablePanelItem[] = allItems + .map((info, i) => ({ + key: i.toString(), + title: info.name, + view: info.value.match(urlPattern) ? ( + + {info.value} + + ) : ( + info.value + ), + titleEdit: (formApi: FormApi) => ( + + {i > 0 && ( + { + formApi.setValue('spec.info', swap(formApi.getFormState().values.spec.info || [], i, i - 1)); + }} + /> + )} + + {i < allItems.length - 1 && ( + { + formApi.setValue('spec.info', swap(formApi.getFormState().values.spec.info || [], i, i + 1)); + }} + /> + )} + + ), + edit: (formApi: FormApi) => ( + + + { + const values = (formApi.getFormState().values.spec.info || []) as Array; + formApi.setValue('spec.info', [...values.slice(0, i), ...values.slice(i + 1, values.length)]); + setAdjustedCount(adjustedCount - 1); + }} + /> + + ) + })) + .concat({ + key: '-1', + title: '', + titleEdit: () => ( + + ), + view: null as any, + edit: null + }); const [badgeType, setBadgeType] = React.useState('URL'); const badgeURL = `${location.protocol}//${location.host}/api/badge?name=${props.app.metadata.name}`; const appURL = `${location.protocol}//${location.host}/applications/${props.app.metadata.name}`; @@ -207,98 +280,159 @@ export const ApplicationSummary = (props: { return (
({ - 'spec.project': !input.spec.project && 'Project name is required', - 'spec.destination.server': !input.spec.destination.server && 'Cluster is required', - 'spec.destination.namespace': !input.spec.destination.namespace && 'Namespace is required', - })} values={app} title={app.metadata.name.toLocaleUpperCase()} items={attributes} /> - {(ctx) => ( -
-
-

Sync Policy

-
-
- {app.spec.syncPolicy && app.spec.syncPolicy.automated && Automated || None} -
-
- {app.spec.syncPolicy && app.spec.syncPolicy.automated && ( - - ) || ( - - )} -
-
- - {app.spec.syncPolicy && app.spec.syncPolicy.automated && ( - -
-
- Prune Resources -
-
- {app.spec.syncPolicy.automated.prune && ( - - ) || ( - - )} -
-
+ save={props.updateApp} + validate={input => ({ + 'spec.project': !input.spec.project && 'Project name is required', + 'spec.destination.server': !input.spec.destination.server && 'Cluster is required', + 'spec.destination.namespace': !input.spec.destination.namespace && 'Namespace is required' + })} + values={app} + title={app.metadata.name.toLocaleUpperCase()} + items={attributes} + /> + + {ctx => ( +
+
+

Sync Policy

-
- Self Heal -
+
{(app.spec.syncPolicy && app.spec.syncPolicy.automated && Automated) || None}
- {app.spec.syncPolicy.automated.selfHeal && ( - - ) || ( - + {(app.spec.syncPolicy && app.spec.syncPolicy.automated && ( + + )) || ( + )}
- - )} -
-
- )}
+ + {app.spec.syncPolicy && app.spec.syncPolicy.automated && ( + +
+
Prune Resources
+
+ {(app.spec.syncPolicy.automated.prune && ( + + )) || ( + + )} +
+
+
+
Self Heal
+
+ {(app.spec.syncPolicy.automated.selfHeal && ( + + )) || ( + + )} +
+
+
+ )} +
+
+ )} +
services.authService.settings()}> - {(settings) => ( - settings.statusBadgeEnabled && ( -
-
-

Status Badge

-
- (

{badgeType}

)} - items={['URL', 'Markdown', 'Textile', 'Rdoc', 'AsciiDoc'].map((type) => ({ title: type, action: () => setBadgeType(type) }))} - /> -