From 4fc4c8d6af515e06b42c66af80bb11010cd00020 Mon Sep 17 00:00:00 2001 From: reggie Date: Sat, 6 May 2023 13:29:02 +0300 Subject: [PATCH 01/38] initial Signed-off-by: reggie --- ui/src/app/app.tsx | 8 + .../__snapshots__/utils.test.tsx.snap | 323 ++++ .../application-conditions.scss | 30 + .../application-conditions.tsx | 32 + .../application-create-panel.scss | 28 + .../application-create-panel.tsx | 529 +++++++ .../set-finalizer-on-application.tsx | 40 + .../application-deployment-history.scss | 46 + .../application-deployment-history.tsx | 103 ++ .../revision-metadata-rows.tsx | 72 + .../application-details-app-dropdown.tsx | 55 + .../application-details.scss | 377 +++++ .../application-details.tsx | 992 +++++++++++++ .../application-resource-filter.tsx | 173 +++ .../application-resource-list.tsx | 157 ++ .../application-fullscreen-logs.scss | 13 + .../application-fullscreen-logs.tsx | 37 + .../application-node-info.scss | 69 + .../application-node-info.tsx | 289 ++++ .../readiness-gates-failed-warning.scss | 12 + .../readiness-gates-failed-warning.tsx | 44 + .../application-operation-state.scss | 19 + .../application-operation-state.tsx | 213 +++ .../application-parameters.tsx | 549 +++++++ .../kustomize-image.test.ts | 38 + .../application-parameters/kustomize-image.ts | 58 + .../application-parameters/kustomize.tsx | 63 + .../vars-input-field.tsx | 37 + .../application-pod-view/pod-tooltip.tsx | 51 + .../application-pod-view/pod-view.scss | 252 ++++ .../application-pod-view/pod-view.tsx | 465 ++++++ .../application-resource-events.tsx | 19 + .../application-resource-tree.scss | 419 ++++++ .../application-resource-tree.test.tsx | 93 ++ .../application-resource-tree.tsx | 1285 ++++++++++++++++ .../arrow-connector.tsx | 24 + .../node-update-animation.tsx | 18 + .../application-resources-diff.scss | 33 + .../application-resources-diff.tsx | 84 ++ .../individual-diff-section.tsx | 33 + .../application-retry-options.scss | 16 + .../application-retry-options.tsx | 128 ++ .../application-retry-view.scss | 6 + .../application-retry-view.tsx | 26 + .../application-status-panel.scss | 198 +++ .../application-status-panel.tsx | 210 +++ .../revision-metadata-panel.tsx | 64 + .../application-summary.scss | 80 + .../application-summary.tsx | 605 ++++++++ .../application-summary/edit-annotations.tsx | 33 + .../edit-notification-subscriptions.scss | 28 + .../edit-notification-subscriptions.tsx | 220 +++ .../application-sync-options.scss | 32 + .../application-sync-options.tsx | 161 ++ .../application-sync-panel.scss | 35 + .../application-sync-panel.tsx | 229 +++ .../components/application-urls.test.ts | 20 + .../components/application-urls.tsx | 79 + .../applications-list/applications-filter.tsx | 295 ++++ .../applications-labels.scss | 23 + .../applications-list/applications-labels.tsx | 36 + .../applications-list/applications-list.scss | 221 +++ .../applications-list/applications-list.tsx | 676 +++++++++ .../applications-source.scss | 5 + .../applications-list/applications-source.tsx | 14 + .../applications-status-bar.scss | 32 + .../applications-status-bar.tsx | 79 + .../applications-summary.tsx | 118 ++ .../applications-list/applications-table.scss | 31 + .../applications-list/applications-table.tsx | 159 ++ .../applications-list/applications-tiles.scss | 11 + .../applications-list/applications-tiles.tsx | 328 +++++ .../applications-list/flex-top-bar.scss | 39 + .../applications-refresh-panel.tsx | 97 ++ .../applications-sync-panel.tsx | 159 ++ .../components/applicationsets-container.tsx | 12 + .../components/filter/filter.scss | 143 ++ .../components/filter/filter.tsx | 185 +++ .../components/label-selector.test.ts | 45 + .../components/label-selector.ts | 39 + .../pod-logs-viewer/container-selector.tsx | 39 + .../pod-logs-viewer/copy-logs-button.tsx | 30 + .../dark-mode-toggle-button.tsx | 19 + .../pod-logs-viewer/download-logs-button.tsx | 16 + .../pod-logs-viewer/follow-toggle-button.tsx | 7 + .../pod-logs-viewer/fullscreen-button.tsx | 26 + .../components/pod-logs-viewer/log-loader.ts | 4 + .../pod-logs-viewer/log-message-filter.tsx | 9 + .../pod-logs-viewer/pod-logs-viewer.scss | 238 +++ .../pod-logs-viewer/pod-logs-viewer.tsx | 254 ++++ .../pod-names-toggle-button.tsx | 7 + .../show-previous-logs-toggle-button.tsx | 24 + .../since-seconds-selector.tsx | 17 + .../pod-logs-viewer/tail-selector.tsx | 15 + .../timestamps-toggle-button.tsx | 23 + .../pod-terminal-viewer.scss | 7 + .../pod-terminal-viewer.tsx | 256 ++++ .../resource-details/resource-details.scss | 20 + .../resource-details/resource-details.tsx | 370 +++++ .../components/resource-icon.tsx | 34 + .../components/resource-label.test.ts | 9 + .../components/resource-label.ts | 5 + .../applicationsets/components/resources.ts | 77 + .../revision-form-field.tsx | 83 ++ .../app/applicationsets/components/utils.scss | 24 + .../applicationsets/components/utils.test.tsx | 223 +++ .../app/applicationsets/components/utils.tsx | 1297 +++++++++++++++++ ui/src/app/applicationsets/index.ts | 5 + ui/src/app/shared/models.ts | 68 + .../services/applicationsets-service.ts | 204 +++ ui/src/app/shared/services/index.ts | 3 + .../services/view-preferences-service.ts | 41 + 112 files changed, 15553 insertions(+) create mode 100644 ui/src/app/applicationsets/components/__snapshots__/utils.test.tsx.snap create mode 100644 ui/src/app/applicationsets/components/application-conditions/application-conditions.scss create mode 100644 ui/src/app/applicationsets/components/application-conditions/application-conditions.tsx create mode 100644 ui/src/app/applicationsets/components/application-create-panel/application-create-panel.scss create mode 100644 ui/src/app/applicationsets/components/application-create-panel/application-create-panel.tsx create mode 100644 ui/src/app/applicationsets/components/application-create-panel/set-finalizer-on-application.tsx create mode 100644 ui/src/app/applicationsets/components/application-deployment-history/application-deployment-history.scss create mode 100644 ui/src/app/applicationsets/components/application-deployment-history/application-deployment-history.tsx create mode 100644 ui/src/app/applicationsets/components/application-deployment-history/revision-metadata-rows.tsx create mode 100644 ui/src/app/applicationsets/components/application-details/application-details-app-dropdown.tsx create mode 100644 ui/src/app/applicationsets/components/application-details/application-details.scss create mode 100644 ui/src/app/applicationsets/components/application-details/application-details.tsx create mode 100644 ui/src/app/applicationsets/components/application-details/application-resource-filter.tsx create mode 100644 ui/src/app/applicationsets/components/application-details/application-resource-list.tsx create mode 100644 ui/src/app/applicationsets/components/application-fullscreen-logs/application-fullscreen-logs.scss create mode 100644 ui/src/app/applicationsets/components/application-fullscreen-logs/application-fullscreen-logs.tsx create mode 100644 ui/src/app/applicationsets/components/application-node-info/application-node-info.scss create mode 100644 ui/src/app/applicationsets/components/application-node-info/application-node-info.tsx create mode 100644 ui/src/app/applicationsets/components/application-node-info/readiness-gates-failed-warning.scss create mode 100644 ui/src/app/applicationsets/components/application-node-info/readiness-gates-failed-warning.tsx create mode 100644 ui/src/app/applicationsets/components/application-operation-state/application-operation-state.scss create mode 100644 ui/src/app/applicationsets/components/application-operation-state/application-operation-state.tsx create mode 100644 ui/src/app/applicationsets/components/application-parameters/application-parameters.tsx create mode 100644 ui/src/app/applicationsets/components/application-parameters/kustomize-image.test.ts create mode 100644 ui/src/app/applicationsets/components/application-parameters/kustomize-image.ts create mode 100644 ui/src/app/applicationsets/components/application-parameters/kustomize.tsx create mode 100644 ui/src/app/applicationsets/components/application-parameters/vars-input-field.tsx create mode 100644 ui/src/app/applicationsets/components/application-pod-view/pod-tooltip.tsx create mode 100644 ui/src/app/applicationsets/components/application-pod-view/pod-view.scss create mode 100644 ui/src/app/applicationsets/components/application-pod-view/pod-view.tsx create mode 100644 ui/src/app/applicationsets/components/application-resource-events/application-resource-events.tsx create mode 100644 ui/src/app/applicationsets/components/application-resource-tree/application-resource-tree.scss create mode 100644 ui/src/app/applicationsets/components/application-resource-tree/application-resource-tree.test.tsx create mode 100644 ui/src/app/applicationsets/components/application-resource-tree/application-resource-tree.tsx create mode 100644 ui/src/app/applicationsets/components/application-resource-tree/arrow-connector.tsx create mode 100644 ui/src/app/applicationsets/components/application-resource-tree/node-update-animation.tsx create mode 100644 ui/src/app/applicationsets/components/application-resources-diff/application-resources-diff.scss create mode 100644 ui/src/app/applicationsets/components/application-resources-diff/application-resources-diff.tsx create mode 100644 ui/src/app/applicationsets/components/application-resources-diff/individual-diff-section.tsx create mode 100644 ui/src/app/applicationsets/components/application-retry-options/application-retry-options.scss create mode 100644 ui/src/app/applicationsets/components/application-retry-options/application-retry-options.tsx create mode 100644 ui/src/app/applicationsets/components/application-retry-view/application-retry-view.scss create mode 100644 ui/src/app/applicationsets/components/application-retry-view/application-retry-view.tsx create mode 100644 ui/src/app/applicationsets/components/application-status-panel/application-status-panel.scss create mode 100644 ui/src/app/applicationsets/components/application-status-panel/application-status-panel.tsx create mode 100644 ui/src/app/applicationsets/components/application-status-panel/revision-metadata-panel.tsx create mode 100644 ui/src/app/applicationsets/components/application-summary/application-summary.scss create mode 100644 ui/src/app/applicationsets/components/application-summary/application-summary.tsx create mode 100644 ui/src/app/applicationsets/components/application-summary/edit-annotations.tsx create mode 100644 ui/src/app/applicationsets/components/application-summary/edit-notification-subscriptions.scss create mode 100644 ui/src/app/applicationsets/components/application-summary/edit-notification-subscriptions.tsx create mode 100644 ui/src/app/applicationsets/components/application-sync-options/application-sync-options.scss create mode 100644 ui/src/app/applicationsets/components/application-sync-options/application-sync-options.tsx create mode 100644 ui/src/app/applicationsets/components/application-sync-panel/application-sync-panel.scss create mode 100644 ui/src/app/applicationsets/components/application-sync-panel/application-sync-panel.tsx create mode 100644 ui/src/app/applicationsets/components/application-urls.test.ts create mode 100644 ui/src/app/applicationsets/components/application-urls.tsx create mode 100644 ui/src/app/applicationsets/components/applications-list/applications-filter.tsx create mode 100644 ui/src/app/applicationsets/components/applications-list/applications-labels.scss create mode 100644 ui/src/app/applicationsets/components/applications-list/applications-labels.tsx create mode 100644 ui/src/app/applicationsets/components/applications-list/applications-list.scss create mode 100644 ui/src/app/applicationsets/components/applications-list/applications-list.tsx create mode 100644 ui/src/app/applicationsets/components/applications-list/applications-source.scss create mode 100644 ui/src/app/applicationsets/components/applications-list/applications-source.tsx create mode 100644 ui/src/app/applicationsets/components/applications-list/applications-status-bar.scss create mode 100644 ui/src/app/applicationsets/components/applications-list/applications-status-bar.tsx create mode 100644 ui/src/app/applicationsets/components/applications-list/applications-summary.tsx create mode 100644 ui/src/app/applicationsets/components/applications-list/applications-table.scss create mode 100644 ui/src/app/applicationsets/components/applications-list/applications-table.tsx create mode 100644 ui/src/app/applicationsets/components/applications-list/applications-tiles.scss create mode 100644 ui/src/app/applicationsets/components/applications-list/applications-tiles.tsx create mode 100644 ui/src/app/applicationsets/components/applications-list/flex-top-bar.scss create mode 100644 ui/src/app/applicationsets/components/applications-refresh-panel/applications-refresh-panel.tsx create mode 100644 ui/src/app/applicationsets/components/applications-sync-panel/applications-sync-panel.tsx create mode 100644 ui/src/app/applicationsets/components/applicationsets-container.tsx create mode 100644 ui/src/app/applicationsets/components/filter/filter.scss create mode 100644 ui/src/app/applicationsets/components/filter/filter.tsx create mode 100644 ui/src/app/applicationsets/components/label-selector.test.ts create mode 100644 ui/src/app/applicationsets/components/label-selector.ts create mode 100644 ui/src/app/applicationsets/components/pod-logs-viewer/container-selector.tsx create mode 100644 ui/src/app/applicationsets/components/pod-logs-viewer/copy-logs-button.tsx create mode 100644 ui/src/app/applicationsets/components/pod-logs-viewer/dark-mode-toggle-button.tsx create mode 100644 ui/src/app/applicationsets/components/pod-logs-viewer/download-logs-button.tsx create mode 100644 ui/src/app/applicationsets/components/pod-logs-viewer/follow-toggle-button.tsx create mode 100644 ui/src/app/applicationsets/components/pod-logs-viewer/fullscreen-button.tsx create mode 100644 ui/src/app/applicationsets/components/pod-logs-viewer/log-loader.ts create mode 100644 ui/src/app/applicationsets/components/pod-logs-viewer/log-message-filter.tsx create mode 100644 ui/src/app/applicationsets/components/pod-logs-viewer/pod-logs-viewer.scss create mode 100644 ui/src/app/applicationsets/components/pod-logs-viewer/pod-logs-viewer.tsx create mode 100644 ui/src/app/applicationsets/components/pod-logs-viewer/pod-names-toggle-button.tsx create mode 100644 ui/src/app/applicationsets/components/pod-logs-viewer/show-previous-logs-toggle-button.tsx create mode 100644 ui/src/app/applicationsets/components/pod-logs-viewer/since-seconds-selector.tsx create mode 100644 ui/src/app/applicationsets/components/pod-logs-viewer/tail-selector.tsx create mode 100644 ui/src/app/applicationsets/components/pod-logs-viewer/timestamps-toggle-button.tsx create mode 100644 ui/src/app/applicationsets/components/pod-terminal-viewer/pod-terminal-viewer.scss create mode 100644 ui/src/app/applicationsets/components/pod-terminal-viewer/pod-terminal-viewer.tsx create mode 100644 ui/src/app/applicationsets/components/resource-details/resource-details.scss create mode 100644 ui/src/app/applicationsets/components/resource-details/resource-details.tsx create mode 100644 ui/src/app/applicationsets/components/resource-icon.tsx create mode 100644 ui/src/app/applicationsets/components/resource-label.test.ts create mode 100644 ui/src/app/applicationsets/components/resource-label.ts create mode 100644 ui/src/app/applicationsets/components/resources.ts create mode 100644 ui/src/app/applicationsets/components/revision-form-field/revision-form-field.tsx create mode 100644 ui/src/app/applicationsets/components/utils.scss create mode 100644 ui/src/app/applicationsets/components/utils.test.tsx create mode 100644 ui/src/app/applicationsets/components/utils.tsx create mode 100644 ui/src/app/applicationsets/index.ts create mode 100644 ui/src/app/shared/services/applicationsets-service.ts diff --git a/ui/src/app/app.tsx b/ui/src/app/app.tsx index b474c86b3ca84..2bb1ffcabb4c8 100644 --- a/ui/src/app/app.tsx +++ b/ui/src/app/app.tsx @@ -5,6 +5,7 @@ import * as React from 'react'; import {Helmet} from 'react-helmet'; import {Redirect, Route, RouteComponentProps, Router, Switch} from 'react-router'; import applications from './applications'; +import applicationsets from './applicationsets'; import help from './help'; import login from './login'; import settings from './settings'; @@ -29,6 +30,7 @@ type Routes = {[path: string]: {component: React.ComponentType, + " ", +] +`; + +exports[`ComparisonStatusIcon.Synced 1`] = ` +Array [ + , + " ", +] +`; + +exports[`ComparisonStatusIcon.Unknown 1`] = ` +Array [ + , + " ", +] +`; + +exports[`HealthStatusIcon.Degraded 1`] = ` + +`; + +exports[`HealthStatusIcon.Healthy 1`] = ` + +`; + +exports[`HealthStatusIcon.Missing 1`] = ` + +`; + +exports[`HealthStatusIcon.Progressing 1`] = ` + +`; + +exports[`HealthStatusIcon.Suspended 1`] = ` + +`; + +exports[`HealthStatusIcon.Unknown 1`] = ` + +`; + +exports[`OperationState.Deleting 1`] = ` +Array [ + , + " ", + "Deleting", +] +`; + +exports[`OperationState.Sync OK 1`] = ` +Array [ + , + " ", + "Sync OK", +] +`; + +exports[`OperationState.Sync error 1`] = ` +Array [ + , + " ", + "Sync error", +] +`; + +exports[`OperationState.Sync failed 1`] = ` +Array [ + , + " ", + "Sync failed", +] +`; + +exports[`OperationState.Syncing 1`] = ` +Array [ + , + " ", + "Syncing", +] +`; + +exports[`OperationState.Unknown 1`] = ` +Array [ + , + " ", + "Unknown", +] +`; + +exports[`OperationState.quiet 1`] = `null`; + +exports[`OperationState.undefined 1`] = `null`; + +exports[`ResourceResultIcon.Hook.Error 1`] = ` + +`; + +exports[`ResourceResultIcon.Hook.Failed 1`] = ` + +`; + +exports[`ResourceResultIcon.Hook.Running 1`] = ` + +`; + +exports[`ResourceResultIcon.Hook.Succeeded 1`] = ` + +`; + +exports[`ResourceResultIcon.Hook.Terminating 1`] = ` + +`; + +exports[`ResourceResultIcon.Pruned 1`] = ` + +`; + +exports[`ResourceResultIcon.SyncFailed 1`] = ` + +`; + +exports[`ResourceResultIcon.Synced 1`] = ` + +`; diff --git a/ui/src/app/applicationsets/components/application-conditions/application-conditions.scss b/ui/src/app/applicationsets/components/application-conditions/application-conditions.scss new file mode 100644 index 0000000000000..d2b6003cddd5f --- /dev/null +++ b/ui/src/app/applicationsets/components/application-conditions/application-conditions.scss @@ -0,0 +1,30 @@ +@import 'node_modules/argo-ui/src/styles/config'; + +.application-conditions { + &__condition { + border-left: 5px solid $argo-color-gray-4; + line-height: 1.8em; + padding: 10px 0; + &--error { + border-left-color: $argo-failed-color-dark; + } + + &--warning { + border-left-color: $argo-status-warning-color; + } + + &--info { + border-left-color: $argo-success-color; + } + + .columns { + white-space: normal; + } + + .row { + line-height: 2; + padding-top: 15px; + padding-bottom: 15px; + } + } +} diff --git a/ui/src/app/applicationsets/components/application-conditions/application-conditions.tsx b/ui/src/app/applicationsets/components/application-conditions/application-conditions.tsx new file mode 100644 index 0000000000000..317b135eeffe4 --- /dev/null +++ b/ui/src/app/applicationsets/components/application-conditions/application-conditions.tsx @@ -0,0 +1,32 @@ +import * as React from 'react'; + +import {Timestamp} from '../../../shared/components'; +import * as models from '../../../shared/models'; +import {getConditionCategory} from '../utils'; + +import './application-conditions.scss'; + +export const ApplicationConditions = ({conditions}: {conditions: models.ApplicationCondition[]}) => { + return ( +
+

Application conditions

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

Application is healthy

) || ( +
+ {conditions.map((condition, index) => ( +
+
+
{condition.type}
+
+ {condition.message} +
+
+ +
+
+
+ ))} +
+ )} +
+ ); +}; diff --git a/ui/src/app/applicationsets/components/application-create-panel/application-create-panel.scss b/ui/src/app/applicationsets/components/application-create-panel/application-create-panel.scss new file mode 100644 index 0000000000000..86e239970bd47 --- /dev/null +++ b/ui/src/app/applicationsets/components/application-create-panel/application-create-panel.scss @@ -0,0 +1,28 @@ +@import 'node_modules/argo-ui/src/styles/config'; + +.application-create-panel { + + &__yaml-button { + position: absolute; + right: 1em; + top: 1em; + } + + &__sync-params { + padding-top: 5px; + } + + .checkbox-container { + margin: 0.5em ; + } + + pre { + font-family: monospace; + line-height: normal; + white-space: pre; + } + + .row.argo-form-row .columns { + padding-left: 0; + } +} diff --git a/ui/src/app/applicationsets/components/application-create-panel/application-create-panel.tsx b/ui/src/app/applicationsets/components/application-create-panel/application-create-panel.tsx new file mode 100644 index 0000000000000..224c3da5f279d --- /dev/null +++ b/ui/src/app/applicationsets/components/application-create-panel/application-create-panel.tsx @@ -0,0 +1,529 @@ +import {AutocompleteField, Checkbox, DataLoader, DropDownMenu, FormField, HelpIcon, Select} from 'argo-ui'; +import * as deepMerge from 'deepmerge'; +import * as React from 'react'; +import {FieldApi, Form, FormApi, FormField as ReactFormField, Text} from 'react-form'; +import {RevisionHelpIcon, YamlEditor} from '../../../shared/components'; +import * as models from '../../../shared/models'; +import {services} from '../../../shared/services'; +import {ApplicationParameters} from '../application-parameters/application-parameters'; +import {ApplicationRetryOptions} from '../application-retry-options/application-retry-options'; +import {ApplicationSyncOptionsField} from '../application-sync-options/application-sync-options'; +import {RevisionFormField} from '../revision-form-field/revision-form-field'; +import {SetFinalizerOnApplication} from './set-finalizer-on-application'; +import './application-create-panel.scss'; +import {getAppDefaultSource} from '../utils'; + +const jsonMergePatch = require('json-merge-patch'); + +const appTypes = new Array<{field: string; type: models.AppSourceType}>( + {type: 'Helm', field: 'helm'}, + {type: 'Kustomize', field: 'kustomize'}, + {type: 'Directory', field: 'directory'}, + {type: 'Plugin', field: 'plugin'} +); + +const DEFAULT_APP: Partial = { + apiVersion: 'argoproj.io/v1alpha1', + kind: 'Application', + metadata: { + name: '' + }, + spec: { + destination: { + name: '', + namespace: '', + server: '' + }, + source: { + path: '', + repoURL: '', + targetRevision: 'HEAD' + }, + sources: [], + project: '' + } +}; + +const AutoSyncFormField = ReactFormField((props: {fieldApi: FieldApi; className: string}) => { + const manual = 'Manual'; + const auto = 'Automatic'; + const { + fieldApi: {getValue, setValue} + } = props; + const automated = getValue() as models.Automated; + + return ( + + + setAppFilter(e.target.value)} + ref={el => + el && + setTimeout(() => { + if (el) { + el.focus(); + } + }, 100) + } + /> + + services.applications.list([], {fields: ['items.metadata.name']})}> + {apps => + apps.items + .filter(app => { + return appFilter.length === 0 || app.metadata.name.toLowerCase().includes(appFilter.toLowerCase()); + }) + .slice(0, 100) // take top 100 results after filtering to avoid performance issues + .map(app => ( +
  • ctx.navigation.goto(`/applicationsets/${app.metadata.name}`)}> + {app.metadata.name} {app.metadata.name === props.appName && ' (current)'} +
  • + )) + } +
    + + )} + + ); +}; diff --git a/ui/src/app/applicationsets/components/application-details/application-details.scss b/ui/src/app/applicationsets/components/application-details/application-details.scss new file mode 100644 index 0000000000000..6eac2b2f57097 --- /dev/null +++ b/ui/src/app/applicationsets/components/application-details/application-details.scss @@ -0,0 +1,377 @@ +@import 'node_modules/argo-ui/src/styles/config'; +@import 'node_modules/foundation-sites/scss/util/util'; +@import '../../../shared/config.scss'; + +$header: 120px; + +.application-details { + height: 100vh; + width: 100%; + &__status-panel { + position: fixed; + left: $sidebar-width; + right: 0; + z-index: 3; + @media screen and (max-width: map-get($breakpoints, xlarge)) { + top: 150px; + } + @media screen and (max-width: map-get($breakpoints, large)) { + top: 146px; + } + } + + .argo-dropdown__content.is-menu { + max-height: 500px; + } + + &__tree { + padding: 1em; + overflow-x: auto; + overflow-y: auto; + margin-top: 150px; + height: calc(100vh - 2 * 50px - 115px); + @media screen and (max-width: map-get($breakpoints, xlarge)) { + margin-top: 165px; + } + } + + &__sliding-panel-pagination-wrap { + margin-top: 1.25em; + } + + &__warning { + font-size: 0.8em; + color: darken($argo-status-warning-color, 20%); + min-height: 1.2rem; + } + + &__refreshing-label { + color: $white-color; + position: fixed; + margin-top: -20px; + left: 50%; + background-color: $argo-color-gray-7; + border: 1px solid $argo-color-gray-5; + border-radius: 5px; + padding: 5px 5px; + font-size: 0.6em; + z-index: 1; + } + + &__tab-content-full-height { + height: calc(100vh - 2 * 76px); + div.row, + div.columns { + height: 100%; + } + } + + &__container { + position: relative; + text-transform: uppercase; + margin-top: 0.5em; + cursor: pointer; + font-size: 0.8em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding-left: 10px; + + i { + position: absolute; + left: 0px; + top: 2px; + } + + span { + color: $argo-color-teal-5; + } + } + + &__resource-icon { + text-align: center; + position: absolute; + left: 0; + top: 10px; + width: 60px; + line-height: 1; + color: $argo-color-gray-7; + font-size: 0.8em; + } + + .application-resource-tree { + margin: 0 auto; + } + + &__view-type { + margin-bottom: -6px; + display: inline-block; + vertical-align: middle; + white-space: nowrap; + i { + cursor: pointer; + color: $argo-color-gray-5; + margin: 0 0.5em; + &::before { + font-size: 1.5em; + } + } + i.selected { + cursor: default; + color: $argo-color-gray-7; + } + } + + &__node-menu { + position: absolute; + right: 0; + top: 0; + } + + &__external_link { + margin-left: 5px; + } + + pre { + font-family: monospace; + line-height: normal; + white-space: pre; + } + + &__action-menu { + text-transform: capitalize; + + &.disabled { + cursor: default !important; + color: $argo-color-gray-3 !important; + } + } + + .argo-table-list__row { + .columns.small-1.xxxlarge-1 { + width: 60px; + text-align: center; + } + } + + @media screen and (max-width: map-get($breakpoints, xlarge)) { + .page__content-wrapper { + min-height: calc(100vh - 3 * 50px); + } + .top-bar.row { + display: block; + .top-bar__left-side, + .top-bar__right-side { + width: 100%; + max-width: 100%; + flex: auto; + } + .top-bar__left-side { + .argo-button { + i { + @media screen and (max-width: map-get($breakpoints, large)) { + margin: 0 auto !important; + } + } + } + } + } + } + + @media screen and (max-width: map-get($breakpoints, large)) { + .top-bar.row { + .top-bar__left-side { + > div { + display: flex; + justify-content: center; + height: 50px; + align-items: center; + .argo-button { + height: 34px; + } + } + } + .top-bar__right-side { + display: flex; + justify-content: center; + } + } + } + + &__commit-message { + line-height: 1.5em; + } + + .filters-group__panel { + top: 230px; + } + + .graph-options-panel { + margin-left: 10px; + z-index: 1; + padding: 5px; + display: inline-block; + background-color: $argo-color-gray-1; + box-shadow: 1px 1px 3px $argo-color-gray-5; + position: absolute; + a { + padding: 5px; + margin: 2px; + color: $argo-color-gray-6; + border: 1px solid transparent; + border-radius: 5px; + + &.group-nodes-button { + cursor: pointer; + position: relative; + display: inline-block; + vertical-align: middle; + font-weight: 500; + line-height: 1.4; + text-align: center; + user-select: none; + transition: background-color 0.2s, border 0.2s, color 0.2s; + text-transform: uppercase; + &:hover { + background-color: #d1d5d9; + } + &:active { + transition: background-color 0.2s, border 0.2s, color 0.2s; + border: 1px $argo-color-teal-5 solid; + } + } + + &.group-nodes-button-on { + color: $argo-color-gray-1; + background-color: $argo-color-gray-6; + border: 3px solid $argo-color-teal-4; + font-size: 14px; + outline-style: solid; + &:hover { + background-color: $argo-color-gray-5; + } + + } + } + + .separator { + border-right: 1px solid $argo-color-gray-4; + padding-top: 6px; + padding-bottom: 6px; + } + + .zoom-value { + user-select: none; + margin-top: 5px; + margin-right: 6px; + margin-left: 4px; + font-size: 14px; + text-align-last: right; + float: right; + width: 40px; + border: 1px $argo-color-gray-5 solid; + background-color: $argo-color-gray-3; + padding: 2px; + color: $argo-color-gray-7; + } + } +} + + +@media screen and (max-width: map-get($breakpoints, large)) { +.sliding-panel__body { + padding: 4px !important; +} +.sliding-panel--is-middle .sliding-panel__wrapper { + width: 90% !important; +} +.sliding-panel--is-middle .sliding-panel__body { + padding: 18px !important; +} +.sliding-panel__close { + z-index: 2 !important; +} +.top-bar__title { + display: none; +} + +.top-bar__left-side { + white-space: normal !important; +} +.top-bar__left-side > div { + display: block !important; +} +.top-bar__right-side { + justify-content: right !important; +} +.application-status-panel.row { + flex-flow: unset; +} +.application-status-panel__item label { + margin-right: 0; +} +.application-status-panel__item { + padding: 5px 10px; +} + +.white-box, .tabs__content { + padding: 4px !important; +} +.white-box__details-row .columns.small-3 { + overflow-wrap: unset !important; + overflow: scroll; +} +.white-box__details-row .columns.small-9{ + padding-left: 4px; +} + +.resource-details__header h1 { + font-size: 16px; +} +.resource-details__header { + margin-top: 30px; + padding-right: 4px; +} + +.tabs__nav a:first-child, .tabs__nav a { + margin-left: 0 !important; +} + +.editable-panel__buttons { + top: unset; +} +} + +@media screen and (max-width: map-get($breakpoints, medium)) { +.sb-page-wrapper .top-bar.row { + display: none !important; +} +} + +.resource-parent-node-info-title { + flex-direction: column; + color: $argo-color-gray-6; + + &__label { + display: flex; + margin-bottom: 0.25em; + flex-shrink: 1; + div:first-child { + margin-right: 10px; + width: 60px; + text-align: right; + } + div:last-child { + font-weight: 500; + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-align: left; + + } + } +} + + + diff --git a/ui/src/app/applicationsets/components/application-details/application-details.tsx b/ui/src/app/applicationsets/components/application-details/application-details.tsx new file mode 100644 index 0000000000000..14ed4793254c6 --- /dev/null +++ b/ui/src/app/applicationsets/components/application-details/application-details.tsx @@ -0,0 +1,992 @@ +import {DropDownMenu, NotificationType, SlidingPanel} from 'argo-ui'; +import * as classNames from 'classnames'; +import * as PropTypes from 'prop-types'; +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import * as models from '../../../shared/models'; +import {RouteComponentProps} from 'react-router'; +import {BehaviorSubject, combineLatest, from, merge, Observable} from 'rxjs'; +import {delay, filter, map, mergeMap, repeat, retryWhen} from 'rxjs/operators'; + +import {DataLoader, EmptyState, ErrorNotification, ObservableQuery, Page, Paginate, Revision, Timestamp} from '../../../shared/components'; +import {AppContext, ContextApis} from '../../../shared/context'; +import * as appModels from '../../../shared/models'; +import {AppDetailsPreferences, AppsDetailsViewKey, AppsDetailsViewType, services} from '../../../shared/services'; + +import {ApplicationConditions} from '../application-conditions/application-conditions'; +import {ApplicationDeploymentHistory} from '../application-deployment-history/application-deployment-history'; +import {ApplicationOperationState} from '../application-operation-state/application-operation-state'; +import {PodGroupType, PodView} from '../application-pod-view/pod-view'; +import {ApplicationResourceTree, ResourceTreeNode} from '../application-resource-tree/application-resource-tree'; +import {ApplicationStatusPanel} from '../application-status-panel/application-status-panel'; +import {ApplicationSyncPanel} from '../application-sync-panel/application-sync-panel'; +import {ResourceDetails} from '../resource-details/resource-details'; +import * as AppUtils from '../utils'; +import {ApplicationResourceList} from './application-resource-list'; +import {Filters, FiltersProps} from './application-resource-filter'; +import {getAppDefaultSource, urlPattern, helpTip} from '../utils'; +import {ChartDetails, ResourceStatus} from '../../../shared/models'; +import {ApplicationsDetailsAppDropdown} from './application-details-app-dropdown'; +import {useSidebarTarget} from '../../../sidebar/sidebar'; + +import './application-details.scss'; +import {AppViewExtension} from '../../../shared/services/extensions-service'; + +interface ApplicationDetailsState { + page: number; + revision?: string; + groupedResources?: ResourceStatus[]; + slidingPanelPage?: number; + filteredGraph?: any[]; + truncateNameOnRight?: boolean; + collapsedNodes?: string[]; + extensions?: AppViewExtension[]; + extensionsMap?: {[key: string]: AppViewExtension}; +} + +interface FilterInput { + name: string[]; + kind: string[]; + health: string[]; + sync: string[]; + namespace: string[]; +} + +const ApplicationDetailsFilters = (props: FiltersProps) => { + const sidebarTarget = useSidebarTarget(); + return ReactDOM.createPortal(, sidebarTarget?.current); +}; + +export const NodeInfo = (node?: string): {key: string; container: number} => { + const nodeContainer = {key: '', container: 0}; + if (node) { + const parts = node.split('/'); + nodeContainer.key = parts.slice(0, 4).join('/'); + nodeContainer.container = parseInt(parts[4] || '0', 10); + } + return nodeContainer; +}; + +export const SelectNode = (fullName: string, containerIndex = 0, tab: string = null, appContext: ContextApis) => { + const node = fullName ? `${fullName}/${containerIndex}` : null; + appContext.navigation.goto('.', {node, tab}, {replace: true}); +}; + +export class ApplicationDetails extends React.Component, ApplicationDetailsState> { + public static contextTypes = { + apis: PropTypes.object + }; + + private appChanged = new BehaviorSubject(null); + private appNamespace: string; + + constructor(props: RouteComponentProps<{appnamespace: string; name: string}>) { + super(props); + const extensions = services.extensions.getAppViewExtensions(); + const extensionsMap: {[key: string]: AppViewExtension} = {}; + extensions.forEach(ext => { + extensionsMap[ext.title] = ext; + }); + this.state = { + page: 0, + groupedResources: [], + slidingPanelPage: 0, + filteredGraph: [], + truncateNameOnRight: false, + collapsedNodes: [], + extensions, + extensionsMap + }; + if (typeof this.props.match.params.appnamespace === 'undefined') { + this.appNamespace = ''; + } else { + this.appNamespace = this.props.match.params.appnamespace; + } + } + + private get showOperationState() { + return new URLSearchParams(this.props.history.location.search).get('operation') === 'true'; + } + + private setNodeExpansion(node: string, isExpanded: boolean) { + const index = this.state.collapsedNodes.indexOf(node); + if (isExpanded && index >= 0) { + this.state.collapsedNodes.splice(index, 1); + const updatedNodes = this.state.collapsedNodes.slice(); + this.setState({collapsedNodes: updatedNodes}); + } else if (!isExpanded && index < 0) { + const updatedNodes = this.state.collapsedNodes.slice(); + updatedNodes.push(node); + this.setState({collapsedNodes: updatedNodes}); + } + } + + private getNodeExpansion(node: string): boolean { + return this.state.collapsedNodes.indexOf(node) < 0; + } + + private get showConditions() { + return new URLSearchParams(this.props.history.location.search).get('conditions') === 'true'; + } + + private get selectedRollbackDeploymentIndex() { + return parseInt(new URLSearchParams(this.props.history.location.search).get('rollback'), 10); + } + + private get selectedNodeInfo() { + return NodeInfo(new URLSearchParams(this.props.history.location.search).get('node')); + } + + private get selectedNodeKey() { + const nodeContainer = this.selectedNodeInfo; + return nodeContainer.key; + } + + private closeGroupedNodesPanel() { + this.setState({groupedResources: []}); + this.setState({slidingPanelPage: 0}); + } + + private toggleCompactView(pref: AppDetailsPreferences) { + services.viewPreferences.updatePreferences({appDetails: {...pref, groupNodes: !pref.groupNodes}}); + } + + private getPageTitle(view: string) { + const {Tree, Pods, Network, List} = AppsDetailsViewKey; + switch (view) { + case Tree: + return 'Application Details Tree'; + case Network: + return 'Application Details Network'; + case Pods: + return 'Application Details Pods'; + case List: + return 'Application Details List'; + } + return ''; + } + + public render() { + return ( + + {q => ( + {error}} + loadingRenderer={() => Loading...} + input={this.props.match.params.name} + load={name => + combineLatest([this.loadAppInfo(name, this.appNamespace), services.viewPreferences.getPreferences(), q]).pipe( + map(items => { + const application = items[0].application; + 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; + } else { + const appDefaultView = (application.metadata && + application.metadata.annotations && + application.metadata.annotations[appModels.AnnotationDefaultView]) as AppsDetailsViewType; + if (appDefaultView != null) { + pref.view = appDefaultView; + } + } + if (params.get('orphaned') != null) { + pref.orphanedResources = params.get('orphaned') === 'true'; + } + if (params.get('podSortMode') != null) { + pref.podView.sortMode = params.get('podSortMode') as PodGroupType; + } else { + const appDefaultPodSort = (application.metadata && + application.metadata.annotations && + application.metadata.annotations[appModels.AnnotationDefaultPodSort]) as PodGroupType; + if (appDefaultPodSort != null) { + pref.podView.sortMode = appDefaultPodSort; + } + } + return {...items[0], pref}; + }) + ) + }> + {({application, tree, pref}: {application: appModels.Application; tree: appModels.ApplicationTree; pref: AppDetailsPreferences}) => { + tree.nodes = tree.nodes || []; + const treeFilter = this.getTreeFilter(pref.resourceFilter); + const setFilter = (items: string[]) => { + this.appContext.apis.navigation.goto('.', {resource: items.join(',')}, {replace: true}); + services.viewPreferences.updatePreferences({appDetails: {...pref, resourceFilter: items}}); + }; + const clearFilter = () => setFilter([]); + const refreshing = application.metadata.annotations && application.metadata.annotations[appModels.AnnotationRefreshKey]; + 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 source = getAppDefaultSource(application); + const resourceNodes = (): any[] => { + const statusByKey = new Map(); + application.status.resources.forEach(res => statusByKey.set(AppUtils.nodeKey(res), res)); + const resources = new Map(); + tree.nodes + .map(node => ({...node, orphaned: false})) + .concat(((pref.orphanedResources && tree.orphanedNodes) || []).map(node => ({...node, orphaned: true}))) + .forEach(node => { + const resource: any = {...node}; + resource.uid = node.uid; + const status = statusByKey.get(AppUtils.nodeKey(node)); + if (status) { + resource.health = status.health; + resource.status = status.status; + resource.hook = status.hook; + resource.syncWave = status.syncWave; + resource.requiresPruning = status.requiresPruning; + } + resources.set(node.uid || AppUtils.nodeKey(node), resource); + }); + const resourcesRef = Array.from(resources.values()); + return resourcesRef; + }; + + const filteredRes = resourceNodes().filter(res => { + const resNode: ResourceTreeNode = {...res, root: null, info: null, parentRefs: [], resourceVersion: '', uid: ''}; + resNode.root = resNode; + return this.filterTreeNode(resNode, treeFilter); + }); + const openGroupNodeDetails = (groupdedNodeIds: string[]) => { + const resources = resourceNodes(); + this.setState({ + groupedResources: groupdedNodeIds + ? resources.filter(res => groupdedNodeIds.includes(res.uid) || groupdedNodeIds.includes(AppUtils.nodeKey(res))) + : [] + }); + }; + + const renderCommitMessage = (message: string) => + message.split(/\s/).map(part => + urlPattern.test(part) ? ( + + {part}{' '} + + ) : ( + part + ' ' + ) + ); + const {Tree, Pods, Network, List} = AppsDetailsViewKey; + const zoomNum = (pref.zoom * 100).toFixed(0); + const setZoom = (s: number) => { + let targetZoom: number = pref.zoom + s; + if (targetZoom <= 0.05) { + targetZoom = 0.1; + } else if (targetZoom > 2.0) { + targetZoom = 2.0; + } + services.viewPreferences.updatePreferences({appDetails: {...pref, zoom: targetZoom}}); + }; + const setFilterGraph = (filterGraph: any[]) => { + this.setState({filteredGraph: filterGraph}); + }; + const setShowCompactNodes = (showCompactView: boolean) => { + services.viewPreferences.updatePreferences({appDetails: {...pref, groupNodes: showCompactView}}); + }; + const toggleNameDirection = () => { + this.setState({truncateNameOnRight: !this.state.truncateNameOnRight}); + }; + const expandAll = () => { + this.setState({collapsedNodes: []}); + }; + const collapseAll = () => { + const nodes = new Array(); + tree.nodes + .map(node => ({...node, orphaned: false})) + .concat((tree.orphanedNodes || []).map(node => ({...node, orphaned: true}))) + .forEach(node => { + const resourceNode: ResourceTreeNode = {...node}; + nodes.push(resourceNode); + }); + const collapsedNodesList = this.state.collapsedNodes.slice(); + if (pref.view === 'network') { + const networkNodes = nodes.filter(node => node.networkingInfo); + networkNodes.forEach(parent => { + const parentId = parent.uid; + if (collapsedNodesList.indexOf(parentId) < 0) { + collapsedNodesList.push(parentId); + } + }); + this.setState({collapsedNodes: collapsedNodesList}); + } else { + const managedKeys = new Set(application.status.resources.map(AppUtils.nodeKey)); + nodes.forEach(node => { + if (!((node.parentRefs || []).length === 0 || managedKeys.has(AppUtils.nodeKey(node)))) { + node.parentRefs.forEach(parent => { + const parentId = parent.uid; + if (collapsedNodesList.indexOf(parentId) < 0) { + collapsedNodesList.push(parentId); + } + }); + } + }); + collapsedNodesList.push(application.kind + '-' + application.metadata.namespace + '-' + application.metadata.name); + this.setState({collapsedNodes: collapsedNodesList}); + } + }; + const appFullName = AppUtils.nodeKey({ + group: 'argoproj.io', + kind: application.kind, + name: application.metadata.name, + namespace: application.metadata.namespace + }); + return ( +
    + } + ], + actionMenu: {items: this.getApplicationActionMenu(application, true)}, + tools: ( + +
    + { + this.appContext.apis.navigation.goto('.', {view: Tree}); + services.viewPreferences.updatePreferences({appDetails: {...pref, view: Tree}}); + }} + /> + { + this.appContext.apis.navigation.goto('.', {view: Pods}); + services.viewPreferences.updatePreferences({appDetails: {...pref, view: Pods}}); + }} + /> + { + 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.state.extensions && + (this.state.extensions || []).map(ext => ( + { + this.appContext.apis.navigation.goto('.', {view: ext.title}); + services.viewPreferences.updatePreferences({appDetails: {...pref, view: ext.title}}); + }} + /> + ))} +
    +
    + ) + }}> +
    + this.selectNode(appFullName, 0, 'diff')} + showOperation={() => this.setOperationStatusVisible(true)} + showConditions={() => this.setConditionsStatusVisible(true)} + showMetadataInfo={revision => this.setState({...this.state, revision})} + /> +
    +
    + {refreshing &&

    Refreshing

    } + {((pref.view === 'tree' || pref.view === 'network') && ( + <> + services.viewPreferences.getPreferences()}> + {viewPref => ( + + )} + + + this.filterTreeNode(node, treeFilter)} + selectedNodeFullName={this.selectedNodeKey} + onNodeClick={fullName => this.selectNode(fullName)} + nodeMenu={node => + AppUtils.renderResourceMenu(node, application, tree, this.appContext.apis, this.appChanged, () => + this.getApplicationActionMenu(application, false) + ) + } + showCompactNodes={pref.groupNodes} + tree={tree} + app={application} + showOrphanedResources={pref.orphanedResources} + useNetworkingHierarchy={pref.view === 'network'} + onClearFilter={clearFilter} + onGroupdNodeClick={groupdedNodeIds => openGroupNodeDetails(groupdedNodeIds)} + zoom={pref.zoom} + podGroupCount={pref.podGroupCount} + appContext={this.appContext} + nameDirection={this.state.truncateNameOnRight} + filters={pref.resourceFilter} + setTreeFilterGraph={setFilterGraph} + setShowCompactNodes={setShowCompactNodes} + setNodeExpansion={(node, isExpanded) => this.setNodeExpansion(node, isExpanded)} + getNodeExpansion={node => this.getNodeExpansion(node)} + /> + + )) || + (pref.view === 'pods' && ( + this.selectNode(fullName)} + nodeMenu={node => + AppUtils.renderResourceMenu(node, application, tree, this.appContext.apis, this.appChanged, () => + this.getApplicationActionMenu(application, false) + ) + } + quickStarts={node => AppUtils.renderResourceButtons(node, application, tree, this.appContext.apis, this.appChanged)} + /> + )) || + (this.state.extensionsMap[pref.view] != null && ( + + )) || ( +
    + services.viewPreferences.getPreferences()}> + {viewPref => ( + + )} + + {(filteredRes.length > 0 && ( + this.setState({page})} + preferencesKey='application-details'> + {data => ( + this.selectNode(fullName)} + resources={data} + nodeMenu={node => + AppUtils.renderResourceMenu( + {...node, root: node}, + application, + tree, + this.appContext.apis, + this.appChanged, + () => this.getApplicationActionMenu(application, false) + ) + } + tree={tree} + /> + )} + + )) || ( + +

    No resources found

    +
    Try to change filter criteria
    +
    + )} +
    + )} +
    + 0} onClose={() => this.closeGroupedNodesPanel()}> +
    + this.setState({slidingPanelPage: page})} + preferencesKey='grouped-nodes-details'> + {data => ( + this.selectNode(fullName)} + resources={data} + nodeMenu={node => + AppUtils.renderResourceMenu({...node, root: node}, application, tree, this.appContext.apis, this.appChanged, () => + this.getApplicationActionMenu(application, false) + ) + } + tree={tree} + /> + )} + +
    +
    + this.selectNode('')}> + this.updateApp(app, query)} + selectedNode={selectedNode} + tab={tab} + /> + + AppUtils.showDeploy(null, null, this.appContext.apis)} + selectedResource={syncResourceKey} + /> + -1} onClose={() => this.setRollbackPanelVisible(-1)}> + {this.selectedRollbackDeploymentIndex > -1 && ( + this.rollbackApplication(info, application)} + selectDeployment={i => this.setRollbackPanelVisible(i)} + /> + )} + + this.setOperationStatusVisible(false)}> + {operationState && } + + this.setConditionsStatusVisible(false)}> + {conditions && } + + this.setState({revision: null})}> + {this.state.revision && + (source.chart ? ( + + services.applications.revisionChartDetails(input.metadata.name, input.metadata.namespace, this.state.revision) + }> + {(m: ChartDetails) => ( +
    +
    +
    +
    Revision:
    +
    {this.state.revision}
    +
    +
    +
    Helm Chart:
    +
    + {source.chart}  + {m.home && ( + { + e.stopPropagation(); + window.open(m.home); + }}> + + + )} +
    +
    + {m.description && ( +
    +
    Description:
    +
    {m.description}
    +
    + )} + {m.maintainers.length > 0 && ( +
    +
    Maintainers:
    +
    {m.maintainers.join(', ')}
    +
    + )} +
    +
    + )} +
    + ) : ( + + services.applications.revisionMetadata(application.metadata.name, application.metadata.namespace, this.state.revision) + }> + {metadata => ( +
    +
    +
    +
    SHA:
    +
    + +
    +
    +
    +
    +
    +
    Date:
    +
    + +
    +
    +
    +
    +
    +
    Tags:
    +
    + {((metadata.tags || []).length > 0 && metadata.tags.join(', ')) || 'No tags'} +
    +
    +
    +
    +
    +
    Author:
    +
    {metadata.author}
    +
    +
    +
    +
    +
    Message:
    +
    +
    {renderCommitMessage(metadata.message)}
    +
    +
    +
    +
    + )} +
    + ))} +
    +
    +
    + ); + }} +
    + )} +
    + ); + } + + private getApplicationActionMenu(app: appModels.Application, needOverlapLabelOnNarrowScreen: boolean) { + const refreshing = app.metadata.annotations && app.metadata.annotations[appModels.AnnotationRefreshKey]; + const fullName = AppUtils.nodeKey({group: 'argoproj.io', kind: app.kind, name: app.metadata.name, namespace: app.metadata.namespace}); + const ActionMenuItem = (prop: {actionLabel: string}) => {prop.actionLabel}; + const hasMultipleSources = app.spec.sources && app.spec.sources.length > 0; + return [ + { + iconClassName: 'fa fa-info-circle', + title: , + action: () => this.selectNode(fullName) + }, + { + iconClassName: 'fa fa-file-medical', + title: , + action: () => this.selectNode(fullName, 0, 'diff'), + disabled: app.status.sync.status === appModels.SyncStatuses.Synced + }, + { + iconClassName: 'fa fa-sync', + title: , + action: () => AppUtils.showDeploy('all', null, this.appContext.apis) + }, + { + iconClassName: 'fa fa-info-circle', + title: , + action: () => this.setOperationStatusVisible(true), + disabled: !app.status.operationState + }, + { + iconClassName: 'fa fa-history', + title: hasMultipleSources ? ( + + + {helpTip('Rollback is not supported for apps with multiple sources')} + + ) : ( + + ), + action: () => { + this.setRollbackPanelVisible(0); + }, + disabled: !app.status.operationState || hasMultipleSources + }, + { + iconClassName: 'fa fa-times-circle', + title: , + action: () => this.deleteApplication() + }, + { + iconClassName: classNames('fa fa-redo', {'status-icon--spin': !!refreshing}), + title: ( + + {' '} + !refreshing && services.applications.get(app.metadata.name, app.metadata.namespace, 'hard') + } + ]} + anchor={() => } + /> + + ), + disabled: !!refreshing, + action: () => { + if (!refreshing) { + services.applications.get(app.metadata.name, app.metadata.namespace, 'normal'); + AppUtils.setAppRefreshing(app); + this.appChanged.next(app); + } + } + } + ]; + } + + private filterTreeNode(node: ResourceTreeNode, filterInput: FilterInput): boolean { + const syncStatuses = filterInput.sync.map(item => (item === 'OutOfSync' ? ['OutOfSync', 'Unknown'] : [item])).reduce((first, second) => first.concat(second), []); + + const root = node.root || ({} as ResourceTreeNode); + const hook = root && root.hook; + if ( + (filterInput.name.length === 0 || this.nodeNameMatchesWildcardFilters(node.name, filterInput.name)) && + (filterInput.kind.length === 0 || filterInput.kind.indexOf(node.kind) > -1) && + // include if node's root sync matches filter + (syncStatuses.length === 0 || hook || (root.status && syncStatuses.indexOf(root.status) > -1)) && + // include if node or node's root health matches filter + (filterInput.health.length === 0 || + hook || + (root.health && filterInput.health.indexOf(root.health.status) > -1) || + (node.health && filterInput.health.indexOf(node.health.status) > -1)) && + (filterInput.namespace.length === 0 || filterInput.namespace.includes(node.namespace)) + ) { + return true; + } + + return false; + } + + private nodeNameMatchesWildcardFilters(nodeName: string, filterInputNames: string[]): boolean { + const regularExpression = new RegExp( + filterInputNames + // Escape any regex input to ensure only * can be used + .map(pattern => '^' + this.escapeRegex(pattern) + '$') + // Replace any escaped * with proper regex + .map(pattern => pattern.replace(/\\\*/g, '.*')) + // Join all filterInputs to a single regular expression + .join('|'), + 'gi' + ); + return regularExpression.test(nodeName); + } + + private escapeRegex(input: string): string { + return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + } + + private loadAppInfo(name: string, appNamespace: string): Observable<{application: appModels.Application; tree: appModels.ApplicationTree}> { + return from(services.applications.get(name, appNamespace)) + .pipe( + mergeMap(app => { + const fallbackTree = { + nodes: app.status.resources.map(res => ({...res, parentRefs: [], info: [], resourceVersion: '', uid: ''})), + orphanedNodes: [], + hosts: [] + } as appModels.ApplicationTree; + return combineLatest( + merge( + from([app]), + this.appChanged.pipe(filter(item => !!item)), + AppUtils.handlePageVisibility(() => + services.applications + .watch({name, appNamespace}) + .pipe( + map(watchEvent => { + if (watchEvent.type === 'DELETED') { + this.onAppDeleted(); + } + return watchEvent.application; + }) + ) + .pipe(repeat()) + .pipe(retryWhen(errors => errors.pipe(delay(500)))) + ) + ), + merge( + from([fallbackTree]), + services.applications.resourceTree(name, appNamespace).catch(() => fallbackTree), + AppUtils.handlePageVisibility(() => + services.applications + .watchResourceTree(name, appNamespace) + .pipe(repeat()) + .pipe(retryWhen(errors => errors.pipe(delay(500)))) + ) + ) + ); + }) + ) + .pipe(filter(([application, tree]) => !!application && !!tree)) + .pipe(map(([application, tree]) => ({application, tree}))); + } + + private onAppDeleted() { + this.appContext.apis.notifications.show({type: NotificationType.Success, content: `Application '${this.props.match.params.name}' was deleted`}); + this.appContext.apis.navigation.goto('/applications'); + } + + private async updateApp(app: appModels.Application, query: {validate?: boolean}) { + const latestApp = await services.applications.get(app.metadata.name, app.metadata.namespace); + latestApp.metadata.labels = app.metadata.labels; + latestApp.metadata.annotations = app.metadata.annotations; + latestApp.spec = app.spec; + const updatedApp = await services.applications.update(latestApp, query); + this.appChanged.next(updatedApp); + } + + private groupAppNodesByKey(application: appModels.Application, tree: appModels.ApplicationTree) { + const nodeByKey = new Map(); + tree.nodes.concat(tree.orphanedNodes || []).forEach(node => nodeByKey.set(AppUtils.nodeKey(node), node)); + nodeByKey.set(AppUtils.nodeKey({group: 'argoproj.io', kind: application.kind, name: application.metadata.name, namespace: application.metadata.namespace}), application); + return nodeByKey; + } + + private getTreeFilter(filterInput: string[]): FilterInput { + const name = new Array(); + const kind = new Array(); + const health = new Array(); + const sync = new Array(); + const namespace = new Array(); + for (const item of filterInput || []) { + const [type, val] = item.split(':'); + switch (type) { + case 'name': + name.push(val); + break; + case 'kind': + kind.push(val); + break; + case 'health': + health.push(val); + break; + case 'sync': + sync.push(val); + break; + case 'namespace': + namespace.push(val); + break; + } + } + return {kind, health, sync, namespace, name}; + } + + private setOperationStatusVisible(isVisible: boolean) { + this.appContext.apis.navigation.goto('.', {operation: isVisible}, {replace: true}); + } + + private setConditionsStatusVisible(isVisible: boolean) { + this.appContext.apis.navigation.goto('.', {conditions: isVisible}, {replace: true}); + } + + private setRollbackPanelVisible(selectedDeploymentIndex = 0) { + this.appContext.apis.navigation.goto('.', {rollback: selectedDeploymentIndex}, {replace: true}); + } + + private selectNode(fullName: string, containerIndex = 0, tab: string = null) { + SelectNode(fullName, containerIndex, tab, this.appContext.apis); + } + + private async rollbackApplication(revisionHistory: appModels.RevisionHistory, application: appModels.Application) { + try { + const needDisableRollback = application.spec.syncPolicy && application.spec.syncPolicy.automated; + let confirmationMessage = `Are you sure you want to rollback application '${this.props.match.params.name}'?`; + if (needDisableRollback) { + confirmationMessage = `Auto-Sync needs to be disabled in order for rollback to occur. +Are you sure you want to disable auto-sync and rollback application '${this.props.match.params.name}'?`; + } + + const confirmed = await this.appContext.apis.popup.confirm('Rollback application', confirmationMessage); + if (confirmed) { + if (needDisableRollback) { + const update = JSON.parse(JSON.stringify(application)) as appModels.Application; + update.spec.syncPolicy = {automated: null}; + await services.applications.update(update); + } + await services.applications.rollback(this.props.match.params.name, this.appNamespace, revisionHistory.id); + this.appChanged.next(await services.applications.get(this.props.match.params.name, this.appNamespace)); + this.setRollbackPanelVisible(-1); + } + } catch (e) { + this.appContext.apis.notifications.show({ + content: , + type: NotificationType.Error + }); + } + } + + private get appContext(): AppContext { + return this.context as AppContext; + } + + private async deleteApplication() { + await AppUtils.deleteApplication(this.props.match.params.name, this.appNamespace, this.appContext.apis); + } +} + +const ExtensionView = (props: {extension: AppViewExtension; application: models.Application; tree: models.ApplicationTree}) => { + const {extension, application, tree} = props; + return ; +}; diff --git a/ui/src/app/applicationsets/components/application-details/application-resource-filter.tsx b/ui/src/app/applicationsets/components/application-details/application-resource-filter.tsx new file mode 100644 index 0000000000000..a3d99f92488f3 --- /dev/null +++ b/ui/src/app/applicationsets/components/application-details/application-resource-filter.tsx @@ -0,0 +1,173 @@ +import * as React from 'react'; +import {Checkbox} from 'argo-ui/v2'; +import {ApplicationTree, HealthStatusCode, HealthStatuses, SyncStatusCode, SyncStatuses} from '../../../shared/models'; +import {AppDetailsPreferences, services} from '../../../shared/services'; +import {Context} from '../../../shared/context'; +import {Filter, FiltersGroup} from '../filter/filter'; +import {ComparisonStatusIcon, HealthStatusIcon} from '../utils'; +import {resources} from '../resources'; +import * as models from '../../../shared/models'; + +const uniq = (value: string, index: number, self: string[]) => self.indexOf(value) === index; + +function toOption(label: string) { + return {label}; +} + +export interface FiltersProps { + children?: React.ReactNode; + pref: AppDetailsPreferences; + tree: ApplicationTree; + resourceNodes: models.ResourceStatus[]; + onSetFilter: (items: string[]) => void; + onClearFilter: () => void; + collapsed?: boolean; +} + +export const Filters = (props: FiltersProps) => { + const ctx = React.useContext(Context); + + const {pref, tree, onSetFilter} = props; + + const onClearFilter = () => { + setLoading(true); + props.onClearFilter(); + }; + + const resourceFilter = pref.resourceFilter || []; + const removePrefix = (prefix: string) => (v: string) => v.replace(prefix + ':', ''); + + const [groupedFilters, setGroupedFilters] = React.useState<{[key: string]: string}>({}); + const [loading, setLoading] = React.useState(true); + + React.useEffect(() => { + const update: {[key: string]: string} = {}; + (resourceFilter || []).forEach(pair => { + const tmp = pair.split(':'); + if (tmp.length === 2) { + const prefix = tmp[0]; + const cur = update[prefix]; + update[prefix] = `${cur ? cur + ',' : ''}${pair}`; + } + }); + setGroupedFilters(update); + setLoading(false); + }, [resourceFilter, loading]); + + const setFilters = (prefix: string, values: string[]) => { + const groups = {...groupedFilters}; + groups[prefix] = values.map(v => `${prefix}:${v}`).join(','); + let strings: string[] = []; + Object.keys(groups).forEach(g => { + strings = strings.concat(groups[g].split(',').filter(f => f !== '')); + }); + onSetFilter(strings); + }; + + const ResourceFilter = (p: {label: string; prefix: string; options: {label: string}[]; abbreviations?: Map; field?: boolean; radio?: boolean}) => { + return loading ? ( +
    Loading...
    + ) : ( + setFilters(p.prefix, v)} + options={p.options} + abbreviations={p.abbreviations} + field={!!p.field} + radio={!!p.radio} + /> + ); + }; + + // we need to include ones that might have been filter in other apps that do not apply to the current app, + // otherwise the user will not be able to clear them from this panel + const alreadyFilteredOn = (prefix: string) => resourceFilter.filter(f => f.startsWith(prefix + ':')).map(removePrefix(prefix)); + + const kinds = tree.nodes + .map(x => x.kind) + .concat(alreadyFilteredOn('kind')) + .filter(uniq) + .sort(); + + const names = tree.nodes + .map(x => x.name) + .concat(alreadyFilteredOn('name')) + .filter(uniq) + .sort(); + + const namespaces = tree.nodes + .map(x => x.namespace) + .filter(x => !!x) + .concat(alreadyFilteredOn('namespace')) + .filter(uniq) + .sort(); + + const selectedFor = (prefix: string) => { + return groupedFilters[prefix] ? groupedFilters[prefix].split(',').map(removePrefix(prefix)) : []; + }; + + const getOptionCount = (label: string, filterType: string): number => { + switch (filterType) { + case 'Sync': + return props.resourceNodes.filter(res => res.status === SyncStatuses[label]).length; + case 'Health': + return props.resourceNodes.filter(res => res.health?.status === HealthStatuses[label]).length; + case 'Kind': + return props.resourceNodes.filter(res => res.kind === label).length; + default: + return 0; + } + }; + + return ( + + {ResourceFilter({label: 'NAME', prefix: 'name', options: names.map(toOption), field: true})} + {ResourceFilter({ + label: 'KINDS', + prefix: 'kind', + options: kinds.map(label => ({ + label, + count: getOptionCount(label, 'Kind') + })), + abbreviations: resources, + field: true + })} + {ResourceFilter({ + label: 'SYNC STATUS', + prefix: 'sync', + options: ['Synced', 'OutOfSync'].map(label => ({ + label, + count: getOptionCount(label, 'Sync'), + icon: + })) + })} + {ResourceFilter({ + label: 'HEALTH STATUS', + prefix: 'health', + options: ['Healthy', 'Progressing', 'Degraded', 'Suspended', 'Missing', 'Unknown'].map(label => ({ + label, + count: getOptionCount(label, 'Health'), + icon: + })) + })} + {namespaces.length > 1 && ResourceFilter({label: 'NAMESPACES', prefix: 'namespace', options: (namespaces || []).filter(l => l && l !== '').map(toOption), field: true})} + {(tree.orphanedNodes || []).length > 0 && ( +
    + { + ctx.navigation.goto('.', {orphaned: val}, {replace: true}); + services.viewPreferences.updatePreferences({appDetails: {...pref, orphanedResources: val}}); + }} + style={{ + marginRight: '8px', + marginLeft: '8px' + }} + /> +
    Show Orphaned
    +
    + )} +
    + ); +}; diff --git a/ui/src/app/applicationsets/components/application-details/application-resource-list.tsx b/ui/src/app/applicationsets/components/application-details/application-resource-list.tsx new file mode 100644 index 0000000000000..dba61c5135492 --- /dev/null +++ b/ui/src/app/applicationsets/components/application-details/application-resource-list.tsx @@ -0,0 +1,157 @@ +import {DropDown} from 'argo-ui'; +import * as React from 'react'; +import * as classNames from 'classnames'; +import * as models from '../../../shared/models'; +import {ResourceIcon} from '../resource-icon'; +import {ResourceLabel} from '../resource-label'; +import {ComparisonStatusIcon, HealthStatusIcon, nodeKey, createdOrNodeKey} from '../utils'; +import {Consumer} from '../../../shared/context'; +import * as _ from 'lodash'; +import Moment from 'react-moment'; +import {format} from 'date-fns'; +import {ResourceNode, ResourceRef} from '../../../shared/models'; + +export const ApplicationResourceList = ({ + resources, + onNodeClick, + nodeMenu, + tree +}: { + resources: models.ResourceStatus[]; + onNodeClick?: (fullName: string) => any; + nodeMenu?: (node: models.ResourceNode) => React.ReactNode; + tree?: models.ApplicationTree; +}) => { + function getResNode(nodes: ResourceNode[], nodeId: string): models.ResourceNode { + for (const node of nodes) { + if (nodeKey(node) === nodeId) { + return node; + } + } + return null; + } + const parentNode = ((resources || []).length > 0 && (getResNode(tree.nodes, nodeKey(resources[0])) as ResourceNode)?.parentRefs?.[0]) || ({} as ResourceRef); + + return ( +
    +
    + {Object.keys(parentNode).length > 0 && ( +
    +
    Parent Node Info
    +
    +
    Name:
    +
    {parentNode?.name}
    +
    +
    +
    Kind:
    +
    {parentNode?.kind}
    +
    +
    + )} +
    +
    +
    +
    +
    +
    NAME
    +
    GROUP/KIND
    +
    SYNC ORDER
    +
    NAMESPACE
    + {(parentNode.kind === 'Rollout' || parentNode.kind === 'Deployment') &&
    REVISION
    } +
    CREATED AT
    +
    STATUS
    +
    +
    + {resources + .sort((first, second) => -createdOrNodeKey(first).localeCompare(createdOrNodeKey(second))) + .map(res => ( +
    onNodeClick(nodeKey(res))}> +
    +
    +
    + +
    +
    {ResourceLabel({kind: res.kind})}
    +
    +
    +
    + {res.name} + {res.kind === 'Application' && ( + + {ctx => ( + + + + + + )} + + )} +
    +
    {[res.group, res.kind].filter(item => !!item).join('/')}
    +
    {res.syncWave || '-'}
    +
    {res.namespace}
    + {res.kind === 'ReplicaSet' && + ((getResNode(tree.nodes, nodeKey(res)) as ResourceNode).info || []) + .filter(tag => !tag.name.includes('Node')) + .slice(0, 4) + .map((tag, i) => { + return ( +
    + {tag?.value?.split(':')[1] || '-'} +
    + ); + })} + +
    + {res.createdAt && ( + + + {res.createdAt} + +  ago   {format(new Date(res.createdAt), 'MM/dd/yy')} + + )} +
    +
    + {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/applicationsets/components/application-fullscreen-logs/application-fullscreen-logs.scss b/ui/src/app/applicationsets/components/application-fullscreen-logs/application-fullscreen-logs.scss new file mode 100644 index 0000000000000..c735215f0cae8 --- /dev/null +++ b/ui/src/app/applicationsets/components/application-fullscreen-logs/application-fullscreen-logs.scss @@ -0,0 +1,13 @@ +@import 'node_modules/argo-ui/src/styles/config'; + +.application-fullscreen-logs { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 20; + height: 100%; + padding: 20px 30px; + background-color: $argo-color-gray-3; +} diff --git a/ui/src/app/applicationsets/components/application-fullscreen-logs/application-fullscreen-logs.tsx b/ui/src/app/applicationsets/components/application-fullscreen-logs/application-fullscreen-logs.tsx new file mode 100644 index 0000000000000..c7e669f46dded --- /dev/null +++ b/ui/src/app/applicationsets/components/application-fullscreen-logs/application-fullscreen-logs.tsx @@ -0,0 +1,37 @@ +import * as React from 'react'; + +import Helmet from 'react-helmet'; +import {RouteComponentProps} from 'react-router-dom'; +import {Query} from '../../../shared/components'; +import {PodsLogsViewer} from '../pod-logs-viewer/pod-logs-viewer'; +import './application-fullscreen-logs.scss'; + +export const ApplicationFullscreenLogs = (props: RouteComponentProps<{name: string; appnamespace: string; container: string; namespace: string}>) => { + return ( + + {q => { + const podName = q.get('podName'); + const name = q.get('name'); + const group = q.get('group'); + const kind = q.get('kind'); + const title = `${podName || `${group}/${kind}/${name}`}:${props.match.params.container}`; + return ( +
    + +

    {title}

    + +
    + ); + }} +
    + ); +}; diff --git a/ui/src/app/applicationsets/components/application-node-info/application-node-info.scss b/ui/src/app/applicationsets/components/application-node-info/application-node-info.scss new file mode 100644 index 0000000000000..1d1fbf941fac7 --- /dev/null +++ b/ui/src/app/applicationsets/components/application-node-info/application-node-info.scss @@ -0,0 +1,69 @@ +@import 'node_modules/argo-ui/src/styles/config'; + +.application-node-info { + &__manifest { + overflow-x: auto; + + .tabs__content { + background-color: white; + } + + &--raw { + font-family: 'Bitstream Vera Sans Mono', 'DejaVu Sans Mono', Monaco, Courier, monospace; + white-space: pre; + font-size: 12px; + color: gray; + padding: 0 0 0 12px; + line-height: 2em; + } + } + + &__labels { + line-height: 28px; + } + + &__label { + background-color: $argo-color-gray-5; + color: white; + border-radius: 5px; + padding: 4px; + display: inline-block; + margin-right: 2px; + line-height: 14px; + } + + &__checkboxes { + text-align: right; + label { + padding-right: 2em; + color: $argo-color-gray-8; + } + } + + &__container { + display: flex; + align-items: center; + flex-direction: row; + line-height: 1.8; + border-bottom: 1px solid rgba(222, 230, 235, 0.7); + + &--name { + width: 15%; + } + &--highlight { + font-style: italic; + } + + &--hint { + text-decoration: underline; + text-decoration-style: dashed; + cursor: pointer; + &:hover { + text-decoration: none; + } + } + &:last-child { + border-bottom: none; + } + } +} diff --git a/ui/src/app/applicationsets/components/application-node-info/application-node-info.tsx b/ui/src/app/applicationsets/components/application-node-info/application-node-info.tsx new file mode 100644 index 0000000000000..8ecb965a1ab8b --- /dev/null +++ b/ui/src/app/applicationsets/components/application-node-info/application-node-info.tsx @@ -0,0 +1,289 @@ +import {Checkbox, DataLoader, Tab, Tabs} from 'argo-ui'; +import classNames from 'classnames'; +import * as deepMerge from 'deepmerge'; +import * as React from 'react'; + +import {YamlEditor, ClipboardText} from '../../../shared/components'; +import {DeepLinks} from '../../../shared/components/deep-links'; +import * as models from '../../../shared/models'; +import {services} from '../../../shared/services'; +import {ResourceTreeNode} from '../application-resource-tree/application-resource-tree'; +import {ApplicationResourcesDiff} from '../application-resources-diff/application-resources-diff'; +import { + ComparisonStatusIcon, + formatCreationTimestamp, + getPodReadinessGatesState, + getPodReadinessGatesState as _getPodReadinessGatesState, + getPodStateReason, + HealthStatusIcon +} from '../utils'; +import './application-node-info.scss'; +import {ReadinessGatesFailedWarning} from './readiness-gates-failed-warning'; + +const RenderContainerState = (props: {container: any}) => { + const state = (props.container.state?.waiting && 'waiting') || (props.container.state?.terminated && 'terminated') || (props.container.state?.running && 'running'); + const status = props.container.state.waiting?.reason || props.container.state.terminated?.reason || props.container.state.running?.reason; + const lastState = props.container.lastState?.terminated; + const msg = props.container.state.waiting?.message || props.container.state.terminated?.message || props.container.state.running?.message; + + return ( +
    +
    {props.container.name}
    +
    + {state && ( + <> + Container is {state} + {status && ' because of '} + + )} + + {status && ( + + {status} + + )} + + {'.'} + {(props.container.state.terminated?.exitCode === 0 || props.container.state.terminated?.exitCode) && ( + <> + {' '} + It exited with exit code {props.container.state.terminated.exitCode}. + + )} + <> + {' '} + It is {props.container?.started ? 'started' : 'not started'} and + {props.container?.ready ? ' ready.' : ' not ready.'} + +
    + {lastState && ( + <> + <> + The container last terminated with exit code {lastState?.exitCode} + + {lastState?.reason && ' because of '} + + {lastState?.reason && ( + + {lastState?.reason} + + )} + + {'.'} + + )} +
    +
    + ); +}; + +export const ApplicationNodeInfo = (props: { + application: models.Application; + node: models.ResourceNode; + live: models.State; + links: models.LinksResponse; + controlled: {summary: models.ResourceStatus; state: models.ResourceDiff}; +}) => { + const attributes: {title: string; value: any}[] = [ + {title: 'KIND', value: props.node.kind}, + {title: 'NAME', value: }, + {title: 'NAMESPACE', value: } + ]; + if (props.node.createdAt) { + attributes.push({ + title: 'CREATED AT', + value: formatCreationTimestamp(props.node.createdAt) + }); + } + if ((props.node.images || []).length) { + attributes.push({ + title: 'IMAGES', + value: ( +
    + {(props.node.images || []).sort().map(image => ( + + {image} + + ))} +
    + ) + }); + } + if (props.live) { + if (props.node.kind === 'Pod') { + const {reason, message, netContainerStatuses} = getPodStateReason(props.live); + attributes.push({title: 'STATE', value: reason}); + if (message) { + attributes.push({title: 'STATE DETAILS', value: message}); + } + if (netContainerStatuses.length > 0) { + attributes.push({ + title: 'CONTAINER STATE', + value: ( +
    + {netContainerStatuses.map((container, i) => { + return ; + })} +
    + ) + }); + } + } else if (props.node.kind === 'Service') { + attributes.push({title: 'TYPE', value: props.live.spec.type}); + let hostNames = ''; + const status = props.live.status; + if (status && status.loadBalancer && status.loadBalancer.ingress) { + hostNames = (status.loadBalancer.ingress || []).map((item: any) => item.hostname || item.ip).join(', '); + } + attributes.push({title: 'HOSTNAMES', value: hostNames}); + } else if (props.node.kind === 'ReplicaSet') { + attributes.push({title: 'REPLICAS', value: `${props.live.spec?.replicas || 0}/${props.live.status?.readyReplicas || 0}/${props.live.status?.replicas || 0}`}); + } + } + + if (props.controlled) { + if (!props.controlled.summary.hook) { + 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); + if (props.controlled.summary.health.message) { + attributes.push({title: 'HEALTH DETAILS', value: props.controlled.summary.health.message}); + } + } + } else if (props.node && (props.node as ResourceTreeNode).health) { + const treeNode = props.node as ResourceTreeNode; + if (treeNode && treeNode.health) { + attributes.push({ + title: 'HEALTH', + value: ( + + {treeNode.health.message || treeNode.health.status} + + ) + } as any); + } + } + + if (props.links) { + attributes.push({ + title: 'LINKS', + value: + }); + } + + const tabs: Tab[] = [ + { + key: 'manifest', + title: 'Live Manifest', + content: ( + services.viewPreferences.getPreferences()}> + {pref => { + const live = deepMerge(props.live, {}) as any; + if (live?.metadata?.managedFields && pref.appDetails.hideManagedFields) { + delete live.metadata.managedFields; + } + return ( + <> +
    + + services.viewPreferences.updatePreferences({ + appDetails: { + ...pref.appDetails, + hideManagedFields: !pref.appDetails.hideManagedFields + } + }) + } + /> + +
    + + services.applications.patchResource(props.application.metadata.name, props.application.metadata.namespace, props.node, patch, patchType) + } + /> + + ); + }} +
    + ) + } + ]; + if (props.controlled && !props.controlled.summary.hook) { + tabs.push({ + key: 'diff', + icon: 'fa fa-file-medical', + title: 'Diff', + content: + }); + tabs.push({ + key: 'desiredManifest', + title: 'Desired Manifest', + content: + }); + } + + const readinessGatesState = React.useMemo(() => { + if (props.live && props.node?.kind === 'Pod') { + return getPodReadinessGatesState(props.live); + } + + return null; + }, [props.live, props.node]); + + return ( +
    + {Boolean(readinessGatesState) && } +
    +
    + {attributes.map(attr => ( +
    +
    {attr.title}
    +
    {attr.value}
    +
    + ))} +
    +
    + +
    + services.viewPreferences.getPreferences()}> + {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/applicationsets/components/application-node-info/readiness-gates-failed-warning.scss b/ui/src/app/applicationsets/components/application-node-info/readiness-gates-failed-warning.scss new file mode 100644 index 0000000000000..b7c9ad7f0bd50 --- /dev/null +++ b/ui/src/app/applicationsets/components/application-node-info/readiness-gates-failed-warning.scss @@ -0,0 +1,12 @@ +@import 'node_modules/argo-ui/src/styles/config'; + +.white-box { + &__readiness-gates-alert { + padding: 20px; + border-left: 6px solid $argo-status-failed-color !important; + + ul { + margin-bottom: 0; + } + } +} diff --git a/ui/src/app/applicationsets/components/application-node-info/readiness-gates-failed-warning.tsx b/ui/src/app/applicationsets/components/application-node-info/readiness-gates-failed-warning.tsx new file mode 100644 index 0000000000000..c38dc598d6634 --- /dev/null +++ b/ui/src/app/applicationsets/components/application-node-info/readiness-gates-failed-warning.tsx @@ -0,0 +1,44 @@ +import * as React from 'react'; +import {selectPostfix} from '../utils'; + +import './readiness-gates-failed-warning.scss'; + +export interface ReadinessGatesFailedWarningProps { + readinessGatesState: { + nonExistingConditions: string[]; + failedConditions: string[]; + }; +} + +export const ReadinessGatesFailedWarning = ({readinessGatesState}: ReadinessGatesFailedWarningProps) => { + if (readinessGatesState.failedConditions.length > 0 || readinessGatesState.nonExistingConditions.length > 0) { + return ( +
    +
    Readiness Gates Failing:
    +
      + {readinessGatesState.failedConditions.length > 0 && ( +
    • + The status of pod readiness gate{selectPostfix(readinessGatesState.failedConditions, '', 's')}{' '} + {readinessGatesState.failedConditions + .map(t => `"${t}"`) + .join(', ') + .trim()}{' '} + {selectPostfix(readinessGatesState.failedConditions, 'is', 'are')} False. +
    • + )} + {readinessGatesState.nonExistingConditions.length > 0 && ( +
    • + Corresponding condition{selectPostfix(readinessGatesState.nonExistingConditions, '', 's')} of pod readiness gate{' '} + {readinessGatesState.nonExistingConditions + .map(t => `"${t}"`) + .join(', ') + .trim()}{' '} + do{selectPostfix(readinessGatesState.nonExistingConditions, 'es', '')} not exist. +
    • + )} +
    +
    + ); + } + return null; +}; diff --git a/ui/src/app/applicationsets/components/application-operation-state/application-operation-state.scss b/ui/src/app/applicationsets/components/application-operation-state/application-operation-state.scss new file mode 100644 index 0000000000000..444f6a211dc04 --- /dev/null +++ b/ui/src/app/applicationsets/components/application-operation-state/application-operation-state.scss @@ -0,0 +1,19 @@ +.application-operation-state { + &__icons_container { + position: absolute; + left: 0; + } + + &__icons_container_padding { + left: 15px; + position: relative; + } + + &__message { + white-space: normal; + line-height: 16px; + display: inline-block; + vertical-align: middle; + } + +} diff --git a/ui/src/app/applicationsets/components/application-operation-state/application-operation-state.tsx b/ui/src/app/applicationsets/components/application-operation-state/application-operation-state.tsx new file mode 100644 index 0000000000000..0f5bbac2615a2 --- /dev/null +++ b/ui/src/app/applicationsets/components/application-operation-state/application-operation-state.tsx @@ -0,0 +1,213 @@ +import {Checkbox, DropDown, Duration, NotificationType, Ticker} from 'argo-ui'; +import * as moment from 'moment'; +import * as PropTypes from 'prop-types'; +import * as React from 'react'; + +import {ErrorNotification, Revision, Timestamp} from '../../../shared/components'; +import {AppContext} from '../../../shared/context'; +import * as models from '../../../shared/models'; +import {services} from '../../../shared/services'; +import * as utils from '../utils'; + +import './application-operation-state.scss'; + +interface Props { + application: models.Application; + operationState: models.OperationState; +} + +const Filter = (props: {filters: string[]; setFilters: (f: string[]) => void; options: string[]; title: string; style?: React.CSSProperties}) => { + const {filters, setFilters, options, title, style} = props; + return ( + ( +
    +