diff --git a/app/app.js b/app/app.js index c42697f1c..714978442 100644 --- a/app/app.js +++ b/app/app.js @@ -61,7 +61,6 @@ const App = Application.extend({ 'authenticated.project': 'authenticated.project', 'authenticated.prefs': 'authenticated.prefs', 'authenticated.cluster.nodes': 'authenticated.cluster.nodes', - 'authenticated.cluster.nodes.node': 'authenticated.cluster.nodes.node', 'authenticated.cluster.security.members.index': 'authenticated.cluster.security.members.index', 'logout': 'logout' } @@ -99,7 +98,6 @@ const App = Application.extend({ 'authenticated.project': 'authenticated.project', 'authenticated.prefs': 'authenticated.prefs', 'authenticated.cluster.nodes': 'authenticated.cluster.nodes', - 'authenticated.cluster.nodes.node': 'authenticated.cluster.nodes.node', 'authenticated.cluster.security.members.index': 'authenticated.cluster.security.members.index', 'logout': 'logout' } @@ -185,6 +183,24 @@ const App = Application.extend({ } } }, + monitoring: { + dependencies: { + services: [ + 'app', + 'intl', + 'grafana', + 'scope', + 'session', + 'modal', + 'globalStore', + 'router', + 'k8s', + 'clusterStore', + 'tooltip', + ], + externalRoutes: {} + } + }, } }); diff --git a/app/application/template.hbs b/app/application/template.hbs index 2a6abd1eb..d6a5ff4ab 100644 --- a/app/application/template.hbs +++ b/app/application/template.hbs @@ -25,4 +25,3 @@ {{component tooltip tooltipTemplate=tooltipTemplate class="container-tooltip" id="tooltip-base" role="tooltip" aria-hidden="false"}} {{modal-root}} -{{! svg-gradients}} diff --git a/app/authenticated/cluster/index/route.js b/app/authenticated/cluster/index/route.js index a6f0028d9..d369c18c8 100644 --- a/app/authenticated/cluster/index/route.js +++ b/app/authenticated/cluster/index/route.js @@ -1,26 +1,7 @@ import Route from '@ember/routing/route'; -import { inject as service } from '@ember/service'; -import { set } from '@ember/object'; -import { on } from '@ember/object/evented'; -import C from 'ui/utils/constants'; export default Route.extend({ - globalStore: service(), - scope: service(), - - model() { - return this.get('globalStore').findAll('node') - .then((nodes) => { - const cluster = this.modelFor('authenticated.cluster'); - - return { - cluster, - nodes, - }; - }); + redirect() { + this.replaceWith('authenticated.cluster.monitoring'); }, - - setDefaultRoute: on('activate', function() { - set(this, `session.${ C.SESSION.CLUSTER_ROUTE }`, 'authenticated.cluster.index'); - }), }); diff --git a/app/authenticated/cluster/index/template.hbs b/app/authenticated/cluster/index/template.hbs deleted file mode 100644 index 79253fcf0..000000000 --- a/app/authenticated/cluster/index/template.hbs +++ /dev/null @@ -1,57 +0,0 @@ -
-

{{t 'clusterDashboard.title'}}: {{model.cluster.displayName}}

- -
- - - - - - - {{action-menu size="sm" classNames="pull-right" model=model.cluster}} -
-
- -{{#if model.cluster.description}} -
- {{banner-message color='bg-secondary mb-0 mt-10' message=(linkify model.cluster.description)}} -
-{{/if}} - - - -
- {{cluster-dashboard nodes=currentClusterNodes}} -
diff --git a/app/authenticated/cluster/networking/template.hbs b/app/authenticated/cluster/networking/template.hbs deleted file mode 100644 index 8708e4b54..000000000 --- a/app/authenticated/cluster/networking/template.hbs +++ /dev/null @@ -1 +0,0 @@ -Networking diff --git a/app/authenticated/cluster/route.js b/app/authenticated/cluster/route.js index 024ab355d..4097c137a 100644 --- a/app/authenticated/cluster/route.js +++ b/app/authenticated/cluster/route.js @@ -7,7 +7,7 @@ import C from 'ui/utils/constants'; const VALID_ROUTES = ['authenticated.cluster.nodes', 'authenticated.cluster.storage.classes', 'authenticated.cluster.storage.persistent-volumes', 'authenticated.cluster.notifier', - 'authenticated.cluster.alert', 'authenticated.cluster.logging', + 'authenticated.cluster.alert', 'authenticated.cluster.logging', 'authenticated.cluster.monitoring', 'authenticated.cluster.security.members.index', 'authenticated.cluster.projects', 'authenticated.cluster.quotas']; diff --git a/app/authenticated/project/certificates/detail/index/template.hbs b/app/authenticated/project/certificates/detail/index/template.hbs index b9ea5d9b1..4ce403940 100644 --- a/app/authenticated/project/certificates/detail/index/template.hbs +++ b/app/authenticated/project/certificates/detail/index/template.hbs @@ -6,8 +6,8 @@
-
-
+
+
@@ -58,7 +58,7 @@
{{/if}} -
+
diff --git a/app/authenticated/project/route.js b/app/authenticated/project/route.js index 3cdece11b..c2a7084be 100644 --- a/app/authenticated/project/route.js +++ b/app/authenticated/project/route.js @@ -10,7 +10,8 @@ const VALID_ROUTES = ['apps-tab', 'authenticated.project.security.members.index' 'authenticated.project.ns', 'authenticated.project.certificates', 'authenticated.project.secrets', 'authenticated.project.config-maps', 'authenticated.project.registries', 'authenticated.project.alert', - 'authenticated.project.logging', 'authenticated.project.pipeline.settings']; + 'authenticated.project.logging', 'authenticated.project.pipeline.settings', + 'authenticated.project.monitoring.project-setting']; export default Route.extend(Preload, { access: service(), diff --git a/app/components/accordion-container/component.js b/app/components/accordion-container/component.js new file mode 100644 index 000000000..4e9e5abc3 --- /dev/null +++ b/app/components/accordion-container/component.js @@ -0,0 +1,35 @@ +import Component from '@ember/component'; +import layout from './template'; + +export default Component.extend({ + layout, + model: null, + expandOnInit: true, + sortBy: 'displayState', + descending: true, + initExpand: true, + headers: [ + { + name: 'displayState', + sort: ['displayState'], + translationKey: 'generic.state', + width: 120 + }, + { + name: 'name', + sort: ['name'], + translationKey: 'generic.name', + }, + { + name: 'image', + sort: ['image'], + translationKey: 'generic.image', + }, + { + name: 'restarts', + sort: ['restarts'], + translationKey: 'generic.restarts', + width: 100 + }, + ], +}); diff --git a/app/components/accordion-container/template.hbs b/app/components/accordion-container/template.hbs new file mode 100644 index 000000000..c28fc9a09 --- /dev/null +++ b/app/components/accordion-container/template.hbs @@ -0,0 +1,62 @@ +{{#accordion-list-item + title=(t 'containersSection.title') + detail=(t 'containersSection.detail') + expandAll=expandAll + expand=(action expandFn) + expandOnInit=expandOnInit + componentName='sortable-table' + as | parent | +}} + {{#sortable-table + tableClassNames="double-rows" + body=containers + bulkActions=false + descending=descending + sortBy=sortBy + stickyHeader=stickyHeader + fullRows=true + search=search + headers=headers as |sortable kind inst dt| + }} + {{#if (eq kind "row")}} + + + {{badge-state model=inst}} + + + {{inst.displayName}} + {{#if inst.initContainer}} +
+ {{t 'containersSection.initContainer'}} +
+ {{/if}} + + + {{inst.image}} + + + {{inst.restarts}} + + + {{action-menu model=inst}} + + + {{#if inst.showTransitioningMessage}} + + +
+ {{uc-first inst.transitioningMessage}} +
+ + + + {{/if}} + {{else if (eq kind "nomatch")}} + {{t 'containersSection.noMatch'}} + {{else if (eq kind "norows")}} + + {{t 'containersSection.noData'}} + + {{/if}} + {{/sortable-table}} +{{/accordion-list-item}} diff --git a/app/components/accordion-pod/component.js b/app/components/accordion-pod/component.js index dec415e63..ef7824caf 100644 --- a/app/components/accordion-pod/component.js +++ b/app/components/accordion-pod/component.js @@ -5,7 +5,7 @@ import layout from './template'; export default Component.extend(ManageLabels, { layout, model: null, - initExpandAll: true, + expandOnInit: true, sortBy: 'displayState', showKind: true, descending: true, @@ -21,6 +21,7 @@ export default Component.extend(ManageLabels, { name: 'name', sort: ['name'], translationKey: 'generic.name', + width: 400 }, { name: 'displayImage', @@ -31,18 +32,7 @@ export default Component.extend(ManageLabels, { name: 'node', sort: ['displayName'], translationKey: 'generic.node', - }, - { - name: 'displayIp', - sort: ['displayIp'], - translationKey: 'generic.ipAddress', width: 180 }, ], - - expandAllObserve: function() { - let expandAll = this.get('expandAll'); - - this.set('initExpandAll', expandAll); - }.observes('expandAll') }); diff --git a/app/components/accordion-pod/template.hbs b/app/components/accordion-pod/template.hbs index 08168b210..bf06ce194 100644 --- a/app/components/accordion-pod/template.hbs +++ b/app/components/accordion-pod/template.hbs @@ -1,8 +1,9 @@ {{#accordion-list-item title=(t 'podsSection.title') detail=(t 'podsSection.detail') - expandAll=initExpandAll + expandAll=expandAll expand=(action expandFn) + expandOnInit=expandOnInit componentName='sortable-table' as | parent | }} @@ -28,25 +29,25 @@ {{badge-state model=inst}} - {{inst.displayName}} + {{inst.displayName}} {{inst.displayImage}}

+ {{#if inst.displayIp}} + {{inst.displayIp}} / + {{/if}} {{t 'generic.createdDate' date=(date-from-now inst.created) htmlSafe=true}} / {{t 'generic.restarts'}} {{inst.restarts}}

{{#if (and inst.node.id inst.node.clusterId)}} - {{inst.node.displayName}} + {{inst.node.displayName}} {{#if (or inst.node.externalIpAddress inst.node.ipAddress)}} {{node-ip model=inst.node}} {{/if}} {{/if}} - - {{inst.displayIp}} - {{action-menu model=inst}} @@ -54,7 +55,7 @@ {{#if inst.dislayContainerMessage}} - + {{#each inst.containers as |container|}} {{#if container.showTransitioningMessage}}
diff --git a/app/components/cluster-dashboard/component.js b/app/components/cluster-dashboard/component.js deleted file mode 100644 index 5c77293c4..000000000 --- a/app/components/cluster-dashboard/component.js +++ /dev/null @@ -1,82 +0,0 @@ -import C from 'ui/utils/constants'; -import Component from '@ember/component'; -import { set, get, computed, observer } from '@ember/object'; -import { alias } from '@ember/object/computed'; -import { inject as service } from '@ember/service'; -import layout from './template'; - -export default Component.extend({ - intl: service(), - scope: service(), - - layout, - - nodes: null, - components: null, - componentStatuses: alias('scope.currentCluster.componentStatuses'), - - init() { - this._super(...arguments); - this.setComponents(); - }, - - updateComponentsStatus: observer('componentStatuses.@each.conditions', 'nodes.@each.{state}', function() { - this.setComponents(); - }), - - showDashboard: computed('scope.currentCluster.isReady', 'nodes.[]', function() { - return get(this, 'nodes').length && get(this, 'scope.currentCluster.isReady') - }), - - inactiveNodes: computed('nodes.@each.state', function() { - return get(this, 'nodes').filter( (n) => C.ACTIVEISH_STATES.indexOf(get(n, 'state')) === -1 ); - }), - - unhealthyComponents: computed('componentStatuses.@each.conditions', function() { - return (get(this, 'componentStatuses') || []) - .filter((s) => !s.conditions.any((c) => c.status === 'True')); - }), - - setComponents() { - const etcd = this.getEtcdComponent(); - const controller = this.getControllerComponent(); - const scheduler = this.getSchedulerComponent(); - const node = this.getNodeComponent(); - - set(this, 'components', [etcd, controller, scheduler, node]); - }, - - getEtcdComponent() { - return { - name: get(this, 'intl').t('clusterDashboard.etcd'), - healthy: this.isHealthy('etcd'), - }; - }, - - getControllerComponent() { - return { - name: get(this, 'intl').t('clusterDashboard.controllerManager'), - healthy: this.isHealthy('controller-manager'), - }; - }, - - getSchedulerComponent() { - return { - name: get(this, 'intl').t('clusterDashboard.scheduler'), - healthy: this.isHealthy('scheduler'), - }; - }, - - getNodeComponent() { - return { - name: get(this, 'intl').t('clusterDashboard.node'), - healthy: get(this, 'inactiveNodes.length') === 0, - }; - }, - - isHealthy(field) { - return (get(this, 'componentStatuses') || []) - .filter((s) => s.name.startsWith(field)) - .any((s) => s.conditions.any((c) => c.status === 'True')); - }, -}); diff --git a/app/components/cluster-dashboard/template.hbs b/app/components/cluster-dashboard/template.hbs deleted file mode 100644 index 966815db8..000000000 --- a/app/components/cluster-dashboard/template.hbs +++ /dev/null @@ -1,45 +0,0 @@ -{{#if showDashboard}} - {{node-gauges nodes=nodes}} -
- {{#each components as |c|}} -
- - -
- {{/each}} -
-
- {{#each unhealthyComponents as |component|}} - - {{/each}} - {{#each inactiveNodes as |node|}} - - {{/each}} -
-{{else}} - {{empty-table - resource="container" - showNew=scope.currentCluster.canAddNode - newRoute="authenticated.cluster.nodes.templates" - newTranslationKey="nodesPage.addNode" - disabled=(rbac-prevents resource="machine" scope="global" permission="create") - ctx="" - }} -{{/if}} diff --git a/app/components/container-dot/component.js b/app/components/container-dot/component.js index 8dad08aeb..951d4ef43 100644 --- a/app/components/container-dot/component.js +++ b/app/components/container-dot/component.js @@ -31,7 +31,7 @@ export default Component.extend({ }, details(/* event*/) { - var route = 'container'; + var route = 'pod'; if ( this.get('model.isVm') ) { route = 'virtualmachine'; diff --git a/app/components/container-metrics/component.js b/app/components/container-metrics/component.js new file mode 100644 index 000000000..2d17a74de --- /dev/null +++ b/app/components/container-metrics/component.js @@ -0,0 +1,20 @@ +import Component from '@ember/component'; +import Metrics from 'shared/mixins/metrics'; +import layout from './template'; +import { get, set } from '@ember/object'; + +export default Component.extend(Metrics, { + layout, + + filters: { resourceType: 'container' }, + + projectScope: true, + + init() { + this._super(...arguments); + set(this, 'metricParams', { + podName: get(this, 'podId'), + containerName: get(this, 'resourceId') + }); + }, +}); \ No newline at end of file diff --git a/app/components/container-metrics/template.hbs b/app/components/container-metrics/template.hbs new file mode 100644 index 000000000..602886c31 --- /dev/null +++ b/app/components/container-metrics/template.hbs @@ -0,0 +1,8 @@ +
+ {{metrics-action + queryAction="query" + allowDetail=false + state=state + }} + {{metrics-graph graphs=graphs loading=state.loading noGraphs=state.noGraphs}} +
\ No newline at end of file diff --git a/app/components/container/form-container-links/component.js b/app/components/container/form-container-links/component.js deleted file mode 100644 index 9f3fdb9b0..000000000 --- a/app/components/container/form-container-links/component.js +++ /dev/null @@ -1,103 +0,0 @@ -import { alias } from '@ember/object/computed'; -import { inject as service } from '@ember/service'; -import Component from '@ember/component'; -import ContainerChoices from 'ui/mixins/container-choices'; -import { - STATUS, - STATUS_INTL_KEY, - classForStatus -} from 'shared/components/accordion-list-item/component'; -import layout from './template'; - -const headers = [ - { - name: 'name', - translationKey: 'formContainerLinks.name.label', - }, - { - name: 'alias', - translationKey: 'formContainerLinks.alias.label', - }, -]; - -export default Component.extend(ContainerChoices, { - router: service(), - growl: service(), - - layout, - // Inputs - editing: null, - instance: null, - - tagName: '', - errors: null, - - headers, - - statusClass: null, - linksArray: alias('instance.instanceLinks'), - - actions: { - addLink() { - let links = this.get('linksArray'); - - if ( !links ) { - links = []; - this.set('linksArray', links); - } - - links.pushObject(this.get('store').createRecord({ - type: 'link', - name: '', - alias: '', - })); - }, - - removeLink(obj) { - this.get('linksArray').removeObject(obj); - }, - - followLink(str) { - let stack, stackName, containerName; - - if ( str.includes('/')) { - [stackName, containerName] = name.split('/'); - let stacks = this.get('store').all('stack'); - - stack = stacks.findBy('name', stackName); - } else { - stack = this.get('stack'); - containerName = str; - } - - if ( stack ) { - let container = stack.get('instances').findBy('name', containerName); - - if ( container ) { - this.get('router').transitionTo('container', container.get('id')); - - return; - } - } - - this.get('growl').fromError(`Unable to find container for "${ name }"`); - }, - }, - - status: function() { - let k = STATUS.NONE; - let count = (this.get('linksArray') || []).filterBy('name').get('length') || 0; - - if ( count ) { - if ( this.get('errors.length') ) { - k = STATUS.INCOMPLETE; - } else { - k = STATUS.COUNTCONFIGURED; - } - } - - this.set('statusClass', classForStatus(k)); - - return this.get('intl').t(`${ STATUS_INTL_KEY }.${ k }`, { count }); - }.property('linksArray.@each.name'), -}); diff --git a/app/components/container/form-container-links/template.hbs b/app/components/container/form-container-links/template.hbs deleted file mode 100644 index 4fdba4df8..000000000 --- a/app/components/container/form-container-links/template.hbs +++ /dev/null @@ -1,66 +0,0 @@ -{{#accordion-list-item - title=(t 'formContainerLinks.title') - detail=(t 'formContainerLinks.detail' appName=settings.appName) - status=status - statusClass=statusClass - expandAll=expandAll - expand=(action expandFn) -}} - {{#if editing}} - - - - - - - - - - {{#each linksArray as |link|}} - - - - - - - {{/each}} -
{{t 'formContainerLinks.name.label'}} {{t 'formContainerLinks.alias.label'}} 
- {{schema/input-container - value=link.name - exclude=launchConfig.id - stack=stack - }} - -

-
- {{input class="form-control input-sm" type="text" value=link.alias placeholder=(t 'formContainerLinks.alias.placeholder')}} - - {{#unless link.existing}} - - {{/unless}} -
- {{else}} - {{#sortable-table - classNames="grid sortable-table" - body=linksArray - descending=descending - bulkActions=false - pagingLabel="pagination.link" - headers=headers as |sortable kind row dt|}} - {{#if (eq kind "row")}} - - {{row.name}} - {{row.alias}} - - {{else if (eq kind "nomatch")}} - {{t 'formContainerLinks.noMatch'}} - {{else if (eq kind "norows")}} - {{t 'formContainerLinks.noData'}} - {{/if}} - {{/sortable-table}} - {{/if}} - -{{/accordion-list-item}} diff --git a/app/components/container/form-custom-metrics/component.js b/app/components/container/form-custom-metrics/component.js new file mode 100644 index 000000000..4c75e9b92 --- /dev/null +++ b/app/components/container/form-custom-metrics/component.js @@ -0,0 +1,52 @@ +import { inject as service } from '@ember/service'; +import { get, set, observer } from '@ember/object'; +import Component from '@ember/component'; +import layout from './template'; + +const HTTPS = 'HTTPS'; +const HTTP = 'HTTP' + +const OPTIONS = [ + { + label: HTTP, + value: HTTP + }, + { + label: HTTPS, + value: HTTPS + } +]; + +export default Component.extend({ + scope: service(), + + layout, + + editing: false, + + protocolOptions: OPTIONS, + + init() { + this._super(...arguments); + + set(this, 'metrics', get(this, 'workload.workloadMetrics') || []); + }, + + actions: { + add() { + get(this, 'metrics').pushObject({ + path: '', + port: '', + schema: HTTP + }); + }, + + remove(obj) { + get(this, 'metrics').removeObject(obj); + }, + }, + + metricsChanged: observer('metrics.@each.{port,path,schema}', function() { + set(this, 'workload.workloadMetrics', get(this, 'metrics').filter((metric) => get(metric, 'port'))); + }) +}); \ No newline at end of file diff --git a/app/components/container/form-custom-metrics/template.hbs b/app/components/container/form-custom-metrics/template.hbs new file mode 100644 index 000000000..4ff18a539 --- /dev/null +++ b/app/components/container/form-custom-metrics/template.hbs @@ -0,0 +1,77 @@ +{{#accordion-list-item + title=(t 'formCustomMetrics.title') + detail=(t 'formCustomMetrics.detail') + expandAll=expandAll + expand=(action expandFn) +}} +
+ {{#if metrics.length}} + + + + + + + + + + + + + {{#each metrics as |metric|}} + + + + + + + + + + + {{/each}} + +
+ {{#if editing}} + {{input-integer class="form-control input-sm public" min="1" max="65535" value=metric.port placeholder=(t 'formCustomMetrics.port.placeholder')}} + {{else}} + {{metric.port}} + {{/if}} +   + {{#input-or-display editable=editing value=metric.path}} + {{input class="form-control input-sm" type="text" value=metric.path placeholder=(t 'formCustomMetrics.path.placeholder')}} + {{/input-or-display}} +   + {{#if editing}} + {{new-select + class="form-control input-sm" + content=protocolOptions + value=metric.schema + }} + {{else}} + {{metric.schema}} + {{/if}} + + {{#if editing}} + + {{/if}} +
+ {{else}} + {{#unless editing}} + {{t 'formCustomMetrics.noPorts'}} + {{/unless}} + {{/if}} +
+ +
+ {{#if editing}} + + {{/if}} +
+ +{{/accordion-list-item}} \ No newline at end of file diff --git a/app/components/container/new-edit/component.js b/app/components/container/new-edit/component.js index 2e7c060d1..778bb7d32 100644 --- a/app/components/container/new-edit/component.js +++ b/app/components/container/new-edit/component.js @@ -15,6 +15,7 @@ export default Component.extend(NewOrEdit, ChildHook, { clusterStore: service(), intl: service(), prefs: service(), + scope: service(), settings: service(), layout, diff --git a/app/components/container/new-edit/template.hbs b/app/components/container/new-edit/template.hbs index c7a3294a2..30c769824 100644 --- a/app/components/container/new-edit/template.hbs +++ b/app/components/container/new-edit/template.hbs @@ -248,6 +248,18 @@ expandFn=expandFn expanded=securitySectionExpanded }} + + {{#unless isSidekick}} + {{#if scope.currentCluster.enableClusterMonitoring}} + {{container/form-custom-metrics + classNames="accordion-wrapper" + workload=service + editing=true + expandAll=al.expandAll + expandFn=expandFn + }} + {{/if}} + {{/unless}} {{/advanced-section}} {{/accordion-list}} diff --git a/app/components/info-multi-stats/component.js b/app/components/info-multi-stats/component.js deleted file mode 100644 index 2f66d0e3a..000000000 --- a/app/components/info-multi-stats/component.js +++ /dev/null @@ -1,45 +0,0 @@ -import Component from '@ember/component'; -import layout from './template'; - -export default Component.extend({ - layout, - model: null, - mode: 'small', - smallWidth: 60, - smallHeight: 25, - largeTargetId: null, - linkName: 'containerStats', - - tagName: '', - cpuFields: [{ - key: 'cpuUser', - displayName: 'infoMultiStats.cpuSection.user' - }, { - key: 'cpuSystem', - displayName: 'infoMultiStats.cpuSection.system' - }], - memoryFields: [{ - key: 'memory', - displayName: 'infoMultiStats.memorySection.used' - }], - networkFields: [{ - key: 'networkTx', - displayName: 'infoMultiStats.networkSection.transmit' - }, { - key: 'networkRx', - displayName: 'infoMultiStats.networkSection.receive' - }], - storageFields: [{ - key: 'storageWrite', - displayName: 'infoMultiStats.storageSection.write' - }, { - key: 'storageRead', - displayName: 'infoMultiStats.storageSection.read' - }], - - actions: { - toggle() { - this.set('mode', (this.get('mode') === 'small' ? 'large' : 'small')); - }, - }, -}); diff --git a/app/components/info-multi-stats/template.hbs b/app/components/info-multi-stats/template.hbs deleted file mode 100644 index 9fe6a35d9..000000000 --- a/app/components/info-multi-stats/template.hbs +++ /dev/null @@ -1,112 +0,0 @@ -{{#multi-container-stats model=model linkName=linkName emitMaps=true as |stats|}} - {{#liquid-if (eq mode "small") class=(if stats.loading 'child-loading')}} - {{#if stats.loading}} -
Connecting…
- {{else if stats.active}} - {{spark-line - data=stats.cpuTotal - width=smallWidth height=smallHeight - prefix="containersPage.table.sparkPrefixCpu" - formatter="percent" - gradient="cpu" - minMax=100 - }} - - {{spark-line - data=stats.memory - width=smallWidth height=smallHeight - prefix="containersPage.table.sparkPrefixMemory" - formatter="mib" - gradient="memory" - maxDoubleInital=true - }} - - {{spark-line - data=stats.networkTotal - width=smallWidth height=smallHeight - prefix="containersPage.table.sparkPrefixNetwork" - formatter="kbps" - gradient="network" - minMax=100 - }} - - {{spark-line - data=stats.storageTotal - width=smallWidth height=smallHeight - prefix="containersPage.table.sparkPrefixStorage" - formatter="kbps" - gradient="storage" - minMax=100 - }} - - - {{else}} -
Stats not available
- {{/if}} - {{/liquid-if}} - {{#if (eq mode "large")}} - {{#ember-wormhole to=largeTargetId}} -
- {{#if stats.loading}} -
-
-
Connecting…
-
-
- {{else if stats.active}} -
-
-

{{t 'infoMultiStats.cpuSection.labelText'}}

- {{graph-area - model=stats - fields=cpuFields - formatter="percent" - gradient="cpu" - minMax=100 - }} -
-
-

{{t 'infoMultiStats.memorySection.labelText'}}

- {{graph-area - model=stats - fields=memoryFields - formatter="mib" - gradient="memory" - maxDoubleInital=true - }} -
-
-
-
-

{{t 'infoMultiStats.networkSection.labelText'}}

- {{graph-area - model=stats - fields=networkFields - formatter="kbps" - gradient="network" - minMax=100 - }} -
-
-

{{t 'infoMultiStats.storageSection.labelText'}}

- {{graph-area - model=stats - fields=storageFields - formatter="kbps" - gradient="storage" - minMax=100 - }} -
-
- - {{else}} -
-
-
Stats not available
-
-
- {{/if}} -
- {{/ember-wormhole}} - {{/if}} -{{/multi-container-stats}} diff --git a/app/components/new-catalog/template.hbs b/app/components/new-catalog/template.hbs index 626efe717..e47982804 100644 --- a/app/components/new-catalog/template.hbs +++ b/app/components/new-catalog/template.hbs @@ -20,7 +20,7 @@ {{marked-down markdown=appReadmeContent}} {{else if (not noAppReadme)}}
- +
{{else if noAppReadme}}

@@ -122,7 +122,7 @@ {{#if getTemplate.isRunning}}
- +
{{else}} @@ -167,7 +167,7 @@ {{#if decoding}}
- +
{{else}} diff --git a/app/components/node-conditions/template.hbs b/app/components/node-conditions/template.hbs deleted file mode 100644 index 93d9dc64a..000000000 --- a/app/components/node-conditions/template.hbs +++ /dev/null @@ -1,14 +0,0 @@ -
- {{#each conditions as |c|}} -
- -
- {{/each}} -
diff --git a/app/components/node-gauges/template.hbs b/app/components/node-gauges/template.hbs deleted file mode 100644 index 13be3637e..000000000 --- a/app/components/node-gauges/template.hbs +++ /dev/null @@ -1,5 +0,0 @@ -
- {{#each gauges as |gauge|}} -
{{percent-gauge value=gauge.value title=gauge.title subtitle=gauge.subtitle ticks=gauge.ticks}}
- {{/each}} -
\ No newline at end of file diff --git a/app/components/node-group/template.hbs b/app/components/node-group/template.hbs index 4ac39d7b5..db8fa001f 100644 --- a/app/components/node-group/template.hbs +++ b/app/components/node-group/template.hbs @@ -1,7 +1,7 @@ {{#if (and model.id model.clusterId)}} - + {{t 'nodeGroup.label' name=model.displayName}} {{else}} diff --git a/app/components/node-row/template.hbs b/app/components/node-row/template.hbs index 4d21465ec..cb8c3a396 100644 --- a/app/components/node-row/template.hbs +++ b/app/components/node-row/template.hbs @@ -10,12 +10,12 @@ {{#if (eq view "global")}} {{#if model.clusterId}} - {{model.displayName}} + {{model.displayName}} {{else}} {{model.displayName}} {{/if}} {{else}} - {{model.displayName}} + {{model.displayName}} {{/if}} {{#if (or model.externalIpAddress model.ipAddress)}} {{node-ip model=model}} diff --git a/app/components/pod-metrics/component.js b/app/components/pod-metrics/component.js new file mode 100644 index 000000000..656915a2e --- /dev/null +++ b/app/components/pod-metrics/component.js @@ -0,0 +1,17 @@ +import Component from '@ember/component'; +import Metrics from 'shared/mixins/metrics'; +import layout from './template'; +import { get, set } from '@ember/object'; + +export default Component.extend(Metrics, { + layout, + + filters: { resourceType: 'pod' }, + + projectScope: true, + + init() { + this._super(...arguments); + set(this, 'metricParams', { podName: get(this, 'resourceId') }); + }, +}); \ No newline at end of file diff --git a/app/components/pod-metrics/template.hbs b/app/components/pod-metrics/template.hbs new file mode 100644 index 000000000..36099f6a6 --- /dev/null +++ b/app/components/pod-metrics/template.hbs @@ -0,0 +1,7 @@ +
+ {{metrics-action + queryAction="query" + state=state + }} + {{metrics-graph graphs=graphs loading=state.loading noGraphs=state.noGraphs}} +
\ No newline at end of file diff --git a/app/components/pod-row/template.hbs b/app/components/pod-row/template.hbs index b4d3932cd..283bb74e7 100644 --- a/app/components/pod-row/template.hbs +++ b/app/components/pod-row/template.hbs @@ -16,7 +16,7 @@ {{badge-state model=model}} - {{model.displayName}} + {{model.displayName}} {{#if model.showTransitioningMessage}}
{{uc-first model.transitioningMessage}}
{{else if model.displayEndpoints}} @@ -32,7 +32,7 @@ {{#copy-inline clipboardText=model.displayIp}}{{format-ip model.displayIp}}{{/copy-inline}} / {{/if}} {{#if (and showNode model.node)}} - {{model.node.displayName}} / + {{model.node.displayName}} / {{/if}} {{t 'generic.createdDate' date=(date-from-now model.created) htmlSafe=true}} / {{t 'generic.restarts'}} {{model.restarts}} diff --git a/app/components/progress-bar-multi/component.js b/app/components/progress-bar-multi/component.js index beeea5165..e248f4dfa 100644 --- a/app/components/progress-bar-multi/component.js +++ b/app/components/progress-bar-multi/component.js @@ -76,7 +76,7 @@ export default Component.extend({ obj.css = (`width: ${ obj.percent }%`).htmlSafe(); }); - return out; + return out.filter((obj) => obj.percent); })); valueDep = `tooltipValues.@each.{${ labelKey },${ valueKey }}`; diff --git a/app/components/progress-bar-multi/template.hbs b/app/components/progress-bar-multi/template.hbs index c90df43ec..3e9ff0688 100644 --- a/app/components/progress-bar-multi/template.hbs +++ b/app/components/progress-bar-multi/template.hbs @@ -9,9 +9,7 @@ }}
{{~#each pieces as |obj|~}} - {{#if obj.percent}} -
- {{/if}} +
{{~/each~}}
{{/tooltip-element}} diff --git a/app/components/progress-bar/component.js b/app/components/progress-bar/component.js deleted file mode 100644 index 5f7b0efe9..000000000 --- a/app/components/progress-bar/component.js +++ /dev/null @@ -1,49 +0,0 @@ -import Component from '@ember/component'; -import layout from './template'; - -export default Component.extend({ - layout, - tagName: 'div', - classNames: ['progress'], - - color: '', - min: 0, - value: 0, - max: 100, - zIndex: null, - - didInsertElement() { - this.percentDidChange(); - this.zIndexDidChange(); - }, - percent: function() { - var min = this.get('min'); - var max = this.get('max'); - var value = Math.max(min, Math.min(max, this.get('value'))); - - var per = value / (max - min) * 100; // Percent 0-100 - - per = Math.round(per * 100) / 100; // Round to 2 decimal places - - return per; - }.property('min', 'max', 'value'), - - colorClass: function() { - var color = this.get('color'); - - if ( !color ) { - return; - } - - return `progress-bar-${ color.replace(/^progress-bar-/, '') }`; - }.property('color'), - - percentDidChange: function() { - this.$('.progress-bar').css('width', `${ this.get('percent') }%`); - }.observes('percent'), - - zIndexDidChange: function() { - this.$().css('zIndex', this.get('zIndex') || 'inherit'); - }.observes('zIndex'), - -}); diff --git a/app/components/progress-bar/template.hbs b/app/components/progress-bar/template.hbs deleted file mode 100644 index 9ec88ff88..000000000 --- a/app/components/progress-bar/template.hbs +++ /dev/null @@ -1,2 +0,0 @@ -
-
{{textLabel}}
diff --git a/app/components/resource-event-list/template.hbs b/app/components/resource-event-list/template.hbs index 0c8149276..187d15bf7 100644 --- a/app/components/resource-event-list/template.hbs +++ b/app/components/resource-event-list/template.hbs @@ -48,7 +48,7 @@ {{/component}} {{else}}
- +
{{/if}} {{/accordion-list-item}} diff --git a/app/components/spark-line/component.js b/app/components/spark-line/component.js deleted file mode 100644 index 7e92a49cc..000000000 --- a/app/components/spark-line/component.js +++ /dev/null @@ -1,213 +0,0 @@ -import { htmlSafe } from '@ember/string'; -import { inject as service } from '@ember/service'; -import Component from '@ember/component'; -import { GRADIENT_COLORS } from 'shared/components/svg-gradients/component'; -import { formatPercent, formatMib, formatKbps } - from 'ui/utils/util'; -import { - select, - scale, - min as d3Min, - max as d3Max, -} from 'd3'; -import layout from './template'; - -const FORMATTERS = { - value: (value) => value, - percent: formatPercent, - mib: formatMib, - kbps: formatKbps -}; - -export default Component.extend({ - intl: service(), - layout, - tagName: 'svg', - classNames: ['spark-line'], - attributeBindings: ['cssSize:style'], - - data: null, - width: null, - height: 20, - margin: 2, - - min: 0, - - minMax: null, // lower bound on how small automatic max can be - max: null, // set an explicit max - maxDoubleInital: false, // if true, set max to double the initial non-zero data point - scaleDown: false, // if true, max is allowed to go back down. If false it can only go up. - - gradient: null, - colorIdx: 0, - interpolation: 'basis', // 'step-after', - formatter: 'value', - - svg: null, - line: null, - dot: null, - text: null, - textBg: null, - x: null, - y: null, - observedMax: null, // The largest max seen so far - - hasData: function() { - if (this.get('data.length') > 0 && !this.get('svg')) { - this.create(); - } - }.observes('data.length'), - - cssSize: function() { - let margin = parseInt(this.get('margin', 10)); - let width = (parseInt(this.get('width'), 10) + 2 * margin); - let height = (parseInt(this.get('height'), 10) + 2 * margin); - - return new htmlSafe(`width: ${ width }px; height: ${ height }px`); - }.property('width', 'height'), - - lastValue: function() { - var data = this.get('data'); - - if (data && data.get('length')) { - return data.objectAt(data.get('length') - 1); - } - }.property('data.[]'), - - updateLine: function() { - var line = this.get('line'); - var interp = this.get('interpolation'); - - if (line) { - line.interpolate(interp); - } - }.observes('interpolation'), - - update: function() { - var svg = this.get('svg'); - var data = (this.get('data') || []).slice(); - var x = this.get('x'); - var y = this.get('y'); - var line = this.get('line'); - var text = this.get('text'); - var textBg = this.get('textBg'); - var width = this.get('width'); - var height = this.get('height'); - var margin = this.get('margin'); - - if (svg && data && x && y && line) { - x.domain([0, data.get('length') - 1]); - x.range([0, width - margin]); - - var min = this.get('min') === null ? d3Min(data) : this.get('min'); - var max = this.adjustMax(d3Max(data)); - - y.domain([min, max]); - y.range([height - margin, margin]); - y.rangeRound([height - margin, margin]); - - // console.log('update', data[data.length-2], data[data.length-1], x.domain(), x.range(), y.domain(), y.range()); - svg.selectAll('path') - .data([data]) - .attr('d', line); - - this.get('dot') - .attr('cx', x(data.length - 1) || 0 ) - .attr('cy', y(data[data.length - 1]) || 0); - - var str = FORMATTERS[this.get('formatter')](this.get('lastValue')); - - text.text(str); - textBg.text(str); - - text - .attr('x', width / 2) - .attr('y', height) - - textBg - .attr('x', width / 2) - .attr('y', height); - } - }.observes('data', 'data.[]'), - create() { - let margin = this.get('margin'); - var svg = select(this.$()[0]) - .attr('transform', `translate(${ margin },${ margin })`); - - this.set('svg', svg); - this.set('x', scale.linear()); - this.set('y', scale.linear()); - - var line = svg.line() - .defined((d) => (typeof d === 'number')) - .x((d, i) => this.get('x')(i)) - .y((d) => this.get('y')(d)); - - this.set('line', line); - - this.updateLine(); - - let path = svg.append('path') - .attr('class', `spark-path`) - .attr('d', line(this.get('data'))); - - if ( this.get('gradient') ) { - path.style('stroke', GRADIENT_COLORS[this.get('gradient')][this.get('colorIdx')]) - } - - var dot = svg.append('circle') - .attr('class', 'spark-dot') - .attr('cx', 0) - .attr('cy', 0) - .attr('r', 2); - - this.set('dot', dot); - - var textBg = svg.append('text') - .attr('class', `spark-text-bg`) - .attr('alignment-baseline', 'middle') - .attr('text-anchor', 'middle') - .attr('x', 0) - .attr('y', 0); - - this.set('textBg', textBg); - - var text = svg.append('text') - .attr('class', `spark-text`) - .attr('class', `spark-text`) - .attr('alignment-baseline', 'middle') - .attr('text-anchor', 'middle') - .attr('x', 0) - .attr('y', 0); - - this.set('text', text); - }, - - adjustMax(dataMax) { - let optMinMax = this.get('minMax'); - let optMax = this.get('max'); - let optScaleDown = this.get('scaleDown'); - let observedMax = this.get('observedMax'); - - let out = dataMax; - - if ( optMax ) { - out = optMax; - } else if ( optMinMax ) { - out = Math.max(optMinMax, out); - } - - if ( observedMax && !optScaleDown ) { - out = Math.max(observedMax, out); - } - - if ( !observedMax && out > 0 && this.get('maxDoubleInital') ) { - out *= 2; - } - - this.set('observedMax', out); - - return out; - }, - -}); diff --git a/app/components/spark-line/template.hbs b/app/components/spark-line/template.hbs deleted file mode 100644 index fb5c4b157..000000000 --- a/app/components/spark-line/template.hbs +++ /dev/null @@ -1 +0,0 @@ -{{yield}} \ No newline at end of file diff --git a/app/components/svg-gradients/component.js b/app/components/svg-gradients/component.js deleted file mode 100644 index 06dfe34b2..000000000 --- a/app/components/svg-gradients/component.js +++ /dev/null @@ -1,50 +0,0 @@ -import Component from '@ember/component'; -import { select } from 'd3'; -import layout from './template'; - -export const GRADIENT_COLORS = { - 'cpu': ['#2ECC71', '#DBE8B1'], - 'memory': ['#00558B', '#AED6F1'], - 'network': ['#E49701', '#F1C40F'], - 'storage': ['#3A6F81', '#ABCED3'], -}; - -export default Component.extend({ - layout, - tagName: '', - didInsertElement() { - var svg = select('body').append('svg:svg') - .attr('id', 'svg-gradients') - .attr('height', '0') - .attr('height', '0') - .attr('width', '0') - .style('position', 'absolute'); - - var defs = svg.append('svg:defs'); - - Object.keys(GRADIENT_COLORS).forEach((name) => { - GRADIENT_COLORS[name].forEach((val, idx) => { - var gradient = defs.append('svg:linearGradient') - .attr('id', `${ name }-${ idx }-gradient`) - .attr('x1', '0%') - .attr('y1', '0%') - .attr('x2', '0%') - .attr('y2', '100%') - .attr('spreadMethod', 'pad'); - - gradient.append('svg:stop') - .attr('offset', '0%') - .attr('stop-color', val) - .attr('stop-opacity', '0.5'); - gradient.append('svg:stop') - .attr('offset', '10%') - .attr('stop-color', val) - .attr('stop-opacity', '0.25'); - gradient.append('svg:stop') - .attr('offset', '100%') - .attr('stop-color', val) - .attr('stop-opacity', '0.1'); - }); - }); - }, -}); diff --git a/app/components/svg-gradients/template.hbs b/app/components/svg-gradients/template.hbs deleted file mode 100644 index fb5c4b157..000000000 --- a/app/components/svg-gradients/template.hbs +++ /dev/null @@ -1 +0,0 @@ -{{yield}} \ No newline at end of file diff --git a/app/components/workload-metrics/component.js b/app/components/workload-metrics/component.js new file mode 100644 index 000000000..42e7eebb7 --- /dev/null +++ b/app/components/workload-metrics/component.js @@ -0,0 +1,18 @@ +import Component from '@ember/component'; +import Metrics from 'shared/mixins/metrics'; +import layout from './template'; +import { get, set } from '@ember/object'; + +export default Component.extend(Metrics, { + layout, + + filters: { resourceType: 'workload' }, + + projectScope: true, + + init() { + this._super(...arguments); + set(this, 'metricParams', { workloadName: get(this, 'resourceId') }); + }, + +}); \ No newline at end of file diff --git a/app/components/workload-metrics/template.hbs b/app/components/workload-metrics/template.hbs new file mode 100644 index 000000000..36099f6a6 --- /dev/null +++ b/app/components/workload-metrics/template.hbs @@ -0,0 +1,7 @@ +
+ {{metrics-action + queryAction="query" + state=state + }} + {{metrics-graph graphs=graphs loading=state.loading noGraphs=state.noGraphs}} +
\ No newline at end of file diff --git a/app/container/controller.js b/app/container/controller.js index 0c2f413ae..22dfe7c02 100644 --- a/app/container/controller.js +++ b/app/container/controller.js @@ -1,26 +1,18 @@ import { inject as service } from '@ember/service'; import Controller from '@ember/controller'; -import { get, set, observer, computed } from '@ember/object'; -import { once } from '@ember/runloop'; +import { get, computed, observer } from '@ember/object'; +import { alias } from '@ember/object/computed'; +import C from 'ui/utils/constants'; export default Controller.extend({ + scope: service(), router: service(), - selectedContainer: null, + monitoringEnabled: alias('scope.currentCluster.isMonitoringReady'), - actions: { - select(container) { - set(this, 'selectedContainer', container); - } - }, - - containerDidChange: observer('model.containers.[]', function() { - once(() => set(this, 'selectedContainer', get(this, 'model.containers.firstObject'))); - }), - - podStateDidChange: observer('model.state', function() { - if ( get(this, 'model.state') === 'removed' && get(this, 'router.currentRouteName') === 'container' ) { - const workloadId = get(this, 'model.workloadId'); + podStateDidChange: observer('model.pod.state', function() { + if ( C.REMOVEDISH_STATES.includes(get(this, 'model.pod.state')) && get(this, 'router.currentRouteName') === 'container' ) { + const workloadId = get(this, 'model.pod.workloadId'); if ( workloadId ) { this.transitionToRoute('workload', workloadId); @@ -29,9 +21,10 @@ export default Controller.extend({ } } }), - displayEnvironmentVars: computed('selectedContainer', function() { + + displayEnvironmentVars: computed('model.environment', function() { var envs = []; - var environment = this.get('selectedContainer.environment') || {}; + var environment = get(this, 'model.environment') || {}; Object.keys(environment).forEach((key) => { envs.pushObject({ diff --git a/app/container/route.js b/app/container/route.js index 2ecb6fc95..3e10468d4 100644 --- a/app/container/route.js +++ b/app/container/route.js @@ -1,5 +1,6 @@ import { get } from '@ember/object'; import Route from '@ember/routing/route'; +import { hash } from 'rsvp'; export default Route.extend({ beforeModel() { @@ -14,12 +15,16 @@ export default Route.extend({ } }, model(params) { - const pod = get(this, 'store').find('pod', params.container_id); + const pod = get(this, 'store').find('pod', params.pod_id); - if ( !pod ) { - this.replaceWith('authenticated.project.index'); - } + return hash({ pod }).then((hash) => { + const container = get(hash, 'pod.containers').findBy('name', params.container_name); - return pod; + if ( !container ) { + this.replaceWith('authenticated.project.index'); + } + + return container; + }); }, }); diff --git a/app/container/template.hbs b/app/container/template.hbs index 039606fc6..de9968918 100644 --- a/app/container/template.hbs +++ b/app/container/template.hbs @@ -1,7 +1,7 @@

- {{t 'podPage.header' name=model.displayName}} + {{t 'containerPage.header' name=model.displayName}}

@@ -9,6 +9,7 @@ {{action-menu model=model showPrimary=false classNames="ml-10 inline-block" size="sm"}}
+ {{#if model.description}} {{banner-message color='bg-secondary mb-0 mt-10' message=(linkify model.description)}} {{/if}} @@ -17,230 +18,144 @@

{{uc-first model.transitioningMessage}}

{{/if}} -
-
- -
- {{#accordion-list as |al expandFn|}} - {{#if model.workload}} - {{container/form-scheduling - initialHostId=model.workload.nodeId - scheduling=model.workload.scheduling - editing=false - expandAll=al.expandAll - expandFn=expandFn - classNames="accordion" - }} +
+ {{#if model.pod}} + {{#link-to "pod" model.podId}}{{model.podId}}{{/link-to}} + {{else if model.podId}} + {{model.podId}} + {{else}} + {{t 'generic.none'}} {{/if}} +
+

- {{container/form-networking - classNames="accordion-wrapper" - service=model - editing=false - expandAll=al.expandAll - expandFn=expandFn - }} - - {{form-labels-annotations - classNames="accordion-wrapper" - model=model - editing=false - expandAll=al.expandAll - expandFn=expandFn - }} - - {{resource-condition-list - resourceType=(t 'generic.pod') - conditions=model.status.conditions - expandAll=al.expandAll - expandFn=expandFn - }} - - {{resource-event-list - resourceType=(t 'generic.pod') - expandAll=al.expandAll - expandFn=expandFn - namespaceId=model.namespaceId - name=model.name - kind="Pod" - }} - {{/accordion-list}} - - -
- -
- -{{#each model.containers as |container|}} -
- {{#if container.showTransitioningMessage}} -

{{uc-first container.transitioningMessage}}

+ -
-
-

- {{t 'podPage.image'}}: - {{container.image}} {{copy-to-clipboard clipboardText=container.image size="small"}} -

+{{#accordion-list as |al expandFn|}} + {{#if scope.currentCluster.isMonitoringReady}} + {{#metrics-summary + expandAll=al.expandAll + expandFn=expandFn + grafanaUrl=model.grafanaUrl + title=(t "metricsAction.sections.container") + }} + {{container-metrics podId=model.pod.id resourceId=model.name}} + {{/metrics-summary}} + {{/if}} + + {{#accordion-list-item + title=(t 'containerPage.portsTab.header') + detail=(t 'containerPage.portsTab.detail') + expandAll=al.expandAll + expand=(action expandFn) + }} + {{container/form-ports + initialPorts=model.ports + editing=false + }} + {{/accordion-list-item}} + + {{container/form-volumes + launchConfig=model + workload=model.pod + namespace=model.pod.namespace + loggingEnabled=true + editing=false + expandAll=al.expandAll + expandFn=expandFn + }} + + {{#accordion-list-item + title=(t 'containerPage.envTab.header') + detail=(t 'containerPage.envTab.detail') + expandAll=al.expandAll + expand=(action expandFn) + }} + {{form-env-var + model=displayEnvironmentVars + }} + {{/accordion-list-item}} + + {{container/form-command + instance=model + service=model.pod + editing=false + expandAll=al.expandAll + expandFn=expandFn + }} + + {{#accordion-list-item + title=(t 'formHealthCheck.title') + detail=(t 'formHealthCheck.detail') + expandAll=al.expandAll + expand=(action expandFn) + }} +
+
+
-
- {{badge-state model=container}} - {{action-menu model=container showPrimary=false classNames="ml-10 inline-block" size="sm"}} +
+ {{#if model.livenessProbe}} + + {{/if}}
-
- - {{#accordion-list as |al expandFn|}} - {{#accordion-list-item - title=(t 'containerPage.portsTab.header') - detail=(t 'containerPage.portsTab.detail') - expandAll=al.expandAll - expand=(action expandFn) - }} - {{container/form-ports - initialPorts=container.ports - editing=false +
+
+
+ {{form-healthcheck + initialCheck=model.readinessProbe + editing=false }} - {{/accordion-list-item}} - - {{container/form-volumes - launchConfig=container - workload=model - namespace=model.namespace - loggingEnabled=true - editing=false - expandAll=al.expandAll - expandFn=expandFn - }} - - {{#accordion-list-item - title=(t 'containerPage.envTab.header') - detail=(t 'containerPage.envTab.detail') - expandAll=al.expandAll - expand=(action expandFn) - }} - {{form-env-var - model=displayEnvironmentVars - }} - {{/accordion-list-item}} - - {{container/form-command - instance=container - service=model - editing=false - expandAll=al.expandAll - expandFn=expandFn - }} - - {{#accordion-list-item - title=(t 'formHealthCheck.title') - detail=(t 'formHealthCheck.detail') - expandAll=al.expandAll - expand=(action expandFn) - }} -
-
- -
-
- {{#if container.livenessProbe}} - - {{/if}} -
+
+ {{#if model.livenessProbe}} +
+ {{form-healthcheck + initialCheck=model.livenessProbe + editing=false + isLiveness=true + }}
-
-
- {{form-healthcheck - initialCheck=container.readinessProbe - editing=false - }} -
- {{#if container.livenessProbe}} -
- {{form-healthcheck - initialCheck=container.livenessProbe - editing=false - isLiveness=true - }} -
- {{/if}} -
- {{/accordion-list-item}} + {{/if}} +
+ {{/accordion-list-item}} - {{container/form-security - instance=container - service=model - editing=false - expandAll=al.expandAll - expandFn=expandFn - }} - {{/accordion-list}} - -{{/each}} + {{container/form-security + instance=model + service=model.pod + editing=false + expandAll=al.expandAll + expandFn=expandFn + }} +{{/accordion-list}} \ No newline at end of file diff --git a/app/instance-initializers/nav.js b/app/instance-initializers/nav.js index 20d856f22..fb9df1aac 100644 --- a/app/instance-initializers/nav.js +++ b/app/instance-initializers/nav.js @@ -1,4 +1,5 @@ import { getProjectId, getClusterId, bulkAdd } from 'ui/utils/navigation-tree'; +import { get } from '@ember/object'; const rootNav = [ // Project @@ -34,17 +35,8 @@ const rootNav = [ id: 'infra', localizedLabel: 'nav.infra.tab', ctx: [getProjectId], - route: 'authenticated.project.alert', + route: 'authenticated.project.certificates', submenu: [ - { - id: 'tools-alerts', - localizedLabel: 'nav.tools.alerts', - icon: 'icon icon-alert', - route: 'authenticated.project.alert', - resource: [], - ctx: [getProjectId], - resourceScope: 'global', - }, { id: 'infra-certificates', localizedLabel: 'nav.infra.certificates', @@ -63,24 +55,6 @@ const rootNav = [ resource: ['configmap'], resourceScope: 'project', }, - { - id: 'tools-logging', - localizedLabel: 'nav.tools.logging', - icon: 'icon icon-files', - route: 'authenticated.project.logging', - resourceScope: 'global', - resource: [], - ctx: [getProjectId], - }, - { - id: 'tools-pipeline', - localizedLabel: 'nav.tools.pipeline', - icon: 'icon icon-garbage', - route: 'authenticated.project.pipeline.settings', - resource: ['sourcecodeproviderconfig'], - resourceScope: 'project', - ctx: [getProjectId], - }, { id: 'infra-registries', localizedLabel: 'nav.infra.registries', @@ -120,12 +94,58 @@ const rootNav = [ resourceScope: 'global', ctx: [getProjectId], }, + { + scope: 'project', + id: 'project-tools', + localizedLabel: 'nav.tools.tab', + ctx: [getProjectId], + resource: [], + resourceScope: 'global', + route: 'authenticated.project.alert', + submenu: [ + { + id: 'tools-alerts', + localizedLabel: 'nav.tools.alerts', + route: 'authenticated.project.alert', + resource: [], + ctx: [getProjectId], + resourceScope: 'global', + }, + { + id: 'tools-logging', + localizedLabel: 'nav.tools.logging', + route: 'authenticated.project.logging', + resourceScope: 'global', + resource: [], + ctx: [getProjectId], + }, + { + id: 'tools-monitoring', + localizedLabel: 'nav.tools.monitoring', + route: 'authenticated.project.monitoring.project-setting', + resourceScope: 'global', + resource: [], + ctx: [getProjectId], + condition() { + return !get(this, 'project.isSystemProject') && get(this, 'cluster.enableClusterMonitoring') + } + }, + { + id: 'tools-pipeline', + localizedLabel: 'nav.tools.pipeline', + route: 'authenticated.project.pipeline.settings', + resource: ['sourcecodeproviderconfig'], + resourceScope: 'project', + ctx: [getProjectId], + }, + ] + }, // Cluster { scope: 'cluster', id: 'cluster-k8s', localizedLabel: 'nav.cluster.dashboard', - route: 'authenticated.cluster.index', + route: 'authenticated.cluster.monitoring', ctx: [getClusterId], resource: ['node'], resourceScope: 'global', @@ -217,22 +237,27 @@ const rootNav = [ { id: 'cluster-tools-notifiers', localizedLabel: 'nav.tools.notifiers', - // icon: 'icon icon-key', route: 'authenticated.cluster.notifier', resourceScope: 'global', resource: [], ctx: [getClusterId], }, - { divider: true }, { id: 'cluster-tools-logging', localizedLabel: 'nav.tools.logging', - // icon: 'icon icon-key', route: 'authenticated.cluster.logging', resourceScope: 'global', resource: [], ctx: [getClusterId], }, + { + id: 'cluster-tools-monitoring', + localizedLabel: 'nav.tools.monitoring', + route: 'authenticated.cluster.monitoring.cluster-setting', + resourceScope: 'global', + resource: [], + ctx: [getClusterId], + }, ], }, diff --git a/app/models/cluster.js b/app/models/cluster.js index 00789f204..3b789c856 100644 --- a/app/models/cluster.js +++ b/app/models/cluster.js @@ -3,25 +3,26 @@ import { inject as service } from '@ember/service'; import Resource from 'ember-api-store/models/resource'; import { hasMany } from 'ember-api-store/utils/denormalize'; import ResourceUsage from 'shared/mixins/resource-usage'; +import Grafana from 'shared/mixins/grafana'; import { equal, alias } from '@ember/object/computed'; import { resolve } from 'rsvp'; import C from 'ui/utils/constants'; -export default Resource.extend(ResourceUsage, { - globalStore: service(), - growl: service(), - scope: service(), - router: service(), +export default Resource.extend(Grafana, ResourceUsage, { + globalStore: service(), + growl: service(), + scope: service(), + router: service(), namespaces: hasMany('id', 'namespace', 'clusterId'), projects: hasMany('id', 'project', 'clusterId'), nodes: hasMany('id', 'node', 'clusterId'), nodePools: hasMany('id', 'nodePool', 'clusterId'), clusterRoleTemplateBindings: hasMany('id', 'clusterRoleTemplateBinding', 'clusterId'), + grafanaDashboardName: 'Cluster', machines: alias('nodes'), roleTemplateBindings: alias('clusterRoleTemplateBindings'), isGKE: equal('driver', 'googleKubernetesEngine'), - getAltActionDelete: computed('action.remove', function() { // eslint-disable-line return get(this, 'canBulkRemove') ? 'delete' : null; }), @@ -56,6 +57,21 @@ export default Resource.extend(ResourceUsage, { return null; }), + isMonitoringReady: computed('monitoringStatus.@each.conditions', function() { + if ( !get(this, 'enableClusterMonitoring') ) { + return false; + } + const conditions = get(this, 'monitoringStatus.conditions') || []; + + if ( get(conditions, 'length') > 0 ) { + const ready = conditions.filterBy('status', 'True') || [] ; + + return get(ready, 'length') === get(conditions, 'length'); + } + + return false; + }), + isReady: computed('conditions.@each.status', function() { return this.hasCondition('Ready'); }), @@ -117,8 +133,14 @@ export default Resource.extend(ResourceUsage, { } }), + systemProject: computed('projects.@each.isSystemProject', function() { + let projects = (get(this, 'projects') || []).filterBy('isSystemProject', true); + + return get(projects, 'firstObject'); + }), + defaultProject: computed('projects.@each.{name,clusterOwner}', function() { - let projects = this.get('projects'); + let projects = get(this, 'projects'); let out = projects.findBy('isDefault'); @@ -152,7 +174,7 @@ export default Resource.extend(ResourceUsage, { }, edit() { - this.get('router').transitionTo('authenticated.cluster.edit', this.get('id')); + get(this, 'router').transitionTo('authenticated.cluster.edit', get(this, 'id')); }, scaleDownPool(id) { @@ -187,8 +209,8 @@ export default Resource.extend(ResourceUsage, { const promise = this._super.apply(this, arguments); return promise.then((/* resp */) => { - if (this.get('scope.currentCluster.id') === this.get('id')) { - this.get('router').transitionTo('global-admin.clusters'); + if (get(this, 'scope.currentCluster.id') === get(this, 'id')) { + get(this, 'router').transitionTo('global-admin.clusters'); } }); }, diff --git a/app/models/container.js b/app/models/container.js index 7890cf3ee..5ab17f600 100644 --- a/app/models/container.js +++ b/app/models/container.js @@ -3,9 +3,11 @@ import { reference } from 'ember-api-store/utils/denormalize'; import DisplayImage from 'shared/mixins/display-image'; import { get, computed } from '@ember/object'; import { inject as service } from '@ember/service'; +import Grafana from 'shared/mixins/grafana'; +import { alias } from '@ember/object/computed'; import { later } from '@ember/runloop'; -var Container = Resource.extend(DisplayImage, { +var Container = Resource.extend(Grafana, DisplayImage, { modalService: service('modal'), intl: service(), scope: service(), @@ -14,7 +16,17 @@ var Container = Resource.extend(DisplayImage, { links: {}, type: 'container', - pod: reference('podId'), + grafanaDashboardName: 'Pods', + pod: reference('podId'), + grafanaResourceId: alias('name'), + + podName: computed('pod.name', function() { + return get(this, 'pod.name'); + }), + + namespaceId: computed('pod.namespaceId', function() { + return get(this, 'pod.namespaceId'); + }), availableActions: computed('state', function() { let isRunning = get(this, 'state') === 'running'; @@ -39,6 +51,14 @@ var Container = Resource.extend(DisplayImage, { return choices; }), + restarts: computed('pod.status.containerStatuses.@each.restartCount', function() { + const state = (get(this, 'pod.status.containerStatuses') || []).findBy('name', get(this, 'name')); + + if ( state ) { + return get(state, 'restartCount'); + } + }), + validateQuota() { const projectLimit = get(this, 'scope.currentProject.resourceQuota.limit'); diff --git a/app/models/node.js b/app/models/node.js index 7de94ce61..81f89f830 100644 --- a/app/models/node.js +++ b/app/models/node.js @@ -1,5 +1,5 @@ import { computed, get } from '@ember/object'; -import { or } from '@ember/object/computed'; +import { or, alias } from '@ember/object/computed'; import Resource from 'ember-api-store/models/resource'; import { download } from 'shared/utils/util'; import C from 'ui/utils/constants'; @@ -7,11 +7,12 @@ import StateCounts from 'ui/mixins/state-counts'; import { inject as service } from '@ember/service'; import { reference } from 'ember-api-store/utils/denormalize'; import ResourceUsage from 'shared/mixins/resource-usage'; +import Grafana from 'shared/mixins/grafana'; const UNSCHEDULABLE_KEYS = ['node-role.kubernetes.io/etcd', 'node-role.kubernetes.io/controlplane']; const UNSCHEDULABLE_EFFECTS = ['NoExecute', 'NoSchedule']; -var Node = Resource.extend(StateCounts, ResourceUsage, { +var Node = Resource.extend(Grafana, StateCounts, ResourceUsage, { modalService: service('modal'), settings: service(), prefs: service(), @@ -22,6 +23,9 @@ var Node = Resource.extend(StateCounts, ResourceUsage, { type: 'node', + grafanaDashboardName: 'Nodes', + grafanaResourceId: alias('ipAddress'), + cluster: reference('clusterId', 'cluster'), nodePool: reference('nodePoolId'), diff --git a/app/models/pod.js b/app/models/pod.js index 50f0c42a7..26bb828df 100644 --- a/app/models/pod.js +++ b/app/models/pod.js @@ -1,5 +1,6 @@ import C from 'ui/utils/constants'; import Resource from 'ember-api-store/models/resource'; +import { alias } from '@ember/object/computed'; import { reference } from 'ember-api-store/utils/denormalize'; import { get, computed } from '@ember/object'; import { inject as service } from '@ember/service'; @@ -7,9 +8,10 @@ import { strPad } from 'ui/utils/util'; import { formatSi } from 'shared/utils/parse-unit'; import { later } from '@ember/runloop'; import { gt } from '@ember/object/computed'; +import Grafana from 'shared/mixins/grafana'; import DisplayImage from 'shared/mixins/display-image'; -var Pod = Resource.extend(DisplayImage, { +var Pod = Resource.extend(Grafana, DisplayImage, { router: service(), modalService: service('modal'), globalStore: service(), @@ -22,6 +24,9 @@ var Pod = Resource.extend(DisplayImage, { canEdit: false, canClone: false, + grafanaDashboardName: 'Pods', + grafanaResourceId: alias('name'), + namespace: reference('namespaceId', 'namespace', 'clusterStore'), node: reference('nodeId', 'node', 'globalStore'), workload: reference('workloadId'), diff --git a/app/models/project.js b/app/models/project.js index 59f2c9865..0c60c5dc4 100644 --- a/app/models/project.js +++ b/app/models/project.js @@ -47,6 +47,21 @@ export default Resource.extend({ return labels[SYSTEM_PROJECT_LABEL] === 'true'; }), + isMonitoringReady: computed('monitoringStatus.@each.conditions', function() { + if ( !get(this, 'enableProjectMonitoring') ) { + return false; + } + const conditions = get(this, 'monitoringStatus.conditions') || []; + + if ( get(conditions, 'length') > 0 ) { + const ready = conditions.filterBy('status', 'True') || [] ; + + return get(ready, 'length') === get(conditions, 'length'); + } + + return false; + }), + active: computed('scope.currentProject.id', 'id', function() { return get(this, 'scope.currentProject.id') === get(this, 'id'); }), diff --git a/app/models/service.js b/app/models/service.js index f86af07b3..c868dc833 100644 --- a/app/models/service.js +++ b/app/models/service.js @@ -32,7 +32,7 @@ export default Resource.extend({ let linkEndpoint = `${ location.origin }/k8s/clusters/${ get(this, 'scope.currentCluster.id') }/api/v1/namespaces/${ get(this, 'namespaceId') }/services/`; if ( get(port, 'name') === 'http' || get(port, 'name') === 'https' ) { - linkEndpoint = `${ get(port, 'name') }:`; + linkEndpoint += `${ get(port, 'name') }:`; } linkEndpoint += `${ get(this, 'name') }:${ get(port, 'port') }/proxy/`; diff --git a/app/models/workload.js b/app/models/workload.js index 5fbd83312..b05c341c2 100644 --- a/app/models/workload.js +++ b/app/models/workload.js @@ -1,5 +1,6 @@ import { later, cancel } from '@ember/runloop'; import { computed, get, set } from '@ember/object'; +import Grafana from 'shared/mixins/grafana'; import { alias, gt, not } from '@ember/object/computed'; import Resource from 'ember-api-store/models/resource'; import { sortableNumericSuffix } from 'shared/utils/util'; @@ -13,7 +14,7 @@ import C from 'shared/utils/constants'; const WORKLOAD_CONFIG_FIELDS = ['cronJobConfig', 'daemonSetConfig', 'deploymentConfig', 'jobConfig', 'replicaSetConfig', 'replicationControllerConfig', 'statefulSetConfig'] -var Workload = Resource.extend(DisplayImage, StateCounts, EndpointPorts, { +var Workload = Resource.extend(Grafana, DisplayImage, StateCounts, EndpointPorts, { intl: service(), growl: service(), modalService: service('modal'), @@ -37,8 +38,9 @@ var Workload = Resource.extend(DisplayImage, StateCounts, EndpointPorts, { canHaveEnvironment: true, canHaveHealthCheck: true, isBalancer: false, + canBalanceTo: true, - canBalanceTo: true, + grafanaResourceId: alias('name'), namespace: reference('namespaceId', 'namespace', 'clusterStore'), canClone: not('hasSidekicks'), diff --git a/app/node/template.hbs b/app/node/template.hbs deleted file mode 100644 index e85616306..000000000 --- a/app/node/template.hbs +++ /dev/null @@ -1,100 +0,0 @@ -
-
-

- {{t 'hostsPage.hostPage.header.title' name=model.node.displayName}} -

-
-
- {{badge-state model=model.node}} - {{action-menu model=model.node showPrimary=false classNames="ml-10 inline-block" size="sm"}} -
-
- -{{#if model.node.description}} - {{banner-message color='bg-secondary mb-0 mt-10' message=(linkify model.node.description)}} -{{/if}} - -
- - -
- -{{node-gauges nodes=model.nodes}} - -{{node-conditions conditionsSource=model.node.conditions}} - -
- {{#accordion-list as |al expandFn|}} - {{resource-condition-list - resourceType=(t 'generic.node') - conditions=model.node.conditions - expandAll=al.expandAll - expandFn=expandFn - }} -
- {{system-info-section - node=model.node - expandAll=al.expandAll - expandFn=expandFn - }} -
-
- {{labels-section - model=model.node - showKind=false - expandAll=al.expandAll - expandFn=expandFn - }} -
-
- {{annotations-section - model=model.node - expandAll=al.expandAll - expandFn=expandFn - }} -
-
- {{taints-section - taints=model.node.taints - expandAll=al.expandAll - expandFn=expandFn - }} -
- {{/accordion-list}} -
diff --git a/app/pod/controller.js b/app/pod/controller.js new file mode 100644 index 000000000..740f427ec --- /dev/null +++ b/app/pod/controller.js @@ -0,0 +1,23 @@ +import { inject as service } from '@ember/service'; +import Controller from '@ember/controller'; +import { get, observer } from '@ember/object'; +import { alias } from '@ember/object/computed'; +import C from 'ui/utils/constants'; + +export default Controller.extend({ + router: service(), + scope: service(), + monitoringEnabled: alias('scope.currentCluster.isMonitoringReady'), + + podStateDidChange: observer('model.state', function() { + if ( C.REMOVEDISH_STATES.includes(get(this, 'model.state')) && get(this, 'router.currentRouteName') === 'pod' ) { + const workloadId = get(this, 'model.workloadId'); + + if ( workloadId ) { + this.transitionToRoute('workload', workloadId); + } else { + this.transitionToRoute('authenticated.project.index'); + } + } + }), +}); diff --git a/app/pod/route.js b/app/pod/route.js new file mode 100644 index 000000000..500996147 --- /dev/null +++ b/app/pod/route.js @@ -0,0 +1,25 @@ +import { get } from '@ember/object'; +import Route from '@ember/routing/route'; + +export default Route.extend({ + beforeModel() { + if (window.ShellQuote) { + return; + } else { + return import('shell-quote').then( (module) => { + window.ShellQuote = module.default; + + return module.default; + }) + } + }, + model(params) { + const pod = get(this, 'store').find('pod', params.pod_id); + + if ( !pod ) { + this.replaceWith('authenticated.project.index'); + } + + return pod; + }, +}); diff --git a/app/pod/template.hbs b/app/pod/template.hbs new file mode 100644 index 000000000..695c62bc8 --- /dev/null +++ b/app/pod/template.hbs @@ -0,0 +1,137 @@ +
+
+

+ {{t 'podPage.header' name=model.displayName}} +

+
+
+ {{badge-state model=model}} + {{action-menu model=model showPrimary=false classNames="ml-10 inline-block" size="sm"}} +
+
+{{#if model.description}} + {{banner-message color='bg-secondary mb-0 mt-10' message=(linkify model.description)}} +{{/if}} + +{{#if model.showTransitioningMessage}} +

{{uc-first model.transitioningMessage}}

+{{/if}} + + + + + +{{#accordion-list as |al expandFn|}} + {{accordion-container + containers=model.containers + expandAll=al.expandAll + expandFn=expandFn + }} + + {{#if scope.currentCluster.isMonitoringReady}} + {{#metrics-summary + expandAll=al.expandAll + expandFn=expandFn + grafanaUrl=model.grafanaUrl + title=(t "metricsAction.sections.pod")}} + {{pod-metrics resourceId=model.id}} + {{/metrics-summary}} + {{/if}} + + {{resource-event-list + resourceType=(t 'generic.pod') + expandAll=al.expandAll + expandFn=expandFn + namespaceId=model.namespaceId + name=model.name + kind="Pod" + }} + + {{resource-condition-list + resourceType=(t 'generic.pod') + conditions=model.status.conditions + expandAll=al.expandAll + expandFn=expandFn + }} + + {{#if model.workload}} + {{container/form-scheduling + initialHostId=model.workload.nodeId + scheduling=model.workload.scheduling + editing=false + expandAll=al.expandAll + expandFn=expandFn + classNames="accordion" + }} + {{/if}} + + {{container/form-networking + classNames="accordion-wrapper" + service=model + editing=false + expandAll=al.expandAll + expandFn=expandFn + }} + + {{form-labels-annotations + classNames="accordion-wrapper" + model=model + editing=false + expandAll=al.expandAll + expandFn=expandFn + }} +{{/accordion-list}} \ No newline at end of file diff --git a/app/router.js b/app/router.js index dd95548b9..1d37c3ed9 100644 --- a/app/router.js +++ b/app/router.js @@ -59,12 +59,10 @@ Router.map(function() { this.route('nodes', function() { this.route('index', { path: '/' }); - this.route('node', { - path: '/:node_id', - resetNamespace: true - }); }); + this.mount('monitoring'); + this.route('projects', { path: '/projects-namespaces' }, function() { this.route('index', { path: '/' }); this.route('edit', { path: '/project/:project_id' }); @@ -122,6 +120,7 @@ Router.map(function() { this.mount('alert', { path: '/alerts' }); this.mount('pipeline'); + this.mount('monitoring'); // Workload this.route('containers', { @@ -131,10 +130,15 @@ Router.map(function() { this.route('run', { path: '/run' }); this.route('index', { path: '/' }); - this.route('container', { - path: '/:container_id', + this.route('pod', { + path: '/:pod_id', resetNamespace: true }); + + this.route('container', { + path: '/:pod_id/container/:container_name', + resetNamespace: true + }) }); this.route('ingresses', { resetNamespace: true }, function() { diff --git a/app/styles/_rancher.scss b/app/styles/_rancher.scss index 25990059d..f0865716b 100644 --- a/app/styles/_rancher.scss +++ b/app/styles/_rancher.scss @@ -29,15 +29,13 @@ @import "app/styles/components/slider"; @import "app/styles/components/badges"; @import "app/styles/components/badge-state"; -@import "app/styles/components/component-badge"; @import "app/styles/components/tables"; @import "app/styles/components/growl"; @import "app/styles/components/login"; @import "app/styles/components/pod"; +@import "app/styles/components/metrics"; @import "app/styles/components/github-avatar"; @import "app/styles/components/spark-line"; -@import "app/styles/components/graph-area"; -@import "app/styles/components/percent-gauge"; @import "app/styles/components/tooltip"; @import "app/styles/components/container-shell"; @import "app/styles/components/modals"; @@ -52,6 +50,7 @@ @import "app/styles/components/pagination"; @import "app/styles/components/theme-toggle"; @import "app/styles/components/progress"; +@import "app/styles/components/percent-gauge"; @import "app/styles/components/namespace-app"; @import "app/styles/components/page-header"; @import "app/styles/components/over-hr"; diff --git a/app/styles/base/_base.scss b/app/styles/base/_base.scss index bf121a89d..97777e11d 100755 --- a/app/styles/base/_base.scss +++ b/app/styles/base/_base.scss @@ -91,3 +91,7 @@ hr { border-top: 1px solid $border; border-bottom: 1px solid $border; } + +.flatpickr-months .flatpickr-month { + height: 36px !important; +} \ No newline at end of file diff --git a/app/styles/base/_helpers.scss b/app/styles/base/_helpers.scss index 10c90635f..2fccf194f 100755 --- a/app/styles/base/_helpers.scss +++ b/app/styles/base/_helpers.scss @@ -289,3 +289,15 @@ .link[disabled] { pointer-events: none; } + +a.disabled { + cursor: default; + transition: ease-in-out all .25s; +} + +.sticky { + position: sticky; + top: 0px; + display:inline-block; + vertical-align: top; +} \ No newline at end of file diff --git a/app/styles/components/_button.scss b/app/styles/components/_button.scss index 6149bffb9..011a51b14 100755 --- a/app/styles/components/_button.scss +++ b/app/styles/components/_button.scss @@ -230,4 +230,4 @@ fieldset[disabled] .btn { .copy-btn.small { color: inherit; -} +} \ No newline at end of file diff --git a/app/styles/components/_component-badge.scss b/app/styles/components/_component-badge.scss deleted file mode 100644 index 2b881e346..000000000 --- a/app/styles/components/_component-badge.scss +++ /dev/null @@ -1,23 +0,0 @@ -.component-badge { - .healthy.circle{ - fill: $healthy-circle-color; - } - .healthy.background{ - fill: $healthy-background-color; - } - .healthy.text{ - fill: $healthy-text-color; - stroke: $healthy-text-color; - } - - .unhealthy.circle{ - fill: $unhealthy-circle-color; - } - .unhealthy.background{ - fill: $unhealthy-background-color; - } - .unhealthy.text{ - fill: $unhealthy-text-color; - stroke: $unhealthy-text-color; - } -} diff --git a/app/styles/components/_graph-area.scss b/app/styles/components/_graph-area.scss deleted file mode 100644 index 4057b3558..000000000 --- a/app/styles/components/_graph-area.scss +++ /dev/null @@ -1,82 +0,0 @@ -.graph-area { - font-size: 12px; - - .line { - fill: none; - stroke: $link-color; - stroke-width: 2px; - } - - .axis { - fill: #9da0ae; - } - - .axis line, - .axis path { - fill: none; - } - - .odd { - fill: $mid-grey; - fill-opacity: .1; - } - - .even { - fill: $dark-grey; - fill-opacity: .1; - } - - .area { - fill-opacity: .7; - } - - .hover-line { - stroke: #9da0ae; - } - - .hover-label { - position: absolute; - pointer-events: none; - top: 0px; - z-index: 20; - } - - .graph-area-tooltip { - border-collapse: collapse; - border-spacing: 0; - background-color: #fff; - empty-cells: show; - box-shadow: 7px 7px 12px -9px #777; - } - - .graph-area-tooltip th { - background-color: #aaa; - font-size: 14px; - padding: 2px 5px; - text-align: left; - color: #FFF; - } - - .graph-area-tooltip tr { - border: 1px solid #CCC; - } - - .graph-area-tooltip td { - font-size: 13px; - height: 25px; - padding: 3px 6px; - background-color: #fff; - border-left: 1px dotted #999; - } - - .graph-area-tooltip td > span { - display: inline-block; - width: 10px; - height: 10px; - margin-right: 6px; - } - - .graph-area-tooltip td.value { - text-align: right; - } -} diff --git a/app/styles/components/_metrics.scss b/app/styles/components/_metrics.scss new file mode 100644 index 000000000..2d9078b4d --- /dev/null +++ b/app/styles/components/_metrics.scss @@ -0,0 +1,51 @@ +$accordion-border: darken($bg-default, 5) !default; + +.graph-area { + height: 200px +} + +.grafana-link { + a:hover { + text-decoration: none; + } + margin: 10px; +} + +.metrics-summary { + .metrics-accordion { + @include no-select; + @extend .clearfix; + background: transparent; + border: solid $accordion-border thin; + display: table; + vertical-align: middle; + width: 100%; + + .title { + display: table-cell; + height: 100%; + vertical-align: middle; + background: transparent; + text-align: left; + } + + div { + &:focus { + outline: 0 !important; + } + } + + .btn { + &:focus { + outline: 0 !important; + } + padding: 15px 25px; + } + } + .metrics-accordion-content { + position: relative; + padding: 20px; + border: solid 1px $border; + border-top: 0; + } +} \ No newline at end of file diff --git a/app/styles/components/_nav-boxes.scss b/app/styles/components/_nav-boxes.scss index e63d73bbb..25d4d6efd 100644 --- a/app/styles/components/_nav-boxes.scss +++ b/app/styles/components/_nav-boxes.scss @@ -81,6 +81,10 @@ .elasticsearch { background-image: url('images/providers/elasticsearch.svg'); } + .prometheus { + background-image: url('images/providers/prometheus.svg'); + background-size: 80px 80px; + } .splunk { background-image: url('images/providers/splunk.svg'); } diff --git a/app/styles/components/_progress.scss b/app/styles/components/_progress.scss index 143ef23f1..a0a00e441 100644 --- a/app/styles/components/_progress.scss +++ b/app/styles/components/_progress.scss @@ -36,3 +36,7 @@ $progress-height : 10px !default; background-color: lighten($success, 50); border: solid lighten($success, 30) 1px; } + +.progress-bar-primary { + background-color: $primary; +} \ No newline at end of file diff --git a/app/styles/pages/_settings.scss b/app/styles/pages/_settings.scss index a9cc29cf1..b1792360f 100644 --- a/app/styles/pages/_settings.scss +++ b/app/styles/pages/_settings.scss @@ -108,3 +108,10 @@ overflow-y: scroll; margin-bottom: 15px; } + +.grafana { + background-image: url('images/providers/grafana.svg'); + background-size: 20px; + width: 20px; + height: 20px; +} \ No newline at end of file diff --git a/app/templates/tooltip-container-dot.hbs b/app/templates/tooltip-container-dot.hbs index 8e5038dca..151a286c8 100644 --- a/app/templates/tooltip-container-dot.hbs +++ b/app/templates/tooltip-container-dot.hbs @@ -1,7 +1,7 @@
{{action-menu model=model showPrimary=false inTooltip=true class="pull-right tooltip-more-actions"}} - +
{{badge-state classNames="btn-xs" model=model}}
diff --git a/app/workload/controller.js b/app/workload/controller.js index 0f3614e3a..7d960618f 100644 --- a/app/workload/controller.js +++ b/app/workload/controller.js @@ -2,11 +2,15 @@ import { computed } from '@ember/object'; import { alias } from '@ember/object/computed'; import Controller from '@ember/controller'; import { get } from '@ember/object'; +import { inject as service } from '@ember/service'; export default Controller.extend({ + scope: service(), + launchConfig: null, - service: alias('model.workload'), + service: alias('model.workload'), + monitoringEnabled: alias('scope.currentCluster.isMonitoringReady'), displayEnvironmentVars: computed('service.launchConfig.environment', function() { var envs = []; diff --git a/app/workload/template.hbs b/app/workload/template.hbs index 87141944c..c7b5c42fa 100644 --- a/app/workload/template.hbs +++ b/app/workload/template.hbs @@ -3,11 +3,6 @@

{{t 'servicePage.header' name=service.displayName}}

- {{#if (and false service.canHaveContainers (not service.isSelector))}} -
- {{info-multi-stats model=service largeTargetId="largeStats"}} -
- {{/if}}
{{badge-state model=service}} @@ -15,9 +10,6 @@
-
-
- {{#if service.description}} {{banner-message color='bg-secondary mb-0 mt-10' message=(linkify service.description)}} {{/if}} @@ -26,211 +18,225 @@

{{uc-first service.transitioningMessage}}

{{/if}} -
-
+
-
- {{#accordion-list as |al expandFn|}} - {{accordion-pod - pods=service.pods +{{#accordion-list as |al expandFn|}} + {{accordion-pod + pods=service.pods + expandAll=al.expandAll + expandFn=expandFn + }} + + {{#if scope.currentCluster.isMonitoringReady}} + {{#metrics-summary expandAll=al.expandAll expandFn=expandFn + grafanaUrl=service.grafanaUrl + title=(t "metricsAction.sections.workload") }} + {{workload-metrics resourceId=service.id}} + {{/metrics-summary}} + {{/if}} - {{resource-event-list - resourceType=service.displayType - expandAll=al.expandAll - expandFn=expandFn - namespaceId=service.namespaceId - name=service.name - kind=service.type - }} + {{resource-event-list + resourceType=service.displayType + expandAll=al.expandAll + expandFn=expandFn + namespaceId=service.namespaceId + name=service.name + kind=service.type + }} - {{#if (or (eq service.type 'job') (eq service.type 'cronJob'))}} - {{container/form-job-config - workload=service - scaleMode=service.type - editing=false - expandAll=al.expandAll - expandFn=expandFn - }} - {{/if}} - - {{#if service.canHaveEnvironment}} - {{#accordion-list-item - title=(t 'formEnvVar.title') - detail=(t 'formEnvVar.detail') - expandAll=al.expandAll - expand=(action expandFn) - }} - {{form-env-var - model=displayEnvironmentVars - }} -
- {{container/form-sources - namespace=service.namespace - sources=launchConfig.environmentFrom - editing=false - }} - {{/accordion-list-item}} - {{/if}} - - {{#accordion-list-item - title=(t 'containerPage.portsTab.header') - detail=(t 'containerPage.portsTab.detail') - expandAll=al.expandAll - expand=(action expandFn) - }} - {{container/form-ports - initialPorts=service.launchConfig.ports - editing=false - }} - {{/accordion-list-item}} - - {{container/form-scheduling - initialHostId=model.workload.nodeId - service=service - scheduling=service.scheduling - editing=false - expandAll=al.expandAll - expandFn=expandFn - classNames="accordion" - }} - - {{#if service.canHaveHealthCheck}} - {{#accordion-list-item - title=(t 'formHealthCheck.title') - detail=(t 'formHealthCheck.detail') - expandAll=al.expandAll - expand=(action expandFn) - }} -
-
- -
-
- {{#if service.launchConfig.livenessProbe}} - - {{/if}} -
-
-
-
- {{form-healthcheck - initialCheck=service.launchConfig.readinessProbe - editing=false - }} -
- {{#if service.launchConfig.livenessProbe}} -
- {{form-healthcheck - initialCheck=service.launchConfig.livenessProbe - editing=false - isLiveness=true - }} -
- {{/if}} -
- {{/accordion-list-item}} - {{/if}} - - {{container/form-upgrade + {{#if (or (eq service.type 'job') (eq service.type 'cronJob'))}} + {{container/form-job-config workload=service scaleMode=service.type editing=false expandAll=al.expandAll expandFn=expandFn }} + {{/if}} - {{container/form-volumes - launchConfig=launchConfig - workload=service - namespace=service.namespace - loggingEnabled=true - editing=false + {{#if service.canHaveEnvironment}} + {{#accordion-list-item + title=(t 'formEnvVar.title') + detail=(t 'formEnvVar.detail') expandAll=al.expandAll - expandFn=expandFn + expand=(action expandFn) }} - - {{container/form-command - tagName='' - instance=launchConfig - service=model.workload - initialLabels=launchConfig.labels - editing=false - expandAll=al.expandAll - expandFn=expandFn - }} - - {{container/form-networking - classNames="accordion-wrapper" - instance=launchConfig - service=service - editing=false - expandAll=al.expandAll - expandFn=expandFn - }} - - {{form-labels-annotations - classNames="accordion-wrapper" - model=service - editing=false - expandAll=al.expandAll - expandFn=expandFn - }} - - {{#if service.canChangeSecurity}} - {{container/form-security - instance=launchConfig - editing=false - expandAll=al.expandAll - expandFn=expandFn + {{form-env-var + model=displayEnvironmentVars }} - {{/if}} +
+ {{container/form-sources + namespace=service.namespace + sources=launchConfig.environmentFrom + editing=false + }} + {{/accordion-list-item}} + {{/if}} - {{/accordion-list}} -
+ {{#accordion-list-item + title=(t 'containerPage.portsTab.header') + detail=(t 'containerPage.portsTab.detail') + expandAll=al.expandAll + expand=(action expandFn) + }} + {{container/form-ports + initialPorts=service.launchConfig.ports + editing=false + }} + {{/accordion-list-item}} + + {{container/form-scheduling + initialHostId=model.workload.nodeId + service=service + scheduling=service.scheduling + editing=false + expandAll=al.expandAll + expandFn=expandFn + classNames="accordion" + }} + + {{#if service.canHaveHealthCheck}} + {{#accordion-list-item + title=(t 'formHealthCheck.title') + detail=(t 'formHealthCheck.detail') + expandAll=al.expandAll + expand=(action expandFn) + }} +
+
+ +
+
+ {{#if service.launchConfig.livenessProbe}} + + {{/if}} +
+
+
+
+ {{form-healthcheck + initialCheck=service.launchConfig.readinessProbe + editing=false + }} +
+ {{#if service.launchConfig.livenessProbe}} +
+ {{form-healthcheck + initialCheck=service.launchConfig.livenessProbe + editing=false + isLiveness=true + }} +
+ {{/if}} +
+ {{/accordion-list-item}} + {{/if}} + + {{container/form-upgrade + workload=service + scaleMode=service.type + editing=false + expandAll=al.expandAll + expandFn=expandFn + }} + + {{container/form-volumes + launchConfig=launchConfig + workload=service + namespace=service.namespace + loggingEnabled=true + editing=false + expandAll=al.expandAll + expandFn=expandFn + }} + + {{container/form-command + tagName='' + instance=launchConfig + service=model.workload + initialLabels=launchConfig.labels + editing=false + expandAll=al.expandAll + expandFn=expandFn + }} + + {{container/form-networking + classNames="accordion-wrapper" + instance=launchConfig + service=service + editing=false + expandAll=al.expandAll + expandFn=expandFn + }} + + {{form-labels-annotations + classNames="accordion-wrapper" + model=service + editing=false + expandAll=al.expandAll + expandFn=expandFn + }} + + {{#if service.canChangeSecurity}} + {{container/form-security + instance=launchConfig + editing=false + expandAll=al.expandAll + expandFn=expandFn + }} + {{/if}} + + {{#if scope.currentCluster.enableClusterMonitoring}} + {{container/form-custom-metrics + classNames="accordion-wrapper" + workload=service + editing=false + expandAll=al.expandAll + expandFn=expandFn + }} + {{/if}} + +{{/accordion-list}} \ No newline at end of file diff --git a/lib/global-admin/addon/catalog/controller.js b/lib/global-admin/addon/catalog/controller.js index 392537ebb..776027cc9 100644 --- a/lib/global-admin/addon/catalog/controller.js +++ b/lib/global-admin/addon/catalog/controller.js @@ -174,11 +174,19 @@ export default Controller.extend({ ); }), + systemLibrary: computed('model.@each.{name,url,branch}', function() { + return this.findMatch( + C.CATALOG.SYSTEM_LIBRARY_KEY, + C.CATALOG.SYSTEM_LIBRARY_VALUE, + ); + }), + custom: computed('library', 'helmStable', 'helmIncubator', function() { const hide = [ get(this, 'library'), get(this, 'helmStable'), - get(this, 'helmIncubator') + get(this, 'helmIncubator'), + get(this, 'systemLibrary'), ]; return get(this, 'model').filter((x) => !hide.includes(x)); diff --git a/lib/global-admin/addon/engine.js b/lib/global-admin/addon/engine.js index 93ad6cbe0..72b7453bb 100644 --- a/lib/global-admin/addon/engine.js +++ b/lib/global-admin/addon/engine.js @@ -39,7 +39,6 @@ const Eng = Engine.extend({ 'authenticated.project', 'authenticated.prefs', 'authenticated.cluster.nodes', - 'authenticated.cluster.nodes.node', 'authenticated.cluster.security.members.index', 'logout' ] diff --git a/lib/monitoring/addon/cluster-setting/route.js b/lib/monitoring/addon/cluster-setting/route.js new file mode 100644 index 000000000..87e547fa3 --- /dev/null +++ b/lib/monitoring/addon/cluster-setting/route.js @@ -0,0 +1,13 @@ +import Route from '@ember/routing/route'; +import { set } from '@ember/object'; +import { inject as service } from '@ember/service'; +import { on } from '@ember/object/evented'; +import C from 'ui/utils/constants'; + +export default Route.extend({ + session: service(), + + setDefaultRoute: on('activate', function() { + set(this, `session.${ C.SESSION.CLUSTER_ROUTE }`, 'authenticated.cluster.monitoring.cluster-setting'); + }), +}); diff --git a/lib/monitoring/addon/cluster-setting/template.hbs b/lib/monitoring/addon/cluster-setting/template.hbs new file mode 100644 index 000000000..394f5b3d2 --- /dev/null +++ b/lib/monitoring/addon/cluster-setting/template.hbs @@ -0,0 +1,4 @@ +
+

{{t 'monitoringPage.cluster.title'}}

+
+{{enable-monitoring}} \ No newline at end of file diff --git a/lib/monitoring/addon/components/cluster-basic-info/component.js b/lib/monitoring/addon/components/cluster-basic-info/component.js new file mode 100644 index 000000000..c6a0422a6 --- /dev/null +++ b/lib/monitoring/addon/components/cluster-basic-info/component.js @@ -0,0 +1,8 @@ +import Component from '@ember/component'; +import layout from './template'; + +export default Component.extend({ + layout, + + cluster: null, +}); \ No newline at end of file diff --git a/lib/monitoring/addon/components/cluster-basic-info/template.hbs b/lib/monitoring/addon/components/cluster-basic-info/template.hbs new file mode 100644 index 000000000..cbee9c736 --- /dev/null +++ b/lib/monitoring/addon/components/cluster-basic-info/template.hbs @@ -0,0 +1,39 @@ + + + diff --git a/lib/monitoring/addon/components/cluster-dashboard-tabs/component.js b/lib/monitoring/addon/components/cluster-dashboard-tabs/component.js new file mode 100644 index 000000000..e42c4d39d --- /dev/null +++ b/lib/monitoring/addon/components/cluster-dashboard-tabs/component.js @@ -0,0 +1,12 @@ +import Component from '@ember/component'; +import layout from './template'; +import { inject as service } from '@ember/service'; + +export default Component.extend({ + scope: service(), + settings: service(), + grafana: service(), + + layout, + classNames: 'row', +}); diff --git a/lib/monitoring/addon/components/cluster-dashboard-tabs/template.hbs b/lib/monitoring/addon/components/cluster-dashboard-tabs/template.hbs new file mode 100644 index 000000000..ba5491cbb --- /dev/null +++ b/lib/monitoring/addon/components/cluster-dashboard-tabs/template.hbs @@ -0,0 +1,42 @@ +{{#accordion-list as |al expandFn|}} + {{#metrics-summary + expandAll=al.expandAll + expandFn=expandFn + grafanaUrl=scope.currentCluster.grafanaUrl + title=(t "clusterDashboard.sections.cluster") + }} + {{cluster-metrics}} + {{/metrics-summary}} + + {{#if scope.currentCluster.isRKE}} + {{#metrics-summary + expandAll=al.expandAll + expandFn=expandFn + grafanaUrl=grafana.etcdUrl + classNames='mt-20' + title=(t "clusterDashboard.sections.etcd") + }} + {{etcd-metrics}} + {{/metrics-summary}} + {{/if}} + + {{#metrics-summary + expandAll=al.expandAll + expandFn=expandFn + grafanaUrl=grafana.k8sUrl + classNames='mt-20' + title=(t "clusterDashboard.sections.k8s") + }} + {{k8s-metrics}} + {{/metrics-summary}} + + {{#metrics-summary + expandAll=al.expandAll + expandFn=expandFn + grafanaUrl=grafana.rancherUrl + classNames='mt-20 mb-20' + title=(t "clusterDashboard.sections.rancher" appName=settings.appName) + }} + {{rancher-metrics noDataLabel='clusterDashboard.noRancherComponents'}} + {{/metrics-summary}} +{{/accordion-list}} \ No newline at end of file diff --git a/lib/monitoring/addon/components/cluster-dashboard/component.js b/lib/monitoring/addon/components/cluster-dashboard/component.js new file mode 100644 index 000000000..77d32b750 --- /dev/null +++ b/lib/monitoring/addon/components/cluster-dashboard/component.js @@ -0,0 +1,56 @@ +import C from 'ui/utils/constants'; +import { on } from '@ember/object/evented'; +import Component from '@ember/component'; +import { get, computed, observer, setProperties } from '@ember/object'; +import { alias } from '@ember/object/computed'; +import { inject as service } from '@ember/service'; +import layout from './template'; + +export default Component.extend({ + intl: service(), + scope: service(), + grafana: service(), + globalStore: service(), + router: service(), + + layout, + + nodes: null, + components: null, + monitoringEnabled: alias('scope.currentCluster.enableClusterMonitoring'), + componentStatuses: alias('scope.currentCluster.componentStatuses'), + + actions: { + enalbeMonitoring() { + get(this, 'router').transitionTo('authenticated.cluster.monitoring.cluster-setting'); + }, + }, + + setComponents: on('init', observer('componentStatuses.@each.conditions', 'nodes.@each.{state}', function() { + setProperties(this, { + etcdHealthy: this.isHealthy('etcd'), + controllerHealthy: this.isHealthy('controller-manager'), + schedulerHealthy: this.isHealthy('scheduler'), + nodesHealthy: get(this, 'inactiveNodes.length') === 0 + }) + })), + + showDashboard: computed('scope.currentCluster.isReady', 'nodes.[]', function() { + return get(this, 'nodes').length && get(this, 'scope.currentCluster.isReady') + }), + + inactiveNodes: computed('nodes.@each.state', function() { + return get(this, 'nodes').filter( (n) => C.ACTIVEISH_STATES.indexOf(get(n, 'state')) === -1 ); + }), + + unhealthyComponents: computed('componentStatuses.@each.conditions', function() { + return (get(this, 'componentStatuses') || []) + .filter((s) => !s.conditions.any((c) => c.status === 'True')); + }), + + isHealthy(field) { + return (get(this, 'componentStatuses') || []) + .filter((s) => s.name.startsWith(field)) + .any((s) => s.conditions.any((c) => c.status === 'True')); + } +}); diff --git a/lib/monitoring/addon/components/cluster-dashboard/template.hbs b/lib/monitoring/addon/components/cluster-dashboard/template.hbs new file mode 100644 index 000000000..80f8cf169 --- /dev/null +++ b/lib/monitoring/addon/components/cluster-dashboard/template.hbs @@ -0,0 +1,54 @@ +{{#if showDashboard}} + {{cluster-basic-info cluster=cluster}} + + {{#unless scope.currentCluster.isMonitoringReady}} + {{#if monitoringEnabled}} +
+
+ {{t 'clusterDashboard.monitoringNotReady'}} +
+
+ {{else}} +
+
+ +
+
+ {{/if}} + {{/unless}} + + {{nodes-reservation nodes=nodes}} + +
+ {{k8s-component-status label='clusterDashboard.etcd' healthy=etcdHealthy url=grafana.etcdUrl}} + {{k8s-component-status label='clusterDashboard.controllerManager' healthy=controllerHealthy url=grafana.controllerUrl}} + {{k8s-component-status label='clusterDashboard.scheduler' healthy=schedulerHealthy url=grafana.schedulerUrl}} + {{k8s-component-status label='clusterDashboard.node' healthy=nodesHealthy url=grafana.nodesUrl}} +
+ +
+ {{#each unhealthyComponents as |component|}} + {{#banner-message icon='icon-alert' color='text-left bg-error mt-30'}} +

{{t 'clusterDashboard.alert.component' component=component.name}}

+ {{/banner-message}} + {{/each}} + {{#each inactiveNodes as |node|}} + {{#banner-message icon='icon-alert' color='text-left bg-error mt-30'}} +

{{t 'clusterDashboard.alert.node' node=node.displayName}}

+ {{/banner-message}} + {{/each}} +
+ + {{#if scope.currentCluster.isMonitoringReady}} + {{cluster-dashboard-tabs dashboards=dashboards}} + {{/if}} +{{else}} + {{empty-table + resource="container" + showNew=scope.currentCluster.canAddNode + newRoute="authenticated.cluster.nodes.templates" + newTranslationKey="nodesPage.addNode" + disabled=(rbac-prevents resource="machine" scope="global" permission="create") + ctx="" + }} +{{/if}} diff --git a/lib/monitoring/addon/components/cluster-metrics/component.js b/lib/monitoring/addon/components/cluster-metrics/component.js new file mode 100644 index 000000000..b7fb85d62 --- /dev/null +++ b/lib/monitoring/addon/components/cluster-metrics/component.js @@ -0,0 +1,9 @@ +import Component from '@ember/component'; +import Metrics from 'shared/mixins/metrics'; +import layout from './template'; + +export default Component.extend(Metrics, { + layout, + + filters: { resourceType: 'cluster' }, +}); diff --git a/lib/monitoring/addon/components/cluster-metrics/template.hbs b/lib/monitoring/addon/components/cluster-metrics/template.hbs new file mode 100644 index 000000000..0276915b9 --- /dev/null +++ b/lib/monitoring/addon/components/cluster-metrics/template.hbs @@ -0,0 +1,7 @@ +
+ {{metrics-action + queryAction="query" + state=state + }} + {{metrics-graph graphs=graphs loading=state.loading noGraphs=state.noGraphs}} +
\ No newline at end of file diff --git a/lib/monitoring/addon/components/enable-monitoring/component.js b/lib/monitoring/addon/components/enable-monitoring/component.js new file mode 100644 index 000000000..3ce21ec0a --- /dev/null +++ b/lib/monitoring/addon/components/enable-monitoring/component.js @@ -0,0 +1,134 @@ +import { inject as service } from '@ember/service'; +import Component from '@ember/component'; +import { set, get, computed } from '@ember/object'; +import { alias } from '@ember/object/computed'; +import layout from './template'; + +const PROMETHEUS = 'prometheus'; +const NONE = 'none'; + +export default Component.extend({ + scope: service(), + growl: service(), + + layout, + + selected: NONE, + level: 'cluster', + justDeployed: false, + enablePrometheusPersistence: false, + enableGrafanaPersistence: false, + prometheusPersistenceSize: '50Gi', + grafanaPersistenceSize: '10Gi', + prometheusStorageClass: null, + grafanaStorageClass: null, + nodeSelectors: null, + retention: 360, + port: 9100, + + cluster: alias('scope.currentCluster'), + project: alias('scope.currentProject'), + + init() { + this._super(...arguments); + + set(this, 'selected', get(this, 'enabled') ? PROMETHEUS : NONE); + }, + + actions: { + changeSelected(selected) { + set(this, 'selected', selected); + }, + + save(cb) { + if ( get(this, 'selected') === NONE ) { + this.send('disable', cb); + } else { + this.send('enable', cb); + } + }, + + enable(cb) { + const resource = get(this, 'level') === 'cluster' ? get(this, 'cluster') : get(this, 'project'); + + let answers; + + if ( get(this, 'level') === 'cluster' ) { + answers = { + '_key-Data_Retention': 'prometheus.retention', + '_key-Node_Exporter_Host_Port': 'exporter-node.ports.metrics.port', + '_key-Grafana_Storage_Enabled': 'grafana.persistence.enabled', + '_key-Grafana_Storage_Size': 'grafana.persistence.size', + '_key-Grafana_Storage_Class': 'grafana.persistence.storageClass', + '_key-Prometheus_Storage_Enabled': 'prometheus.persistence.enabled', + '_key-Prometheus_Storage_Size': 'prometheus.persistence.size', + '_key-Prometheus_Storage_Class': 'prometheus.persistence.storageClass', + '_tpl-Node_Selector': 'nodeSelector#(prometheus,grafana,exporter-kube-state)', + 'prometheus.retention': `${ get(this, 'retention') }h`, + 'exporter-node.ports.metrics.port': `${ get(this, 'port') }`, + 'grafana.persistence.enabled': `${ get(this, 'enableGrafanaPersistence') }`, + 'prometheus.persistence.enabled': `${ get(this, 'enablePrometheusPersistence') }`, + 'prometheus.persistence.storageClass': `${ get(this, 'prometheusStorageClass') === null ? 'default' : get(this, 'prometheusStorageClass') }`, + 'grafana.persistence.storageClass': `${ get(this, 'grafanaStorageClass') === null ? 'default' : get(this, 'grafanaStorageClass') }`, + 'grafana.persistence.size': `${ get(this, 'grafanaPersistenceSize') }`, + 'prometheus.persistence.size': `${ get(this, 'prometheusPersistenceSize') }`, + }; + } else { + answers = { + '_key-Grafana_Storage_Enabled': 'grafana.persistence.enabled', + '_key-Grafana_Storage_Size': 'grafana.persistence.size', + '_key-Grafana_Storage_Class': 'grafana.persistence.storageClass', + '_tpl-Node_Selector': 'nodeSelector#(grafana)', + 'grafana.persistence.enabled': `${ get(this, 'enableGrafanaPersistence') }`, + 'grafana.persistence.size': `${ get(this, 'grafanaPersistenceSize') }`, + 'grafana.persistence.storageClass': `${ get(this, 'grafanaStorageClass') === null ? 'default' : get(this, 'grafanaStorageClass') }`, + } + } + + (get(this, 'nodeSelectors') || []).forEach((selector) => { + answers[`nodeSelector.${ get(selector, 'key') }`] = get(selector, 'value'); + }); + resource.doAction('enableMonitoring', { answers }).then(() => { + set(this, 'justDeployed', true); + cb(true); + }).catch((err) => { + get(this, 'growl').fromError(err); + cb(false); + }); + }, + + disable(cb) { + const resource = get(this, 'level') === 'cluster' ? get(this, 'cluster') : get(this, 'project'); + + resource.doAction('disableMonitoring').then(() => { + cb(true); + }).catch((err) => { + get(this, 'growl').fromError(err); + cb(false); + }); + } + }, + + enabled: computed('cluster.enableClusterMonitoring', 'project.enableProjectMonitoring', 'level', function() { + return get(this, 'level') === 'cluster' ? get(this, 'cluster.enableClusterMonitoring') : get(this, 'project.enableProjectMonitoring'); + }), + + isReady: computed('cluster.isMonitoringReady', 'project.isMonitoringReady', 'level', function() { + return get(this, 'level') === 'cluster' ? get(this, 'cluster.isMonitoringReady') : get(this, 'project.isMonitoringReady'); + }), + + status: computed('isReady', function() { + if ( get(this, 'isReady') ) { + return 'ready'; + } else if ( get(this, 'justDeployed') ) { + return 'justDeployed' + } else { + return 'notReady' + } + }), + + saveDisabled: computed('selected', 'enabled', function() { + return (get(this, 'selected') === NONE && !get(this, 'enabled')) || (get(this, 'selected') === PROMETHEUS && get(this, 'enabled')); + }), + +}); diff --git a/lib/monitoring/addon/components/enable-monitoring/template.hbs b/lib/monitoring/addon/components/enable-monitoring/template.hbs new file mode 100644 index 000000000..952031f7f --- /dev/null +++ b/lib/monitoring/addon/components/enable-monitoring/template.hbs @@ -0,0 +1,131 @@ +{{#if enabled}} + {{#banner-message color=(if (or isReady justDeployed) 'bg-success' 'bg-error') icon=(if (or isReady justDeployed) 'icon-check' 'icon-alert')}} + {{#if (eq level 'cluster')}} +

+ {{t (concat 'monitoringPage.cluster.' status) htmlSafe=true}} {{cluster.systemProject.displayName}} {{t 'generic.project'}}. +

+ {{else}} +

+ {{t (concat 'monitoringPage.project.' status) htmlSafe=true}} +

+ {{/if}} + {{/banner-message}} +{{else}} + {{banner-message color='bg-info' icon='icon-info' message=(t (concat 'monitoringPage.' level '.info') htmlSafe=true)}} +{{/if}} + + + +
+
+ {{#if (eq selected 'none')}} + {{#if enabled}} + {{t (concat 'monitoringPage.' level '.toDisable')}} + {{else}} + {{t (concat 'monitoringPage.' level '.disabled')}} + {{/if}} + {{else if (eq selected 'prometheus')}} + {{#if enabled}} + {{t (concat 'monitoringPage.toUpdate.' level) htmlSafe=true}} + {{else}} + {{t 'monitoringPage.prometheus'}} + {{/if}} + {{/if}} +
+
+
+ +{{#if (and (eq selected 'prometheus') (not enabled))}} +

{{t 'monitoringPage.config.header'}}

+
+ +
+
+
+ +
+ {{input-integer min=0 value=retention}} + {{t 'generic.hours'}} +
+
+ {{#if (eq level 'cluster')}} +
+ + {{input-integer min=1 min=65535 value=port}} +
+ {{else}} +
+ + {{schema/input-boolean value=enableGrafanaPersistence}} +
+ {{/if}} +
+ + {{#if (eq level 'cluster')}} +
+
+ + {{schema/input-boolean value=enablePrometheusPersistence}} +
+
+ + {{schema/input-boolean value=enableGrafanaPersistence}} +
+
+ {{/if}} + {{#if enablePrometheusPersistence}} +
+
+ + {{schema/input-string value=prometheusPersistenceSize placeholder=(t 'monitoringPage.config.prometheus.size.placeholder')}} +
+
+ + {{schema/input-storageclass value=prometheusStorageClass}} +
+
+ {{/if}} + + {{#if enableGrafanaPersistence}} +
+
+ + {{schema/input-string value=grafanaPersistenceSize placeholder=(t 'monitoringPage.config.grafana.size.placeholder')}} +
+
+ + {{schema/input-storageclass value=grafanaStorageClass}} +
+
+ {{/if}} + +
+
+ + {{form-key-value + changedArray=(action (mut nodeSelectors)) + allowEmptyValue=true + addActionLabel="monitoringPage.nodeSelector.addSelectorLabel" + }} +
+
+
+{{/if}} + +
+ {{save-cancel + saveDisabled=saveDisabled + cancelDisabled=true + editing=true + save="save" + }} +
\ No newline at end of file diff --git a/lib/monitoring/addon/components/etcd-metrics/component.js b/lib/monitoring/addon/components/etcd-metrics/component.js new file mode 100644 index 000000000..8552850bf --- /dev/null +++ b/lib/monitoring/addon/components/etcd-metrics/component.js @@ -0,0 +1,31 @@ +import Component from '@ember/component'; +import Metrics from 'shared/mixins/metrics'; +import layout from './template'; +import { get, set, observer } from '@ember/object'; + +export default Component.extend(Metrics, { + layout, + + filters: { resourceType: 'etcd' }, + + singleStatsDidChange: observer('single.[]', function() { + const leaderChange = (get(this, 'single') || []).findBy('graph.title', 'etcd-leader-change'); + + if ( leaderChange ) { + set(this, 'leaderChange', get(leaderChange, 'series.firstObject.points.firstObject.firstObject')); + } + + const hasLeader = (get(this, 'single') || []).findBy('graph.title', 'etcd-server-leader-sum'); + + if ( hasLeader ) { + set(this, 'hasLeader', get(hasLeader, 'series.firstObject.points.firstObject.firstObject') === 1); + } + + const failedProposals = (get(this, 'single') || []).findBy('graph.title', 'etcd-server-failed-proposal'); + + if ( failedProposals ) { + set(this, 'failedProposals', get(failedProposals, 'series.firstObject.points.firstObject.firstObject')); + } + }) + +}); diff --git a/lib/monitoring/addon/components/etcd-metrics/template.hbs b/lib/monitoring/addon/components/etcd-metrics/template.hbs new file mode 100644 index 000000000..9a16dc253 --- /dev/null +++ b/lib/monitoring/addon/components/etcd-metrics/template.hbs @@ -0,0 +1,35 @@ +
+ {{metrics-action + queryAction="query" + state=state + }} + + {{#if single.length}} +
+
+ +
+
+ {{/if}} + + {{metrics-graph graphs=graphs loading=state.loading noGraphs=state.noGraphs}} +
\ No newline at end of file diff --git a/lib/monitoring/addon/components/ingress-response-list/component.js b/lib/monitoring/addon/components/ingress-response-list/component.js new file mode 100644 index 000000000..7be56e812 --- /dev/null +++ b/lib/monitoring/addon/components/ingress-response-list/component.js @@ -0,0 +1,30 @@ +import Component from '@ember/component'; +import layout from './template'; + +export default Component.extend({ + layout, + + data: null, + + sortBy: 'time', + descending: true, + labelArray: null, + + headers: [ + { + name: 'host', + sort: ['host'], + translationKey: 'ingressResponse.host', + }, + { + name: 'path', + sort: ['path', 'host'], + translationKey: 'ingressResponse.path', + }, + { + name: 'time', + sort: ['time', 'path', 'host'], + translationKey: 'ingressResponse.responseTime', + }, + ], +}); \ No newline at end of file diff --git a/lib/monitoring/addon/components/ingress-response-list/template.hbs b/lib/monitoring/addon/components/ingress-response-list/template.hbs new file mode 100644 index 000000000..c09a098af --- /dev/null +++ b/lib/monitoring/addon/components/ingress-response-list/template.hbs @@ -0,0 +1,39 @@ +{{#accordion-list-item + title=(t 'ingressResponse.title') + detail=(t 'ingressResponse.detail') + expandAll=expandAll + expand=(action expandFn) + expandOnInit=true + componentName='sortable-table' + as | parent | +}} + {{#component parent.intent + classNames="grid fixed mb-0 sortable-table" + bulkActions=false + rowActions=false + paging=false + search=true + sortBy=sortBy + stickyHeader=false + descending=descending + headers=headers + body=data + as |sortable kind inst| + }} + {{#if (eq kind "row")}} + + {{inst.host}} + {{inst.path}} + {{inst.time}} + + {{else if (eq kind "norows")}} + + {{t 'ingressResponse.noData'}} + + {{else if (eq kind "nomatch")}} + + {{t 'ingressResponse.noMatch'}} + + {{/if}} + {{/component}} +{{/accordion-list-item}} \ No newline at end of file diff --git a/lib/monitoring/addon/components/k8s-component-status/component.js b/lib/monitoring/addon/components/k8s-component-status/component.js new file mode 100644 index 000000000..c350a2787 --- /dev/null +++ b/lib/monitoring/addon/components/k8s-component-status/component.js @@ -0,0 +1,11 @@ +import Component from '@ember/component'; +import layout from './template'; + +export default Component.extend({ + layout, + + classNames: 'col span-3 mt-0', + label: null, + healthy: null, + url: null, +}); diff --git a/lib/monitoring/addon/components/k8s-component-status/template.hbs b/lib/monitoring/addon/components/k8s-component-status/template.hbs new file mode 100644 index 000000000..f508c5c4e --- /dev/null +++ b/lib/monitoring/addon/components/k8s-component-status/template.hbs @@ -0,0 +1,12 @@ +{{#banner-message icon=(if healthy 'icon-check' 'icon-alert') color=(if healthy 'bg-success mt-0' 'bg-error mt-0')}} +

+ {{t label}} + {{#if url}} + + +

+ + + {{/if}} +

+{{/banner-message}} \ No newline at end of file diff --git a/lib/monitoring/addon/components/k8s-metrics/component.js b/lib/monitoring/addon/components/k8s-metrics/component.js new file mode 100644 index 000000000..4f7b3e059 --- /dev/null +++ b/lib/monitoring/addon/components/k8s-metrics/component.js @@ -0,0 +1,37 @@ +import Component from '@ember/component'; +import Metrics from 'shared/mixins/metrics'; +import layout from './template'; +import { get, set, observer } from '@ember/object'; +import { formatSecond } from 'shared/utils/util'; + +export default Component.extend(Metrics, { + layout, + + filters: { displayResourceType: 'kube-component' }, + + singleStatsDidChange: observer('single.[]', function() { + const responseSeconds = (get(this, 'single') || []).findBy('graph.title', 'ingresscontroller-upstream-response-seconds'); + + if ( responseSeconds ) { + set(this, 'responseSeconds', (get(responseSeconds, 'series') || []) + .sortBy('series.firstObject.points.firstObject.firstObject').map((serie) => { + const name = get(serie, 'name') || ''; + const tokens = name.substring(24, get(name, 'length') - 1).split(' '); + let host; + let path; + + if ( get(tokens, 'length') === 2 ) { + host = tokens[0].substring(7, get(tokens[0], 'length')); + path = tokens[1].substring(5, get(tokens[1], 'length')); + } + + return { + host, + path, + time: formatSecond(get(serie, 'points.firstObject.firstObject')) + } + })); + } + }) + +}); diff --git a/lib/monitoring/addon/components/k8s-metrics/template.hbs b/lib/monitoring/addon/components/k8s-metrics/template.hbs new file mode 100644 index 000000000..d49e5f768 --- /dev/null +++ b/lib/monitoring/addon/components/k8s-metrics/template.hbs @@ -0,0 +1,20 @@ +
+ {{metrics-action + queryAction="query" + state=state + }} + + {{metrics-graph graphs=graphs loading=state.loading noGraphs=state.noGraphs}} + + {{#if single.length}} + {{#accordion-list as |al expandFn|}} +
+ {{ingress-response-list + expandAll=al.expandAll + expandFn=expandFn + data=responseSeconds + }} +
+ {{/accordion-list}} + {{/if}} +
\ No newline at end of file diff --git a/app/components/node-conditions/component.js b/lib/monitoring/addon/components/node-conditions/component.js similarity index 100% rename from app/components/node-conditions/component.js rename to lib/monitoring/addon/components/node-conditions/component.js diff --git a/lib/monitoring/addon/components/node-conditions/template.hbs b/lib/monitoring/addon/components/node-conditions/template.hbs new file mode 100644 index 000000000..102b6a633 --- /dev/null +++ b/lib/monitoring/addon/components/node-conditions/template.hbs @@ -0,0 +1,8 @@ +
+ {{#each conditions as |c|}} +
+ {{banner-message color=(if c.healthy 'bg-success' 'bg-error') icon='icon-check' message=c.name}} +
+ {{/each}} +
+ diff --git a/lib/monitoring/addon/components/node-dashboard/component.js b/lib/monitoring/addon/components/node-dashboard/component.js new file mode 100644 index 000000000..89e278870 --- /dev/null +++ b/lib/monitoring/addon/components/node-dashboard/component.js @@ -0,0 +1,12 @@ +import Component from '@ember/component'; +import { alias } from '@ember/object/computed'; +import { inject as service } from '@ember/service'; +import layout from './template'; + +export default Component.extend({ + scope: service(), + layout, + model: null, + + monitoringEnabled: alias('scope.currentCluster.enableClusterMonitoring'), +}); diff --git a/lib/monitoring/addon/components/node-dashboard/template.hbs b/lib/monitoring/addon/components/node-dashboard/template.hbs new file mode 100644 index 000000000..2621fd8d0 --- /dev/null +++ b/lib/monitoring/addon/components/node-dashboard/template.hbs @@ -0,0 +1,98 @@ + + + + + + +{{nodes-reservation nodes=model.nodes}} + +{{node-conditions conditionsSource=model.node.conditions}} + +
+ {{#accordion-list as |al expandFn|}} + {{#if scope.currentCluster.isMonitoringReady}} + {{#metrics-summary + classNames="mb-20" + expandAll=al.expandAll + expandFn=expandFn + grafanaUrl=model.node.grafanaUrl + title=(t "clusterDashboard.sections.node")}} + {{node-metrics resourceId=model.node.id}} + {{/metrics-summary}} + {{/if}} + {{resource-condition-list + resourceType=(t 'generic.node') + conditions=model.node.conditions + expandAll=al.expandAll + expandFn=expandFn + }} +
+ {{system-info-section + node=model.node + expandAll=al.expandAll + expandFn=expandFn + }} +
+
+ {{labels-section + model=model.node + showKind=false + expandAll=al.expandAll + expandFn=expandFn + }} +
+
+ {{annotations-section + model=model.node + expandAll=al.expandAll + expandFn=expandFn + }} +
+
+ {{taints-section + taints=model.node.taints + expandAll=al.expandAll + expandFn=expandFn + }} +
+ {{/accordion-list}} +
\ No newline at end of file diff --git a/lib/monitoring/addon/components/node-metrics/component.js b/lib/monitoring/addon/components/node-metrics/component.js new file mode 100644 index 000000000..f18af4a35 --- /dev/null +++ b/lib/monitoring/addon/components/node-metrics/component.js @@ -0,0 +1,9 @@ +import Component from '@ember/component'; +import Metrics from 'shared/mixins/metrics'; +import layout from './template'; + +export default Component.extend(Metrics, { + layout, + + filters: { resourceType: 'node' }, +}); diff --git a/lib/monitoring/addon/components/node-metrics/template.hbs b/lib/monitoring/addon/components/node-metrics/template.hbs new file mode 100644 index 000000000..1f3b7d3cc --- /dev/null +++ b/lib/monitoring/addon/components/node-metrics/template.hbs @@ -0,0 +1,7 @@ +
+ {{metrics-action + queryAction="query" + state=state + }} + {{metrics-graph graphs=graphs loading=state.loading noGraphs=state.noGraphs}} +
\ No newline at end of file diff --git a/app/components/node-gauges/component.js b/lib/monitoring/addon/components/nodes-reservation/component.js similarity index 76% rename from app/components/node-gauges/component.js rename to lib/monitoring/addon/components/nodes-reservation/component.js index a43e61395..9dc10e82f 100644 --- a/app/components/node-gauges/component.js +++ b/lib/monitoring/addon/components/nodes-reservation/component.js @@ -1,8 +1,8 @@ import Component from '@ember/component'; -import { observer } from '@ember/object'; import { inject as service } from '@ember/service'; -import { formatSi, parseSi, exponentNeeded } from 'shared/utils/parse-unit'; import layout from './template'; +import { computed } from '@ember/object'; +import { formatSi, parseSi, exponentNeeded } from 'shared/utils/parse-unit'; export default Component.extend({ intl: service(), @@ -11,56 +11,36 @@ export default Component.extend({ nodes: null, - gauges: null, - - init() { - this._super(...arguments); - setTimeout(() => { - this.setDashboard(); - }, 150); - }, - - updateDashboard: observer('nodes.@each.{allocatable,requested}', 'intl.locale', function() { - this.setDashboard(); - }), - - setDashboard() { - const cpuGauge = this.getCpuGauge(); - const memoryGauge = this.getMemoryGauge(); - const podsGauge = this.getPodsGauge(); - - this.set('gauges', [cpuGauge, memoryGauge, podsGauge]); - }, - - getCpuGauge() { + cpuReservation: computed('nodes.@each.{allocatable,requested}', 'intl.locale', function() { return this.getGauge('cpu', (u, t) => formatSi(u, 1000, '', '', 0, exponentNeeded(t), 1).replace(/\s.*$/, ''), (t) => formatSi(t, 1000, '', '', 0, exponentNeeded(t), 1), 'reserved', ); - }, + }), - getMemoryGauge() { + memoryReservation: computed('nodes.@each.{allocatable,requested}', 'intl.locale', function() { return this.getGauge('memory', (u, t) => formatSi(u, 1024, '', '', 0, exponentNeeded(t), 1).replace(/\s.*$/, ''), (t) => formatSi(t, 1024, 'iB', 'B', 0, exponentNeeded(t), 1), 'reserved', ); - }, + }), - getPodsGauge() { + podUsage: computed('nodes.@each.{allocatable,requested}', 'intl.locale', function() { return this.getGauge('pods', (u, t) => formatSi(u, 1000, '', '', 0, exponentNeeded(t), 1).replace(/\s.*$/, ''), (t) => formatSi(t, 1000, '', '', 0, exponentNeeded(t), 1), 'used', ); - }, + }), getGauge(field, usedFormatCb, totalFormatCb, keyword) { const nodes = this.getNodes(field); + const value = this.getValue(nodes) return { - value: this.getValue(nodes), - title: this.get('intl').t(`clusterDashboard.${ field }`), + percent: value.percent, + value: value.current, subtitle: this.getSubtitle(nodes, totalFormatCb, usedFormatCb, keyword), - ticks: this.getTicks(nodes), + ticks: this.getTicks(nodes) }; }, @@ -129,7 +109,10 @@ export default Component.extend({ }); const value = Math.round(used * 100 / total); - return isNaN(value) ? 0 : value; + return { + percent: isNaN(value) ? 0 : value, + current: used, + } }, getSubtitle(nodes, totalCb, usedCb, keyword) { @@ -142,8 +125,8 @@ export default Component.extend({ }); return this.get('intl').t(`clusterDashboard.subtitle.${ keyword }`, { - used: usedCb ? usedCb(used, total) : used, - total: totalCb ? totalCb(total) : total, + used: usedCb ? (usedCb(used, total) || '').trim() : used, + total: totalCb ? (totalCb(total) || '').trim() : total, }); }, -}); \ No newline at end of file +}); diff --git a/lib/monitoring/addon/components/nodes-reservation/template.hbs b/lib/monitoring/addon/components/nodes-reservation/template.hbs new file mode 100644 index 000000000..266fbd42a --- /dev/null +++ b/lib/monitoring/addon/components/nodes-reservation/template.hbs @@ -0,0 +1,28 @@ +
+
+ {{percent-gauge + value=cpuReservation.percent + title=(t 'clusterDashboard.cpu') + subtitle=cpuReservation.subtitle + ticks=cpuReservation.ticks + }} +
+ +
+ {{percent-gauge + value=memoryReservation.percent + title=(t 'clusterDashboard.memory') + subtitle=memoryReservation.subtitle + ticks=memoryReservation.ticks + }} +
+ +
+ {{percent-gauge + value=podUsage.percent + title=(t 'clusterDashboard.pods') + subtitle=podUsage.subtitle + ticks=podUsage.ticks + }} +
+
diff --git a/lib/monitoring/addon/components/rancher-metrics/component.js b/lib/monitoring/addon/components/rancher-metrics/component.js new file mode 100644 index 000000000..648437122 --- /dev/null +++ b/lib/monitoring/addon/components/rancher-metrics/component.js @@ -0,0 +1,9 @@ +import Component from '@ember/component'; +import Metrics from 'shared/mixins/metrics'; +import layout from './template'; + +export default Component.extend(Metrics, { + layout, + + filters: { displayResourceType: 'rancher-component' }, +}); diff --git a/lib/monitoring/addon/components/rancher-metrics/template.hbs b/lib/monitoring/addon/components/rancher-metrics/template.hbs new file mode 100644 index 000000000..ab8733448 --- /dev/null +++ b/lib/monitoring/addon/components/rancher-metrics/template.hbs @@ -0,0 +1,8 @@ +
+ {{metrics-action + queryAction="query" + state=state + }} + + {{metrics-graph graphs=graphs loading=state.loading noGraphs=state.noGraphs noDataLabel='clusterDashboard.noRancherComponents'}} +
\ No newline at end of file diff --git a/app/components/system-info-section/component.js b/lib/monitoring/addon/components/system-info-section/component.js similarity index 100% rename from app/components/system-info-section/component.js rename to lib/monitoring/addon/components/system-info-section/component.js diff --git a/app/components/system-info-section/template.hbs b/lib/monitoring/addon/components/system-info-section/template.hbs similarity index 100% rename from app/components/system-info-section/template.hbs rename to lib/monitoring/addon/components/system-info-section/template.hbs diff --git a/app/components/taints-section/component.js b/lib/monitoring/addon/components/taints-section/component.js similarity index 100% rename from app/components/taints-section/component.js rename to lib/monitoring/addon/components/taints-section/component.js diff --git a/app/components/taints-section/template.hbs b/lib/monitoring/addon/components/taints-section/template.hbs similarity index 100% rename from app/components/taints-section/template.hbs rename to lib/monitoring/addon/components/taints-section/template.hbs diff --git a/lib/monitoring/addon/engine.js b/lib/monitoring/addon/engine.js new file mode 100644 index 000000000..4928a8c94 --- /dev/null +++ b/lib/monitoring/addon/engine.js @@ -0,0 +1,32 @@ +import Engine from 'ember-engines/engine'; +import loadInitializers from 'ember-load-initializers'; +import Resolver from './resolver'; +import config from './config/environment'; + +const { modulePrefix } = config; + +const Eng = Engine.extend({ + modulePrefix, + Resolver, + dependencies: { + services: [ + 'app', + 'grafana', + 'intl', + 'scope', + 'session', + 'modal', + 'globalStore', + 'router', + 'k8s', + 'clusterStore', + 'tooltip', + ], + externalRoutes: [ + ] + } +}); + +loadInitializers(Eng, modulePrefix); + +export default Eng; diff --git a/app/authenticated/cluster/index/controller.js b/lib/monitoring/addon/index/controller.js similarity index 56% rename from app/authenticated/cluster/index/controller.js rename to lib/monitoring/addon/index/controller.js index de1dc9b16..bbbc0cfe7 100644 --- a/app/authenticated/cluster/index/controller.js +++ b/lib/monitoring/addon/index/controller.js @@ -1,32 +1,18 @@ import { alias } from '@ember/object/computed'; import { inject as service } from '@ember/service'; import { get, computed } from '@ember/object'; -import Controller, { inject as controller } from '@ember/controller'; +import Controller from '@ember/controller'; export default Controller.extend({ modalService: service('modal'), scope: service(), k8s: service(), - projectController: controller('authenticated.project'), - tags: alias('projectController.tags'), + cluster: alias('scope.currentCluster'), actions: { - dashboard() { - // window.open(this.get('k8s.kubernetesDashboard'),'_blank'); - }, - kubectl() { - /* @TODO-2.0 - if (e.metaKey) { - let proj = this.get('scope.currentProject.id'); - later(() => { - window.open(`//${window.location.host}/env/${proj}/infra/console?kubernetes=true&isPopup=true`, '_blank', "toolbars=0,width=900,height=700,left=200,top=200"); - }); - } else { -*/ this.get('modalService').toggleModal('modal-kubectl', {}); - // } }, kubeconfig() { diff --git a/lib/monitoring/addon/index/route.js b/lib/monitoring/addon/index/route.js new file mode 100644 index 000000000..855bdbd0e --- /dev/null +++ b/lib/monitoring/addon/index/route.js @@ -0,0 +1,22 @@ +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; +import { set } from '@ember/object'; +import { on } from '@ember/object/evented'; +import C from 'ui/utils/constants'; + +export default Route.extend({ + globalStore: service(), + scope: service(), + session: service(), + + model() { + return this.get('globalStore').findAll('node') + .then((nodes) => { + return { nodes, }; + }); + }, + + setDefaultRoute: on('activate', function() { + set(this, `session.${ C.SESSION.CLUSTER_ROUTE }`, 'authenticated.cluster.monitoring'); + }), +}); diff --git a/lib/monitoring/addon/index/template.hbs b/lib/monitoring/addon/index/template.hbs new file mode 100644 index 000000000..6adca6293 --- /dev/null +++ b/lib/monitoring/addon/index/template.hbs @@ -0,0 +1,27 @@ +
+

{{t 'clusterDashboard.title'}}: {{cluster.displayName}}

+ +
+ + + + + {{action-menu size="sm" classNames="pull-right" model=cluster}} +
+
+ +{{#if cluster.description}} +
+ {{banner-message color='bg-secondary mb-0 mt-10' message=(linkify cluster.description)}} +
+{{/if}} + +
+ {{cluster-dashboard cluster=cluster nodes=currentClusterNodes}} +
\ No newline at end of file diff --git a/app/node/route.js b/lib/monitoring/addon/node-detail/route.js similarity index 100% rename from app/node/route.js rename to lib/monitoring/addon/node-detail/route.js diff --git a/lib/monitoring/addon/node-detail/template.hbs b/lib/monitoring/addon/node-detail/template.hbs new file mode 100644 index 000000000..086838aaf --- /dev/null +++ b/lib/monitoring/addon/node-detail/template.hbs @@ -0,0 +1,17 @@ +
+
+

+ {{t 'hostsPage.hostPage.header.title' name=model.node.displayName}} +

+
+
+ {{badge-state model=model.node}} + {{action-menu model=model.node showPrimary=false classNames="ml-10 inline-block" size="sm"}} +
+
+ +{{#if model.node.description}} + {{banner-message color='bg-secondary mb-0 mt-10' message=(linkify model.node.description)}} +{{/if}} + +{{node-dashboard model=model}} diff --git a/lib/monitoring/addon/project-setting/route.js b/lib/monitoring/addon/project-setting/route.js new file mode 100644 index 000000000..39a469686 --- /dev/null +++ b/lib/monitoring/addon/project-setting/route.js @@ -0,0 +1,13 @@ +import Route from '@ember/routing/route'; +import { set } from '@ember/object'; +import { inject as service } from '@ember/service'; +import { on } from '@ember/object/evented'; +import C from 'ui/utils/constants'; + +export default Route.extend({ + session: service(), + + setDefaultRoute: on('activate', function() { + set(this, `session.${ C.SESSION.PROJECT_ROUTE }`, 'authenticated.project.monitoring.project-setting'); + }), +}); diff --git a/lib/monitoring/addon/project-setting/template.hbs b/lib/monitoring/addon/project-setting/template.hbs new file mode 100644 index 000000000..38b853377 --- /dev/null +++ b/lib/monitoring/addon/project-setting/template.hbs @@ -0,0 +1,4 @@ +
+

{{t 'monitoringPage.project.title'}}

+
+{{enable-monitoring level="project"}} \ No newline at end of file diff --git a/lib/monitoring/addon/resolver.js b/lib/monitoring/addon/resolver.js new file mode 100644 index 000000000..2fb563d6c --- /dev/null +++ b/lib/monitoring/addon/resolver.js @@ -0,0 +1,3 @@ +import Resolver from 'ember-resolver'; + +export default Resolver; diff --git a/lib/monitoring/addon/routes.js b/lib/monitoring/addon/routes.js new file mode 100644 index 000000000..a7fc605fa --- /dev/null +++ b/lib/monitoring/addon/routes.js @@ -0,0 +1,9 @@ +import buildRoutes from 'ember-engines/routes'; + +export default buildRoutes(function() { + // Define your engine's route map here + this.route('index', { path: '/' }); + this.route('cluster-setting'); + this.route('project-setting'); + this.route('node-detail', { path: '/:node_id' }) +}); diff --git a/lib/monitoring/addon/templates/application.hbs b/lib/monitoring/addon/templates/application.hbs new file mode 100644 index 000000000..e2147cab0 --- /dev/null +++ b/lib/monitoring/addon/templates/application.hbs @@ -0,0 +1 @@ +{{outlet}} \ No newline at end of file diff --git a/lib/monitoring/config/environment.js b/lib/monitoring/config/environment.js new file mode 100644 index 000000000..8b7687f21 --- /dev/null +++ b/lib/monitoring/config/environment.js @@ -0,0 +1,11 @@ +/* eslint-env node */ +'use strict'; + +module.exports = function(environment) { + let ENV = { + modulePrefix: 'monitoring', + environment + }; + + return ENV; +}; diff --git a/lib/monitoring/index.js b/lib/monitoring/index.js new file mode 100644 index 000000000..c28a8f84a --- /dev/null +++ b/lib/monitoring/index.js @@ -0,0 +1,19 @@ +/* eslint-disable */ +const webpack = require('webpack'); + +'use strict'; + +const EngineAddon = require('ember-engines/lib/engine-addon'); + +module.exports = EngineAddon.extend({ + name: 'monitoring', + + lazyLoading: { enabled: true }, + + isDevelopingAddon() { + return true; + }, + options: { + babel: { plugins: [require('ember-auto-import/babel-plugin')] } + } +}); diff --git a/lib/monitoring/package.json b/lib/monitoring/package.json new file mode 100644 index 000000000..88439fbb7 --- /dev/null +++ b/lib/monitoring/package.json @@ -0,0 +1,19 @@ +{ + "name": "monitoring", + "keywords": [ + "ember-addon", + "ember-engine" + ], + "dependencies": { + "ember-intl": "*", + "ember-href-to": "*", + "ember-auto-import": "*", + "ember-cli-htmlbars": "*", + "ember-cli-babel": "*" + }, + "ember-addon": { + "paths": [ + "../shared" + ] + } +} diff --git a/lib/monitoring/translations/en-us.yaml b/lib/monitoring/translations/en-us.yaml new file mode 100644 index 000000000..6f9f6581d --- /dev/null +++ b/lib/monitoring/translations/en-us.yaml @@ -0,0 +1,138 @@ +generic: + cpu: CPU + memory: Memory + hours: Hours + noData: No Data + +metrics: + cluster-cpu-usage: CPU Utilization + cluster-cpu-load: Load Average + cluster-memory-usage: Memory Utilization + cluster-fs-usage-percent: Disk Utilization + cluster-disk-io: Disk I/O + cluster-network-io: Network I/O + cluster-network-packet: Network Packets + etcd-db-bytes-sum: DB Size + etcd-grpc-client: GRPC Client Traffic + etcd-peer-traffic: Peer Traffic + etcd-raft-proposals: Raft Proposals + etcd-rpc-rate: RPC Rate + etcd-stream: Active Streams + etcd-sync-duration: Disk Sync Duration + apiserver-request-count: API Server Request Rate + apiserver-request-latency: API Server Request Latency + scheduler-e-2-e-scheduling-latency-seconds-quantile: Scheduler E2E Scheduling Latency + scheduler-pod-unscheduler: Scheduling Failed Pods + scheduler-total-preemption-attempts: Scheduler Preemption Attempts + controllermanager-queue-depth: Controller Manager Queue Depth + ingresscontroller-nginx-connection: Ingress Controller Connections + ingresscontroller-request-process-time: Ingress Controller Request Process Time + node-cpu-load: Load Average + node-cpu-usage: CPU Utilization + node-memory-usage: Memory Utilization + node-fs-usage-percent: Disk Utilization + node-disk-io: Disk I/O + node-network-io: Network I/O + node-network-packet: Network Packets + etcd-leader-change: Leader Change + etcd-disk-operate: Disk operations + fluentd-buffer-queue-length: Fluentd Buffer Queue Rate + fluentd-output-errors: Fluentd Output Errors Rate + fluentd-output-record-number: Fluentd Output Rate + fluentd-input-record-number: Fluentd Input Rate + +ingressResponse: + host: Host + path: Path + responseTime: Response Time + noData: No Data + noMatch: No records match the current search + detail: Top 10 ingress upstream response time + title: Ingress Upstream Response Time + +etcd: + hasLeader: + label: Etcd has a leader + leaderChange: + label: Number of leader changes + failedProposals: + label: Number of failed proposals + +clusterDashboard: + title: Dashboard + sections: + cluster: Cluster Metrics + etcd: Etcd Metrics + k8s: Kubernetes Components Metrics + rancher: '{appName} Components Metrics' + node: Node Metrics + noRancherComponents: 'There are no other {appName} components in this cluster.' + cpu: CPU + memory: Memory + pods: Pods + value: Value + more: More + subtitle: + reserved: "{used} of {total} Reserved" + used: "{used} of {total} Used" + enalbeMonitoring: Enable Monitoring to see live metrics + monitoringNotReady: Monitoring API is not ready + node: Nodes + etcd: Etcd + scheduler: Scheduler + controllerManager: Controller Manager + alert: + node: "Alert: Node {node} is not active." + component: "Alert: Component {component} is unhealthy." + +monitoringPage: + toUpdate: + cluster: Monitoring is enabled. Please go to System Project Catalog Apps and find cluster-monitoring app to update the configuration. + project: Monitoring is enabled. Please go to Catalog Apps and find project-monitoring app to update the configuration. + prometheus: Monitoring is not enabled yet, click the Save button below to enable it. + config: + types: + none: None + prometheus: Prometheus + header: Prometheus Configuration + prometheus: + enablePersistence: + label: Enable Persistent Storage for Prometheus + size: + label: Prometheus Persistent Volume Size + placeholder: e.g. 50Gi + storageClass: + label: Default StorageClass for Prometheus + grafana: + enablePersistence: + label: Enable Persistent Storage for Grafana + size: + label: Grafana Persistent Volume Size + placeholder: e.g. 10Gi + storageClass: + label: Default StorageClass for Grafana + retention: + label: Data Retention + storageClass: + label: Storage Class + nodeexporter: + label: Node Exporter Host Port + nodeSelector: + addSelectorLabel: Add Selector + helpText: Select the nodes where monitoring relalted workloads will be scheduled to + cluster: + title: Cluster Monitoring + info: 'We will use Prometheus to collect metrics from Nodes, Kubernetes components and Pods in this cluster.' + disabled: Monitoring is disabled in the current cluster. + toDisable: Monitoring is enabled. Click the Save button below will disable monitoring in current cluster. + ready: Monitoring is ready. System monitoring tool has been deployed to + notReady: Monitoring API is not ready. You can find system monitoring tool in + justDeployed: Deploying system monitoring tool to + project: + title: Project Monitoring + info: 'We will use Prometheus to collect metrics from workloads in this project.' + disabled: Monitoring is disabled in the current project. + toDisable: Monitoring is enabled. Click the Save button below will disable monitoring in current project. + ready: Monitoring is ready. + notReady: Monitoring API is not ready. + justDeployed: Deploying project monitoring tool to this project. \ No newline at end of file diff --git a/lib/monitoring/translations/zh-hans.yaml b/lib/monitoring/translations/zh-hans.yaml new file mode 100644 index 000000000..d13450f28 --- /dev/null +++ b/lib/monitoring/translations/zh-hans.yaml @@ -0,0 +1,128 @@ +generic: + cpu: CPU + memory: 内存 + hours: 小时 + noData: 没有数据 + +metrics: + cluster-cpu-usage: CPU使用率 + cluster-cpu-load: CPU负载 + cluster-memory-usage: 内存使用率 + cluster-fs-usage-percent: 磁盘使用率 + cluster-disk-io: 磁盘I/O + cluster-network-io: 网络I/O + cluster-network-packet: 网络数据包 + etcd-db-bytes-sum: 数据库大小 + etcd-grpc-client: GRPC客户端流量 + etcd-peer-traffic: 传输流量 + etcd-raft-proposals: Raft提议 + etcd-rpc-rate: RPC速率 + etcd-stream: 活动流 + etcd-sync-duration: 磁盘同步周期 + apiserver-request-count: API Server请求速率 + apiserver-request-latency: API Server请求延迟 + scheduler-e-2-e-scheduling-latency-seconds-quantile: 调度器e2e调度延迟 + scheduler-pod-unscheduler: 调度失败的Pod + scheduler-total-preemption-attempts: 调度器尝试调度次数 + controllermanager-queue-depth: 控制器队列深度 + ingresscontroller-nginx-connection: Ingress控制器连接数 + ingresscontroller-request-process-time: Ingress控制器请求处理时长 + node-cpu-load: CPU负载 + node-cpu-usage: CPU使用率 + node-memory-usage: 内存使用率 + node-fs-usage-percent: 磁盘使用率 + node-disk-io: 磁盘I/O + node-network-io: 网络I/O + node-network-packet: 网络数据包 + etcd-leader-change: Leader变更 + etcd-disk-operate: 磁盘操作 + fluentd-buffer-queue-length: Fluentd缓存区队列长度 + fluentd-output-errors: Fluentd输出错误速率 + fluentd-output-record-number: Fluentd输出速率 + fluentd-input-record-number: Fluentd输入速率 + +ingressResponse: + host: 主机 + path: 路径 + responseTime: 响应时间 + noData: 没有数据 + noMatch: 没有记录匹配当前搜索 + detail: Upstream响应时长(排序前十位) + title: Ingress Upstream响应时长 + +etcd: + hasLeader: + label: Etcd是否有Leader + leaderChange: + label: Leader变更次数 + failedProposals: + label: 失败提议次数 + +clusterDashboard: + title: 仪表盘 + sections: + cluster: 集群监控 + etcd: Etcd监控 + k8s: Kubernetes组件监控 + rancher: '{appName}组件监控' + node: 节点监控 + noRancherComponents: '当前集群没有部署其他的{appName}组件。' + value: 值 + more: 更多 + subtitle: + reserved: "已保留{total}中的{used}" + used: "已使用{total}中的{used}" + enalbeMonitoring: 启用监控并查看实时监控指标 + monitoringNotReady: 监控API未就绪 + alert: + node: "警告: 节点{node}未激活" + component: "警告: 组件{component}不健康" + +monitoringPage: + toUpdate: + cluster: 监控已启用。请在系统项目的应用商店中找到cluster-monitoring应用,从而监控的配置。 + project: 监控已启用。请在该项目的应用商店中找到project-monitoring应用,从而监控的配置。 + prometheus: 监控尚未启用,点击下面的保存按钮去启用监控。 + config: + header: Prometheus配置 + prometheus: + enablePersistence: + label: 为Prometheus启用持久化存储 + size: + label: Prometheus持久化存储大小 + placeholder: 例如:50Gi + storageClass: + label: Prometheus默认存储类 + grafana: + enablePersistence: + label: 为Grafana启用持久化存储 + size: + label: Grafana持久化存储大小 + placeholder: 例如:10Gi + storageClass: + label: Grafana默认存储类 + retention: + label: 数据保存时间 + storageClass: + label: 存储类 + nodeexporter: + label: Node Exporter主机端口 + nodeSelector: + addSelectorLabel: 添加选择器 + helpText: 选择部署监控相关的工作负载所运行的节点。 + cluster: + title: 集群监控 + info: '使用Prometheus收集当前集群中节点,Kubernetes组件和Pod的监控指标。' + disabled: 当前集群监控尚未启用。 + toDisable: 监控已启用。点击下面的保存按钮来禁用当前集群的监控。 + ready: 监控已就绪。系统监控组件部署在 + notReady: 监控API未就绪。系统监控组件部署在 + justDeployed: 正在部署系统监控组件 + project: + title: 项目监控 + info: '使用Prometheus收集当前项目中的监控指标。' + disabled: 当前项目监控尚未启用。 + toDisable: 监控已启用。点击下面的保存按钮来禁用当前项目的监控。 + ready: 监控已就绪。 + notReady: 监控API未就绪。 + justDeployed: 正在部署项目监控组件。 \ No newline at end of file diff --git a/lib/nodes/addon/engine.js b/lib/nodes/addon/engine.js index 93ad6cbe0..72b7453bb 100644 --- a/lib/nodes/addon/engine.js +++ b/lib/nodes/addon/engine.js @@ -39,7 +39,6 @@ const Eng = Engine.extend({ 'authenticated.project', 'authenticated.prefs', 'authenticated.cluster.nodes', - 'authenticated.cluster.nodes.node', 'authenticated.cluster.security.members.index', 'logout' ] diff --git a/lib/pipeline/addon/components/build-log/template.hbs b/lib/pipeline/addon/components/build-log/template.hbs index 003d6dfde..780c2d278 100644 --- a/lib/pipeline/addon/components/build-log/template.hbs +++ b/lib/pipeline/addon/components/build-log/template.hbs @@ -1,6 +1,6 @@
{{#unless fullscreen}} -
+
{{build-stages stageInfo=activity.pipelineConfig.stages body=activity.stages diff --git a/lib/pipeline/addon/components/edit-pipeline-config/template.hbs b/lib/pipeline/addon/components/edit-pipeline-config/template.hbs index c3b452d47..aa206300b 100644 --- a/lib/pipeline/addon/components/edit-pipeline-config/template.hbs +++ b/lib/pipeline/addon/components/edit-pipeline-config/template.hbs @@ -16,7 +16,7 @@ {{#if pipeline.loading}}
- +
{{else if pipeline.url}} diff --git a/lib/shared/addon/catalog/service.js b/lib/shared/addon/catalog/service.js index 7fcf65e52..5aef5c570 100644 --- a/lib/shared/addon/catalog/service.js +++ b/lib/shared/addon/catalog/service.js @@ -8,6 +8,7 @@ import { allSettled, hash } from 'rsvp'; import { union } from '@ember/object/computed'; const RANCHER_VERSION = 'rancherVersion'; +const SYSTEM_CATALOG = 'system-library'; export default Service.extend({ globalStore: service(), @@ -220,7 +221,9 @@ export default Service.extend({ let categories = []; data.forEach((obj) => { - categories.pushObjects(obj.get('categoryArray')); + if ( get(obj, 'catalogId') !== SYSTEM_CATALOG ) { + categories.pushObjects(obj.get('categoryArray')); + } }); categories = uniqKeys(categories); categories.unshift(''); @@ -230,7 +233,7 @@ export default Service.extend({ return false; } - return true; + return get(tpl, 'catalogId') !== SYSTEM_CATALOG; }); data = data.sortBy('name'); diff --git a/lib/shared/addon/components/accordion-list-item/component.js b/lib/shared/addon/components/accordion-list-item/component.js index ab1aca0d8..0ad8efb4c 100644 --- a/lib/shared/addon/components/accordion-list-item/component.js +++ b/lib/shared/addon/components/accordion-list-item/component.js @@ -96,6 +96,9 @@ export default Component.extend({ this.expand(this); } }, + goToGrafana() { + window.open(get(this, 'grafanaUrl'), '_blank'); + } }, expdObserver: on('init', observer('expanded', function() { diff --git a/lib/shared/addon/components/accordion-list-item/template.hbs b/lib/shared/addon/components/accordion-list-item/template.hbs index 2756b816a..5e018bd90 100644 --- a/lib/shared/addon/components/accordion-list-item/template.hbs +++ b/lib/shared/addon/components/accordion-list-item/template.hbs @@ -14,6 +14,14 @@ {{status}}
{{/if}} + {{#if grafanaUrl}} + + {{/if}}
{{#if everExpanded}} diff --git a/app/components/annotations-section/component.js b/lib/shared/addon/components/annotations-section/component.js similarity index 100% rename from app/components/annotations-section/component.js rename to lib/shared/addon/components/annotations-section/component.js diff --git a/app/components/annotations-section/template.hbs b/lib/shared/addon/components/annotations-section/template.hbs similarity index 100% rename from app/components/annotations-section/template.hbs rename to lib/shared/addon/components/annotations-section/template.hbs diff --git a/lib/shared/addon/components/component-badge/component.js b/lib/shared/addon/components/component-badge/component.js deleted file mode 100644 index 4c4644803..000000000 --- a/lib/shared/addon/components/component-badge/component.js +++ /dev/null @@ -1,40 +0,0 @@ -import Component from '@ember/component'; -import ThrottledResize from 'shared/mixins/throttled-resize'; -import initGraph from 'ui/utils/component-badge'; -import layout from './template'; - -export default Component.extend(ThrottledResize, { - layout, - tagName: 'div', - classNames: ['mt-10', 'component-badge'], - - component: null, - healthy: null, - - svg: null, - - didRender() { - this._super(); - if (!this.get('svg')) { - this.create(); - } - }, - - updateHealthStatus: function() { - this.get('svg').updateHealthStatus(this.get('healthy')); - }.observes('healthy'), - create() { - this.set('svg', initGraph({ - el: this.$()[0], - component: this.get('component'), - healthy: this.get('healthy'), - })); - }, - - onResize() { - if (this.get('svg')) { - this.get('svg').fit(); - } - }, - -}); diff --git a/lib/shared/addon/components/component-badge/template.hbs b/lib/shared/addon/components/component-badge/template.hbs deleted file mode 100644 index fb5c4b157..000000000 --- a/lib/shared/addon/components/component-badge/template.hbs +++ /dev/null @@ -1 +0,0 @@ -{{yield}} \ No newline at end of file diff --git a/lib/shared/addon/components/graph-area/component.js b/lib/shared/addon/components/graph-area/component.js index 585331c54..c082b535a 100644 --- a/lib/shared/addon/components/graph-area/component.js +++ b/lib/shared/addon/components/graph-area/component.js @@ -1,67 +1,252 @@ import { inject as service } from '@ember/service'; -import Component from '@ember/component'; +import { get, set, observer } from '@ember/object'; import ThrottledResize from 'shared/mixins/throttled-resize'; -import initGraph from 'ui/utils/graph-area'; +import { escapeHtml } from 'shared/utils/util'; +import Component from '@ember/component'; +import { all } from 'rsvp' +import { theme as THEME } from './theme'; +import { + roundValue, + formatPercent, + formatMib, + formatSecond, + formatKbps +} from 'shared/utils/util'; import layout from './template'; +function getFormatter(unit, full = false) { + switch (unit) { + case 'seconds': + return formatSecond; + case 'pps': + return (value) => { + return `${ roundValue(value) } ${ full ? 'Packets Per Second' : 'Pps' }` + } + case 'ops': + return (value) => { + return `${ roundValue(value) } ${ full ? 'Operation Per Second' : 'Ops' }` + } + case 'ms': + return (value) => { + return `${ roundValue(value) } ms` + } + case 'mcpu': + return (value) => { + return `${ roundValue(value) } mCPU` + } + case 'percent': + return formatPercent; + case 'bps': + case 'kbps': + return formatKbps; + case 'byte': + return formatMib; + default: + return roundValue; + } +} + +function getConverter(unit) { + switch (unit) { + case 'percent': + return (value) => value * 100; + case 'mcpu': + return (value) => value * 1000; + case 'bps': + return (value) => value / 1024; + case 'byte': + return (value) => value / 1048576; + default: + return (value) => value; + } +} + +const LOADING_PARAMS = { + text: '', + color: '#3d3d3d', + textColor: '#3d3d3d', + maskColor: 'rgba(255, 255, 255, 0.8)', + zlevel: 0 +} + +var ECharts = null; + export default Component.extend(ThrottledResize, { intl: service(), layout, + tagName: 'div', classNames: ['graph-area'], model: null, fields: null, + chart: null, - min: 0, - minMax: null, // lower bound on how small automatic max can be - max: null, // set an explicit max - maxDoubleInital: false, // if true, set max to double the initial non-zero data point - scaleDown: false, // if true, max is allowed to go back down. If false it can only go up. + formatter: 'value', + needRefresh: false, - gradient: null, + init() { + this._super(...arguments); - formatter: 'value', - svg: null, - - didRender() { - this._super(); - if (this.get('fields.length') > 0 && !this.get('svg')) { - this.create(); - setTimeout(() => { - this.get('svg').start(); - }, 100); + if ( !ECharts ) { + all([ + import('echarts/lib/echarts'), + import('echarts/lib/chart/line'), + import('echarts/lib/component/tooltip'), + ]).then( (modules) => { + ECharts = modules[0].default; + ECharts.registerTheme('walden', THEME); + this.didRender(); + }); } }, willDestroyElement() { - if (this.get('svg')) { - this.get('svg').destory(); + const chart = get(this, 'chart'); + + if ( chart ) { + chart.dispose(); } - this._super(); - }, - create() { - this.set('svg', initGraph({ - el: this.$()[0], - min: this.get('min'), - minMax: this.get('minMax'), - max: this.get('max'), - gradient: this.get('gradient'), - formatter: this.get('formatter'), - maxDoubleInital: this.get('maxDoubleInital'), - fields: this.get('fields'), - query: this.query.bind(this) - })); }, - query(field) { - return this.get(`model.${ field }`); + didRender() { + if ( ECharts && !get(this, 'chart') ) { + this.create(); + setTimeout(() => { + const chart = get(this, 'chart'); + + chart.resize(); + }, 200); + } }, + loadingDidChange: observer('loading', function() { + const chart = get(this, 'chart'); + + if ( chart && get(this, 'loading') ) { + chart.showLoading(LOADING_PARAMS); + } else if (chart) { + chart.hideLoading(); + } + }), + + dataDidChange: observer('series', function() { + if ( get(this, 'chart') ) { + this.draw(); + } + }), + onResize() { - if (this.get('svg')) { - this.get('svg').fit(); + if ( !ECharts || this.isDestroyed || this.isDestroying ) { + return; + } + + if ( get(this, 'chart') ) { + get(this, 'chart').resize(); } }, + create() { + const chart = ECharts.init(this.$('.content')[0], 'walden'); + + set(this, 'chart', chart); + chart.showLoading(LOADING_PARAMS); + this.draw(); + }, + + draw() { + const chart = get(this, 'chart'); + + if ( !chart ) { + return; + } + + const minMax = get(this, 'formatter') === 'percent' ? 100 : null; + let setMax = true; + const series = []; + const fields = (get(this, 'series') || []).map((serie) => { + return { + id: get(serie, 'name'), + data: get(serie, 'points').map((p) => [p[1], getConverter(get(this, 'formatter'))(p[0])]) + } + }); + + fields.forEach((field) => { + const serie = field.data || []; + const data = []; + + serie.forEach((d) => { + if ( minMax && setMax && d[1] > minMax ) { + setMax = false; + } + data.push(d); + }); + + series.push({ + name: field.id, + type: 'line', + showSymbol: false, + animation: false, + data, + itemStyle: { normal: { lineStyle: { width: 1 } } } + }); + }); + + const formatter = getFormatter(get(this, 'formatter'), true); + let option = { + tooltip: { + trigger: 'axis', + position(pos, params, dom, rect, size) { + const obj = { top: 60 }; + + obj[['left', 'right'][+(pos[0] < size.viewSize[0] / 2)]] = 5; + + return obj; + }, + formatter(params) { + let html = ''; + + params.forEach((p, i) => { + if ( i === 0 ) { + html = `
${ p.axisValueLabel }` + } + + const value = escapeHtml(formatter(p.data[1])); + let label = escapeHtml(p.seriesName); + + html += `
${ label } : ${ value }`; + }); + + html += '
'; + + return html.htmlSafe(); + } + }, + grid: { + top: '10px', + left: '30px', + right: '30px', + bottom: '3%', + containLabel: true + }, + xAxis: { + type: 'time', + boundaryGap: false, + }, + yAxis: { + type: 'value', + axisLabel: { formatter: getFormatter(get(this, 'formatter')) }, + splitArea: { show: true }, + }, + series, + }; + + if ( setMax && minMax ) { + option.yAxis.max = minMax; + } + + chart.setOption(option, true); + + chart.hideLoading(); + }, }); diff --git a/lib/shared/addon/components/graph-area/template.hbs b/lib/shared/addon/components/graph-area/template.hbs index fb5c4b157..92769612f 100644 --- a/lib/shared/addon/components/graph-area/template.hbs +++ b/lib/shared/addon/components/graph-area/template.hbs @@ -1 +1 @@ -{{yield}} \ No newline at end of file +
\ No newline at end of file diff --git a/lib/shared/addon/components/graph-area/theme.js b/lib/shared/addon/components/graph-area/theme.js new file mode 100644 index 000000000..5c668dfa3 --- /dev/null +++ b/lib/shared/addon/components/graph-area/theme.js @@ -0,0 +1,401 @@ +const theme = { + 'color': [ + '#3fb1e3', + '#6be6c1', + '#626c91', + '#a0a7e6', + '#c4ebad', + '#96dee8' + ], + 'backgroundColor': 'rgba(252,252,252,0)', + 'textStyle': {}, + 'title': { + 'textStyle': { 'color': '#666666' }, + 'subtextStyle': { 'color': '#999999' } + }, + 'line': { + 'itemStyle': { 'normal': { 'borderWidth': '2' } }, + 'lineStyle': { 'normal': { 'width': '1' } }, + 'symbolSize': '8', + 'symbol': 'emptyCircle', + 'smooth': false + }, + 'radar': { + 'itemStyle': { 'normal': { 'borderWidth': '2' } }, + 'lineStyle': { 'normal': { 'width': '1' } }, + 'symbolSize': '8', + 'symbol': 'emptyCircle', + 'smooth': false + }, + 'bar': { + 'itemStyle': { + 'normal': { + 'barBorderWidth': 0, + 'barBorderColor': '#ccc' + }, + 'emphasis': { + 'barBorderWidth': 0, + 'barBorderColor': '#ccc' + } + } + }, + 'pie': { + 'itemStyle': { + 'normal': { + 'borderWidth': 0, + 'borderColor': '#ccc' + }, + 'emphasis': { + 'borderWidth': 0, + 'borderColor': '#ccc' + } + } + }, + 'scatter': { + 'itemStyle': { + 'normal': { + 'borderWidth': 0, + 'borderColor': '#ccc' + }, + 'emphasis': { + 'borderWidth': 0, + 'borderColor': '#ccc' + } + } + }, + 'boxplot': { + 'itemStyle': { + 'normal': { + 'borderWidth': 0, + 'borderColor': '#ccc' + }, + 'emphasis': { + 'borderWidth': 0, + 'borderColor': '#ccc' + } + } + }, + 'parallel': { + 'itemStyle': { + 'normal': { + 'borderWidth': 0, + 'borderColor': '#ccc' + }, + 'emphasis': { + 'borderWidth': 0, + 'borderColor': '#ccc' + } + } + }, + 'sankey': { + 'itemStyle': { + 'normal': { + 'borderWidth': 0, + 'borderColor': '#ccc' + }, + 'emphasis': { + 'borderWidth': 0, + 'borderColor': '#ccc' + } + } + }, + 'funnel': { + 'itemStyle': { + 'normal': { + 'borderWidth': 0, + 'borderColor': '#ccc' + }, + 'emphasis': { + 'borderWidth': 0, + 'borderColor': '#ccc' + } + } + }, + 'gauge': { + 'itemStyle': { + 'normal': { + 'borderWidth': 0, + 'borderColor': '#ccc' + }, + 'emphasis': { + 'borderWidth': 0, + 'borderColor': '#ccc' + } + } + }, + 'candlestick': { + 'itemStyle': { + 'normal': { + 'color': '#e6a0d2', + 'color0': 'transparent', + 'borderColor': '#e6a0d2', + 'borderColor0': '#3fb1e3', + 'borderWidth': '2' + } + } + }, + 'graph': { + 'itemStyle': { + 'normal': { + 'borderWidth': 0, + 'borderColor': '#ccc' + } + }, + 'lineStyle': { + 'normal': { + 'width': '1', + 'color': '#cccccc' + } + }, + 'symbolSize': '8', + 'symbol': 'emptyCircle', + 'smooth': false, + 'color': [ + '#3fb1e3', + '#6be6c1', + '#626c91', + '#a0a7e6', + '#c4ebad', + '#96dee8' + ], + 'label': { 'normal': { 'textStyle': { 'color': '#ffffff' } } } + }, + 'map': { + 'itemStyle': { + 'normal': { + 'areaColor': '#eeeeee', + 'borderColor': '#aaaaaa', + 'borderWidth': 0.5 + }, + 'emphasis': { + 'areaColor': 'rgba(63,177,227,0.25)', + 'borderColor': '#3fb1e3', + 'borderWidth': 1 + } + }, + 'label': { + 'normal': { 'textStyle': { 'color': '#ffffff' } }, + 'emphasis': { 'textStyle': { 'color': 'rgb(63,177,227)' } } + } + }, + 'geo': { + 'itemStyle': { + 'normal': { + 'areaColor': '#eeeeee', + 'borderColor': '#aaaaaa', + 'borderWidth': 0.5 + }, + 'emphasis': { + 'areaColor': 'rgba(63,177,227,0.25)', + 'borderColor': '#3fb1e3', + 'borderWidth': 1 + } + }, + 'label': { + 'normal': { 'textStyle': { 'color': '#ffffff' } }, + 'emphasis': { 'textStyle': { 'color': 'rgb(63,177,227)' } } + } + }, + 'categoryAxis': { + 'axisLine': { + 'show': true, + 'lineStyle': { 'color': '#cccccc' } + }, + 'axisTick': { + 'show': false, + 'lineStyle': { 'color': '#333' } + }, + 'axisLabel': { + 'show': true, + 'textStyle': { 'color': '#999999' } + }, + 'splitLine': { + 'show': true, + 'lineStyle': { + 'type': 'dashed', + 'color': [ + '#eeeeee' + ] + } + }, + 'splitArea': { + 'show': false, + 'areaStyle': { + 'color': [ + 'rgba(250,250,250,0.05)', + 'rgba(200,200,200,0.02)' + ] + } + } + }, + 'valueAxis': { + 'axisLine': { + 'show': true, + 'lineStyle': { 'color': '#cccccc' } + }, + 'axisTick': { + 'show': false, + 'lineStyle': { 'color': '#333' } + }, + 'axisLabel': { + 'show': true, + 'textStyle': { 'color': '#999999' } + }, + 'splitLine': { + 'show': true, + 'lineStyle': { + 'type': 'dashed', + 'color': [ + '#eeeeee' + ] + } + }, + 'splitArea': { + 'show': false, + 'areaStyle': { + 'color': [ + 'rgba(250,250,250,0.05)', + 'rgba(200,200,200,0.02)' + ] + } + } + }, + 'logAxis': { + 'axisLine': { + 'show': true, + 'lineStyle': { 'color': '#cccccc' } + }, + 'axisTick': { + 'show': false, + 'lineStyle': { 'color': '#333' } + }, + 'axisLabel': { + 'show': true, + 'textStyle': { 'color': '#999999' } + }, + 'splitLine': { + 'show': true, + 'lineStyle': { + 'type': 'dashed', + 'color': [ + '#eeeeee' + ] + } + }, + 'splitArea': { + 'show': false, + 'areaStyle': { + 'color': [ + 'rgba(250,250,250,0.05)', + 'rgba(200,200,200,0.02)' + ] + } + } + }, + 'timeAxis': { + 'axisLine': { + 'show': true, + 'lineStyle': { 'color': '#cccccc' } + }, + 'axisTick': { + 'show': false, + 'lineStyle': { 'color': '#333' } + }, + 'axisLabel': { + 'show': true, + 'textStyle': { 'color': '#999999' } + }, + 'splitLine': { + 'show': true, + 'lineStyle': { + 'type': 'dashed', + 'color': [ + '#eeeeee' + ] + } + }, + 'splitArea': { + 'show': false, + 'areaStyle': { + 'color': [ + 'rgba(250,250,250,0.05)', + 'rgba(200,200,200,0.02)' + ] + } + } + }, + 'toolbox': { + 'iconStyle': { + 'normal': { 'borderColor': '#999999' }, + 'emphasis': { 'borderColor': '#666666' } + } + }, + 'legend': { 'textStyle': { 'color': '#999999' } }, + 'tooltip': { + 'axisPointer': { + 'lineStyle': { + 'color': '#cccccc', + 'width': 1 + }, + 'crossStyle': { + 'color': '#cccccc', + 'width': 1 + } + } + }, + 'timeline': { + 'lineStyle': { + 'color': '#626c91', + 'width': 1 + }, + 'itemStyle': { + 'normal': { + 'color': '#626c91', + 'borderWidth': 1 + }, + 'emphasis': { 'color': '#626c91' } + }, + 'controlStyle': { + 'normal': { + 'color': '#626c91', + 'borderColor': '#626c91', + 'borderWidth': 0.5 + }, + 'emphasis': { + 'color': '#626c91', + 'borderColor': '#626c91', + 'borderWidth': 0.5 + } + }, + 'checkpointStyle': { + 'color': '#3fb1e3', + 'borderColor': 'rgba(63,177,227,0.15)' + }, + 'label': { + 'normal': { 'textStyle': { 'color': '#626c91' } }, + 'emphasis': { 'textStyle': { 'color': '#626c91' } } + } + }, + 'visualMap': { + 'color': [ + '#2a99c9', + '#afe8ff' + ] + }, + 'dataZoom': { + 'backgroundColor': 'rgba(255,255,255,0)', + 'dataBackgroundColor': 'rgba(222,222,222,1)', + 'fillerColor': 'rgba(114,230,212,0.25)', + 'handleColor': '#cccccc', + 'handleSize': '100%', + 'textStyle': { 'color': '#999999' } + }, + 'markPoint': { + 'label': { + 'normal': { 'textStyle': { 'color': '#ffffff' } }, + 'emphasis': { 'textStyle': { 'color': '#ffffff' } } + } + } +} + +export { theme }; diff --git a/lib/shared/addon/components/input-yaml/template.hbs b/lib/shared/addon/components/input-yaml/template.hbs index 1aee03fba..9b362d6e6 100644 --- a/lib/shared/addon/components/input-yaml/template.hbs +++ b/lib/shared/addon/components/input-yaml/template.hbs @@ -37,7 +37,7 @@ {{#if loading}}
- +
{{else}} diff --git a/app/components/labels-section/component.js b/lib/shared/addon/components/labels-section/component.js similarity index 100% rename from app/components/labels-section/component.js rename to lib/shared/addon/components/labels-section/component.js diff --git a/app/components/labels-section/template.hbs b/lib/shared/addon/components/labels-section/template.hbs similarity index 100% rename from app/components/labels-section/template.hbs rename to lib/shared/addon/components/labels-section/template.hbs diff --git a/lib/shared/addon/components/metrics-action/component.js b/lib/shared/addon/components/metrics-action/component.js new file mode 100644 index 000000000..61332f4f6 --- /dev/null +++ b/lib/shared/addon/components/metrics-action/component.js @@ -0,0 +1,152 @@ +import Component from '@ember/component'; +import { get, set, observer, setProperties } from '@ember/object'; +import { inject as service } from '@ember/service'; +import C from 'ui/utils/constants'; +import layout from './template'; +import { next } from '@ember/runloop'; +const CUSTOM = 'custom'; +const PERIODS = [ + { + label: 'metricsAction.periods.custom', + value: CUSTOM + }, + { + label: 'metricsAction.periods.5m', + value: 'now-5m', + interval: '5s' + }, + { + label: 'metricsAction.periods.1h', + value: 'now-1h', + interval: '60s' + }, + { + label: 'metricsAction.periods.6h', + value: 'now-6h', + interval: '60s' + }, + { + label: 'metricsAction.periods.24h', + value: 'now-24h', + interval: '300s' + }, + { + label: 'metricsAction.periods.7d', + value: 'now-168h', + interval: '1800s' + }, + { + label: 'metricsAction.periods.30d', + value: 'now-720h', + interval: '3600s' + }, +]; + +export default Component.extend({ + intl: service(), + globalStore: service(), + scope: service(), + prefs: service(), + + layout, + classNames: 'mb-20', + + resourceType: 'cluster', + dashboardName: null, + allowDetail: true, + + selected: 'now-1h', + periods: PERIODS, + + init() { + this._super(...arguments); + const periodPref = get(this, `prefs.${ C.PREFS.PERIOD }`); + + if ( periodPref ) { + set(this, 'selected', periodPref); + } + + next(() => { + this.query(); + }); + }, + + actions: { + refresh() { + this.query(); + }, + + fromDidChange(from) { + if ( get(from, 'length') ) { + set(this, 'from', get(from, 'firstObject').getTime()); + } + }, + + toDidChange(to) { + if ( get(to, 'length') ) { + set(this, 'to', get(to, 'firstObject').getTime()); + } + }, + + onTimePickerClose() { + this.query(false); + }, + + toggle(detail) { + if ( !get(this, 'state.loading') ) { + set(this, 'state.detail', detail); + this.query(); + } + } + }, + + periodDidChange: observer('selected', function() { + setProperties(this, { + from: new Date().getTime() - 60000, + to: new Date().getTime(), + now: new Date().getTime() + }); + if ( get(this, 'selected') !== CUSTOM ) { + set(this, `prefs.${ C.PREFS.PERIOD }`, get(this, 'selected')); + } + + this.query(); + }), + + query(forceRefresh = true) { + const period = get(this, 'selected'); + let from; + let to; + let interval; + + if ( period !== CUSTOM ) { + const params = PERIODS.findBy('value', get(this, 'selected')); + + from = period, + to = 'now', + interval = get(params, 'interval') + } else { + from = get(this, 'from').toString(); + to = get(this, 'to').toString() || new Date().getTime().toString(); + interval = `${ Math.round((to - from) / 120000) }s` + } + setProperties(get(this, 'state'), { + from, + to, + interval, + }); + + if ( period === CUSTOM ) { + if ( !forceRefresh && get(this, 'preFrom') === from && get(this, 'preTo') === to ) { + return; + } else { + setProperties(this, { + preFrom: from, + preTo: to + }); + } + } + this.sendAction('queryAction'); + }, + +}); diff --git a/lib/shared/addon/components/metrics-action/template.hbs b/lib/shared/addon/components/metrics-action/template.hbs new file mode 100644 index 000000000..dbac41f90 --- /dev/null +++ b/lib/shared/addon/components/metrics-action/template.hbs @@ -0,0 +1,64 @@ +
+
+
+ {{new-select + content=periods + value=selected + localizedLabel=true + disabled=state.loading + }} +
+ {{#if (eq selected 'custom')}} +
+ {{ember-flatpickr + disabled=state.loading + enableSeconds=true + enableTime=true + dateFormat="m/d h:m" + date=from + maxDate=to + onChange=(action "fromDidChange") + onClose=(action "onTimePickerClose") + placeholder=(t 'metricsAction.from.label') + }} +
+
+ {{ember-flatpickr + disabled=state.loading + enableSeconds=true + dateFormat="m/d h:m" + enableTime=true + minDate=from + maxDate=now + date=to + onChange=(action "toDidChange") + onClose=(action "onTimePickerClose") + placeholder=(t 'metricsAction.to.label') + }} +
+ {{/if}} + +
+ {{#if allowDetail}} +
+ {{#if state.detail}} +
+
+ + +
+
+ {{else}} +
+
+ + +
+
+ {{/if}} +
+ {{/if}} +
+ diff --git a/lib/shared/addon/components/metrics-graph/component.js b/lib/shared/addon/components/metrics-graph/component.js new file mode 100644 index 000000000..d2d0c9ebb --- /dev/null +++ b/lib/shared/addon/components/metrics-graph/component.js @@ -0,0 +1,30 @@ +import Component from '@ember/component'; +import layout from './template'; +import { inject as service } from '@ember/service'; +import { set, get, observer } from '@ember/object'; + +export default Component.extend({ + settings: service(), + + layout, + + rows: null, + graphs: null, + loading: null, + noGraphs: null, + noDataLabel: 'generic.noData', + + graphsDidChange: observer('graphs', function() { + let out = []; + const graphs = (get(this, 'graphs') || []); + + graphs.forEach((graph, index) => { + if (index % 3 === 0) { + out.pushObject([graph]); + } else { + get(out, 'lastObject').pushObject(graph); + } + }); + set(this, 'rows', out); + }), +}); diff --git a/lib/shared/addon/components/metrics-graph/template.hbs b/lib/shared/addon/components/metrics-graph/template.hbs new file mode 100644 index 000000000..e7d5ba289 --- /dev/null +++ b/lib/shared/addon/components/metrics-graph/template.hbs @@ -0,0 +1,28 @@ +{{#each rows as |row|}} +
+ {{#each row as |item|}} +
+

{{t (concat 'metrics.' item.graph.title)}}

+ {{graph-area + series=item.series + loading=loading + formatter=item.graph.unit + }} +
+ {{/each}} +
+{{else}} + {{#if noGraphs}} +
+
+ {{t noDataLabel appName=settings.appName}} +
+
+ {{else}} +
+
+ +
+
+ {{/if}} +{{/each}} \ No newline at end of file diff --git a/lib/shared/addon/components/metrics-summary/component.js b/lib/shared/addon/components/metrics-summary/component.js new file mode 100644 index 000000000..ecab67a00 --- /dev/null +++ b/lib/shared/addon/components/metrics-summary/component.js @@ -0,0 +1,7 @@ +import Component from '@ember/component'; +import layout from './template' +export default Component.extend({ + layout, + + title: null, +}); diff --git a/lib/shared/addon/components/metrics-summary/template.hbs b/lib/shared/addon/components/metrics-summary/template.hbs new file mode 100644 index 000000000..0368572bb --- /dev/null +++ b/lib/shared/addon/components/metrics-summary/template.hbs @@ -0,0 +1,11 @@ +{{#accordion-list-item + grafanaUrl=grafanaUrl + title=title + detail=(t 'metricsAction.description') + expandAll=expandAll + expand=(action expandFn) + componentName='sortable-table' + as | parent | +}} + {{yield this}} +{{/accordion-list-item}} \ No newline at end of file diff --git a/app/components/node-ip/component.js b/lib/shared/addon/components/node-ip/component.js similarity index 100% rename from app/components/node-ip/component.js rename to lib/shared/addon/components/node-ip/component.js diff --git a/app/components/node-ip/template.hbs b/lib/shared/addon/components/node-ip/template.hbs similarity index 100% rename from app/components/node-ip/template.hbs rename to lib/shared/addon/components/node-ip/template.hbs diff --git a/app/components/percent-gauge/component.js b/lib/shared/addon/components/percent-gauge/component.js similarity index 99% rename from app/components/percent-gauge/component.js rename to lib/shared/addon/components/percent-gauge/component.js index 9e05bada6..8485778c4 100644 --- a/app/components/percent-gauge/component.js +++ b/lib/shared/addon/components/percent-gauge/component.js @@ -50,10 +50,10 @@ export default Component.extend(ThrottledResize, { updateTicks: observer('ticks.@each.{label,value}', function() { get(this, 'svg').updateTicks(get(this, 'ticks')); }), + onResize() { if ( get(this, 'svg') && get(this, 'ready') ) { get(this, 'svg').fit(); } - }, - + } }); diff --git a/app/components/percent-gauge/template.hbs b/lib/shared/addon/components/percent-gauge/template.hbs similarity index 100% rename from app/components/percent-gauge/template.hbs rename to lib/shared/addon/components/percent-gauge/template.hbs diff --git a/lib/shared/addon/components/progress-bar/component.js b/lib/shared/addon/components/progress-bar/component.js new file mode 100644 index 000000000..312442802 --- /dev/null +++ b/lib/shared/addon/components/progress-bar/component.js @@ -0,0 +1,53 @@ +import Component from '@ember/component'; +import { computed, get, observer } from '@ember/object'; +import layout from './template'; + +export default Component.extend({ + layout, + + color: '', + min: 0, + value: 0, + max: 100, + zIndex: null, + + didInsertElement() { + this.percentDidChange(); + this.zIndexDidChange(); + }, + + percentDidChange: observer('percent', function() { + this.$('.progress-bar').css('width', `${ get(this, 'percent') }%`); + }), + + zIndexDidChange: observer('zIndex', function() { + this.$().css('zIndex', get(this, 'zIndex') || 'inherit'); + }), + + tooltipContent: computed('percent', function() { + return `${ get(this, 'percent') } %`; + }), + + percent: computed('min', 'max', 'value', function() { + var min = get(this, 'min'); + var max = get(this, 'max'); + var value = Math.max(min, Math.min(max, get(this, 'value'))); + + var per = value / (max - min) * 100; // Percent 0-100 + + per = Math.round(per * 100) / 100; // Round to 2 decimal places + + return per; + }), + + colorClass: computed('color', function() { + var color = get(this, 'color'); + + if ( !color ) { + return; + } + + return `progress-bar-${ color.replace(/^progress-bar-/, '') }`; + }), + +}); diff --git a/lib/shared/addon/components/progress-bar/template.hbs b/lib/shared/addon/components/progress-bar/template.hbs new file mode 100644 index 000000000..b6f7c2855 --- /dev/null +++ b/lib/shared/addon/components/progress-bar/template.hbs @@ -0,0 +1,6 @@ + +{{#tooltip-element type="tooltip-basic" model=tooltipContent tooltipTemplate='tooltip-static' aria-describedby="tooltip-base" inlineBlock=false}} +
+
+
+{{/tooltip-element}} \ No newline at end of file diff --git a/app/components/resource-condition-list/component.js b/lib/shared/addon/components/resource-condition-list/component.js similarity index 100% rename from app/components/resource-condition-list/component.js rename to lib/shared/addon/components/resource-condition-list/component.js diff --git a/app/components/resource-condition-list/template.hbs b/lib/shared/addon/components/resource-condition-list/template.hbs similarity index 100% rename from app/components/resource-condition-list/template.hbs rename to lib/shared/addon/components/resource-condition-list/template.hbs diff --git a/lib/shared/addon/grafana/service.js b/lib/shared/addon/grafana/service.js new file mode 100644 index 000000000..be5081a57 --- /dev/null +++ b/lib/shared/addon/grafana/service.js @@ -0,0 +1,104 @@ +import { observer } from '@ember/object'; +import Service, { inject as service } from '@ember/service'; +import { get, set } from '@ember/object'; +import { on } from '@ember/object/evented'; + +const GRAFANA_LINKS = [ + { + id: 'etcd', + title: 'Etcd' + }, + { + id: 'scheduler', + title: 'Kubernetes Components' + }, + { + id: 'controller', + title: 'Kubernetes Components' + }, + { + id: 'nodes', + title: 'Nodes' + }, + { + id: 'k8s', + title: 'Kubernetes Components' + }, + { + id: 'rancher', + title: 'Rancher Components' + }, +]; + +export default Service.extend({ + scope: service(), + globalStore: service(), + + grafanaLinks: GRAFANA_LINKS, + dashboards: null, + + updateLinks() { + ( get(this, 'grafanaLinks') || [] ).forEach((link) => { + const dashboards = get(this, 'dashboards') || []; + const target = dashboards.findBy('title', get(link, 'title')); + + if ( target ) { + const grafanaUrl = `${ get(this, 'scope.currentCluster.monitoringStatus.grafanaEndpoint') }${ get(target, 'url') }`; + + set(this, `${ get(link, 'id') }Url`, grafanaUrl); + } else { + set(this, `${ get(link, 'id') }Url`, null); + } + }); + }, + + monitoringStatusDidChange: on('init', observer('scope.currentCluster.isMonitoringReady', 'scope.currentProject.isMonitoringReady', function() { + const found = get(this, 'globalStore').all('project').findBy('isSystemProject', true); + + if (found ) { + const isClusterReady = get(this, 'scope.currentCluster.isMonitoringReady'); + + if ( isClusterReady ) { + const rootUrl = get(this, 'scope.currentCluster.monitoringStatus.grafanaEndpoint'); + + get(this, 'globalStore').rawRequest({ + url: `${ rootUrl }api/search`, + method: 'GET', + }).then((xhr) => { + if (this.isDestroyed || this.isDestroying) { + return; + } + + const dashboards = xhr.body || []; + + set(this, 'dashboards', dashboards); + this.updateLinks(); + }); + } else { + set(this, 'dashboards', []); + this.updateLinks(); + } + } else { + const isProjectReady = get(this, 'scope.currentProject.isMonitoringReady'); + + if ( isProjectReady ) { + const rootUrl = get(this, 'scope.currentProject.monitoringStatus.grafanaEndpoint'); + + get(this, 'globalStore').rawRequest({ + url: `${ rootUrl }api/search`, + method: 'GET', + }).then((xhr) => { + if (this.isDestroyed || this.isDestroying) { + return; + } + + const dashboards = xhr.body || []; + + set(this, 'dashboards', dashboards); + }); + } else { + set(this, 'dashboards', []); + } + } + })), +}); diff --git a/lib/shared/addon/mixins/cattle-transitioning-resource.js b/lib/shared/addon/mixins/cattle-transitioning-resource.js index 3cafa8dd8..1761046ad 100644 --- a/lib/shared/addon/mixins/cattle-transitioning-resource.js +++ b/lib/shared/addon/mixins/cattle-transitioning-resource.js @@ -268,7 +268,7 @@ export default Mixin.create({ return []; }), - _availableActions: computed('availableActions.[]', 'links.{self,yaml}', 'canEdit', 'canEditYaml', 'canViewYaml', 'canRemove', function() { + _availableActions: computed('availableActions.[]', 'links.{self,yaml}', 'canEdit', 'canEditYaml', 'canViewYaml', 'canRemove', 'grafanaUrl', function() { const out = get(this, 'availableActions').slice(); let nextSort = 1; @@ -340,10 +340,18 @@ export default Mixin.create({ out.push({ sort: 99, + label: 'action.viewInGrafana', + icon: 'icon icon-link', + action: 'goToGrafana', + enabled: !!get(this, 'grafanaUrl'), + }); + + out.push({ + sort: 100, divider: true }); out.push({ - sort: 100, + sort: 101, label: 'action.remove', icon: 'icon icon-trash', action: 'promptDelete', @@ -423,6 +431,12 @@ export default Mixin.create({ window.open(url, '_blank'); }, + + goToGrafana() { + let url = get(this, 'grafanaUrl'); + + window.open(url, '_blank'); + }, }, displayName: computed('name', 'id', function() { diff --git a/lib/shared/addon/mixins/grafana.js b/lib/shared/addon/mixins/grafana.js new file mode 100644 index 000000000..18379f795 --- /dev/null +++ b/lib/shared/addon/mixins/grafana.js @@ -0,0 +1,52 @@ +import Mixin from '@ember/object/mixin'; +import { get, computed } from '@ember/object'; +import { inject as service } from '@ember/service'; + +export default Mixin.create({ + globalStore: service(), + scope: service(), + grafana: service(), + + grafanaUrl: computed('grafana.dashboards', function() { + let dashboardName = get(this, 'baseType') === 'workload' ? (get(this, 'type') || '').capitalize() : get(this, 'grafanaDashboardName'); + + const dashboard = (get(this, 'grafana.dashboards') || []).findBy('title', dashboardName); + + if (!dashboard) { + return; + } + + const found = get(this, 'globalStore').all('project').findBy('isSystemProject', true); + + let grafanaUrl; + + if ( found ) { + grafanaUrl = `${ get(this, 'scope.currentCluster.monitoringStatus.grafanaEndpoint') }${ dashboard.url }`; + } else { + grafanaUrl = `${ get(this, 'scope.currentProject.monitoringStatus.grafanaEndpoint') }${ dashboard.url }`; + } + + switch (get(this, 'type')) { + case 'node': + grafanaUrl += `?var-node=${ get(this, 'grafanaResourceId') }&var-port=9100`; + break; + case 'deployment': + grafanaUrl += `?var-deployment_namespace=${ get(this, 'namespaceId') }&var-deployment_name=${ get(this, 'grafanaResourceId') }`; + break; + case 'daemonSet': + grafanaUrl += `?var-daemonset_namespace=${ get(this, 'namespaceId') }&var-daemonset_name=${ get(this, 'grafanaResourceId') }`; + break; + case 'statefulSet': + grafanaUrl += `?var-statefulset_namespace=${ get(this, 'namespaceId') }&var-statefulset_name=${ get(this, 'grafanaResourceId') }`; + break; + case 'pod': + grafanaUrl += `?var-namespace=${ get(this, 'namespaceId') }&var-pod=${ get(this, 'grafanaResourceId') }&var-container=All`; + break; + case 'container': + grafanaUrl += `?var-namespace=${ get(this, 'namespaceId') }&var-pod=${ get(this, 'podName') }&var-container=${ get(this, 'grafanaResourceId') }`; + break; + } + + return grafanaUrl; + }), +}); \ No newline at end of file diff --git a/lib/shared/addon/mixins/metrics.js b/lib/shared/addon/mixins/metrics.js new file mode 100644 index 000000000..37e03afe9 --- /dev/null +++ b/lib/shared/addon/mixins/metrics.js @@ -0,0 +1,351 @@ +import Mixin from '@ember/object/mixin'; +import { get, set, setProperties } from '@ember/object'; +import { inject as service } from '@ember/service'; +const SINGLE_METRICS = ['etcd-leader-change', 'etcd-server-leader-sum', 'etcd-server-failed-proposal', 'ingresscontroller-upstream-response-seconds']; +const GRAPHS = { + 'apiserver-request-count': { + 'priority': 301, + 'unit': 'number' + }, + 'apiserver-request-latency': { + 'priority': 300, + 'unit': 'ms' + }, + 'cluster-cpu-load': { + 'priority': 101, + 'unit': 'number' + }, + 'cluster-cpu-usage': { + 'priority': 100, + 'unit': 'percent' + }, + 'cluster-disk-io': { + 'priority': 104, + 'unit': 'kbps' + }, + 'cluster-fs-usage-percent': { + 'priority': 103, + 'unit': 'percent' + }, + 'cluster-memory-usage': { + 'priority': 102, + 'unit': 'percent' + }, + 'cluster-network-io': { + 'priority': 106, + 'unit': 'kbps' + }, + 'cluster-network-packet': { + 'priority': 105, + 'unit': 'pps' + }, + 'controllermanager-queue-depth': { + 'priority': 310, + 'unit': 'number' + }, + 'etcd-db-bytes-sum': { + 'priority': 204, + 'unit': 'byte' + }, + 'etcd-disk-operate': { + 'priority': 209, + 'unit': 'seconds' + }, + 'etcd-grpc-client': { + 'priority': 203, + 'unit': 'kbps' + }, + 'etcd-leader-change': { + 'priority': 202, + 'unit': 'number' + }, + 'etcd-peer-traffic': { + 'priority': 206, + 'unit': 'kbps' + }, + 'etcd-raft-proposals': { + 'priority': 207, + 'unit': 'number' + }, + 'etcd-rpc-rate': { + 'priority': 208, + 'unit': 'ops' + }, + 'etcd-server-failed-proposal': { + 'priority': 201, + 'unit': 'number' + }, + 'etcd-server-leader-sum': { + 'priority': 200, + 'unit': 'number' + }, + 'etcd-stream': { + 'priority': 205, + 'unit': 'number' + }, + 'etcd-sync-duration': { + 'priority': 209, + 'unit': 'seconds' + }, + 'fluentd-buffer-queue-length': { + 'priority': 301, + 'unit': 'number' + }, + 'fluentd-input-record-number': { + 'priority': 300, + 'unit': 'number' + }, + 'fluentd-output-errors': { + 'priority': 301, + 'unit': 'number' + }, + 'fluentd-output-record-number': { + 'priority': 301, + 'unit': 'number' + }, + 'ingresscontroller-nginx-connection': { + 'priority': 330, + 'unit': 'number' + }, + 'ingresscontroller-request-process-time': { + 'priority': 331, + 'unit': 'seconds' + }, + 'ingresscontroller-upstream-response-seconds': { + 'priority': 332, + 'unit': 'seconds' + }, + 'node-cpu-load': { + 'priority': 501, + 'unit': 'number' + }, + 'node-cpu-usage': { + 'priority': 500, + 'unit': 'percent' + }, + 'node-disk-io': { + 'priority': 504, + 'unit': 'kbps' + }, + 'node-fs-usage-percent': { + 'priority': 503, + 'unit': 'percent' + }, + 'node-memory-usage': { + 'priority': 502, + 'unit': 'percent' + }, + 'node-network-io': { + 'priority': 506, + 'unit': 'kbps' + }, + 'node-network-packet': { + 'priority': 505, + 'unit': 'pps' + }, + 'scheduler-e-2-e-scheduling-latency-seconds-quantile': { + 'priority': 320, + 'unit': 'seconds' + }, + 'scheduler-pod-unscheduler': { + 'priority': 322, + 'unit': 'number' + }, + 'scheduler-total-preemption-attempts': { + 'priority': 321, + 'unit': 'number' + }, + 'container-cpu-usage': { + 'priority': 800, + 'unit': 'mcpu' + }, + 'container-disk-io': { + 'priority': 804, + 'unit': 'kbps' + }, + 'container-memory-usage-bytes-sum': { + 'priority': 801, + 'unit': 'byte' + }, + 'container-network-io': { + 'priority': 803, + 'unit': 'kbps' + }, + 'container-network-packet': { + 'priority': 802, + 'unit': 'pps' + }, + 'pod-cpu-usage': { + 'priority': 700, + 'unit': 'mcpu' + }, + 'pod-disk-io': { + 'priority': 704, + 'unit': 'kbps' + }, + 'pod-memory-usage-bytes-sum': { + 'priority': 701, + 'unit': 'byte' + }, + 'pod-network-io': { + 'priority': 703, + 'unit': 'kbps' + }, + 'pod-network-packet': { + 'priority': 702, + 'unit': 'pps' + }, + 'workload-cpu-usage': { + 'priority': 600, + 'unit': 'mcpu' + }, + 'workload-disk-io': { + 'priority': 604, + 'unit': 'kbps' + }, + 'workload-memory-usage-bytes-sum': { + 'priority': 601, + 'unit': 'byte' + }, + 'workload-network-io': { + 'priority': 603, + 'unit': 'kbps' + }, + 'workload-network-packet': { + 'priority': 602, + 'unit': 'pps' + } +} + + +export default Mixin.create({ + globalStore: service(), + scope: service(), + growl: service(), + + filters: null, + graphs: null, + state: null, + projectScope: false, + metricParams: null, + + init() { + this._super(...arguments); + + set(this, 'state', { + loading: false, + detail: true, + noGraphs: false, + from: null, + to: null, + interval: null, + }) + }, + + updateData(out) { + const single = []; + const graphs = []; + + out.sortBy('graph.priority').forEach((item) => { + if ( SINGLE_METRICS.indexOf(get(item, 'graph.title')) > -1 ) { + single.push(item); + } else { + graphs.push(item); + } + }) + + setProperties(this, { + 'state.noGraphs': get(graphs, 'length') === 0, + graphs, + single + }); + }, + + actions: { + query(){ + const gs = get(this, 'globalStore'); + + set(this, 'state.loading', true); + + let metricParams = {}; + + if ( get(this, 'resourceId') ) { + if ( get(this, 'metricParams') ) { + metricParams = get(this, 'metricParams'); + } else { + set(metricParams, 'instance', get(this, 'resourceId')); + } + } + + let url; + + if ( get(this, 'projectScope') ) { + url = '/v3/projectmonitorgraphs?action=query'; + } else { + url = '/v3/clustermonitorgraphs?action=query'; + } + + const filters = get(this, 'filters') || {}; + + const cluster = get(this, 'scope.currentCluster.id'); + const project = get(this, 'scope.currentProject.id'); + + if ( project ) { + set(filters, 'projectId', project); + } else if (cluster) { + set(filters, 'clusterId', cluster); + } + + gs.rawRequest({ + url, + method: 'POST', + data: { + filters, + metricParams, + interval: get(this, 'state.interval'), + isDetails: get(this, 'state.detail'), + from: get(this, 'state.from'), + to: get(this, 'state.to'), + } + }).then((metrics) => { + if (this.isDestroyed || this.isDestroying) { + return; + } + + const metricsBody = get(JSON.parse(get(metrics, 'body')) || {}, 'data') || []; + const out = metricsBody.map((metric) => { + const title = get(metric, 'graphID').split(':')[1]; + const graph = GRAPHS[title]; + + if ( graph ) { + set(graph, 'title', title); + } + + return { + graph, + series: get(metric, 'series') || [] + } + }) + + this.updateData(out); + }).catch((err) => { + if (this.isDestroyed || this.isDestroying) { + return; + } + + setProperties(this, { + 'state.noGraphs': true, + graphs: [], + single: [] + }); + get(this, 'growl').fromError(get(err, 'body.message') || get(err, 'message') || err); + }).finally(() => { + if (this.isDestroyed || this.isDestroying) { + return; + } + + set(this, 'state.loading', false); + }); + } + } +}); diff --git a/lib/shared/addon/mixins/resource-usage.js b/lib/shared/addon/mixins/resource-usage.js index ad2f0b2ec..5da9608b6 100644 --- a/lib/shared/addon/mixins/resource-usage.js +++ b/lib/shared/addon/mixins/resource-usage.js @@ -7,6 +7,32 @@ import { inject as service } from '@ember/service'; export default Mixin.create({ intl: service(), + cpuTotal: computed('allocatable.cpu', function() { + const total = parseSi(get(this, 'allocatable.cpu')); + + if ( total ) { + const minExp = exponentNeeded(total); + const totalStr = formatSi(total, 1000, '', '', 0, minExp, 1); + + return `${ totalStr } Core${ totalStr === '1' ? '' : 's' }`; + } else { + return null; + } + }), + + memoryTotal: computed('allocatable.memory', function() { + const total = parseSi(get(this, 'allocatable.memory')); + + if ( total ) { + const minExp = exponentNeeded(total); + const totalStr = formatSi(total, 1024, 'iB', 'B', 0, minExp, 1); + + return totalStr; + } else { + return null; + } + }), + cpuUsage: computed('requested.cpu', 'allocatable.cpu', function() { const used = parseSi(get(this, 'requested.cpu')) || 0; const total = parseSi(get(this, 'allocatable.cpu')); diff --git a/lib/shared/addon/utils/component-badge.js b/lib/shared/addon/utils/component-badge.js deleted file mode 100644 index eafc72e0d..000000000 --- a/lib/shared/addon/utils/component-badge.js +++ /dev/null @@ -1,78 +0,0 @@ -import { select } from 'd3'; - -export default function initGraph(options) { - const { - el, r, width, height, margin, fontSize - } = getConfig(options); - const svg = select(el).append('svg').attr('width', width).attr('height', height); - - const healthyStatus = options.healthy ? 'healthy' : 'unhealthy'; - - const rectangle = svg.append('rect').attr('x', r).attr('y', margin).attr('width', width) - .attr('height', height - margin * 2) - .attr('class', `${ healthyStatus } background`); - - const circle = svg.append('circle').attr('cx', r).attr('cy', r).attr('r', r) - .attr('class', `${ healthyStatus } circle`); - - const text = svg.append('svg:text') - .attr('x', 3 * r) - .attr('y', height / 2) - .attr('dy', fontSize / 2) - .attr('text-anchor', 'start') - .text(options.component) - .style('font-size', `${ fontSize }px`) - .attr('class', `${ healthyStatus } text`); - - const icon = svg.append('image') - .attr('x', 0.5 * r) - .attr('y', 0.5 * r) - .attr('width', r) - .attr('height', r) - .attr('xlink:href', options.healthy ? '/assets/images/checkmark.svg' : '/assets/images/flag.svg'); - - return { - updateHealthStatus(healthy) { - rectangle.attr('class', `${ healthyStatus } background`); - circle.attr('class', `${ healthyStatus } circle`); - text.attr('class', `${ healthyStatus } text`); - icon.attr('xlink:href', healthy ? '/assets/images/checkmark.svg' : '/assets/images/flag.svg'); - }, - - fit() { - fit(svg, el, rectangle, circle, text, icon); - }, - }; -} - -function fit(svg, el, rectangle, circle, text, icon) { - const { - r, width, height, margin, fontSize - } = getConfig({ el }); - - svg.attr('width', width).attr('height', height); - rectangle.attr('x', r).attr('y', margin).attr('width', width).attr('height', height - margin * 2); - circle.attr('cx', r).attr('cy', r).attr('r', r); - text.attr('x', 3 * r).attr('y', height / 2).attr('dy', fontSize / 2).style('font-size', `${ fontSize }px`); - icon.attr('x', 0.5 * r).attr('y', 0.5 * r).attr('width', r).attr('height', r); -} - -function getWidth(el) { - const width = el.parentNode.offsetWidth; - - return width > 0 ? width : 0; -} - -function getConfig(options) { - const width = getWidth(options.el); - const height = width / 2.5; - - return { - el: options.el, - r: height / 8, - fontSize: width / 25, - margin: height / 30, - width, - height: height / 4, - }; -} diff --git a/lib/shared/addon/utils/constants.js b/lib/shared/addon/utils/constants.js index d6ad59e89..c12a7319a 100644 --- a/lib/shared/addon/utils/constants.js +++ b/lib/shared/addon/utils/constants.js @@ -10,6 +10,8 @@ var C = { CATALOG: { LIBRARY_KEY: 'library', LIBRARY_VALUE: 'https://git.rancher.io/charts', + SYSTEM_LIBRARY_KEY: 'system-library', + SYSTEM_LIBRARY_VALUE: 'https://git.rancher.io/system-charts', HELM_STABLE_KEY: 'helm', HELM_STABLE_VALUE: 'https://kubernetes-charts.storage.googleapis.com/', HELM_INCUBATOR_KEY: 'helm-incubator', @@ -180,6 +182,7 @@ var C = { LAST_SCALE_MODE: 'last-scale-mode', LAST_IMAGE_PULL_POLICY: 'last-image-pull-policy', WRAP_LINES: 'wrap-lines', + PERIOD: 'period', LAST_NAMESPACE: 'last-namespace', SORT_STACKS_BY: 'sort-stacks-by', TABLE_COUNT: 'table-count', diff --git a/lib/shared/addon/utils/graph-area.js b/lib/shared/addon/utils/graph-area.js deleted file mode 100644 index cda05207d..000000000 --- a/lib/shared/addon/utils/graph-area.js +++ /dev/null @@ -1,298 +0,0 @@ -import { GRADIENT_COLORS } from 'shared/components/svg-gradients/component'; -import { - formatPercent, - formatMib, - formatKbps -} from 'shared/utils/util'; -import initTooltip from 'shared/utils/graph-tooltip'; -import { - min as d3Min, - max as d3Max, - select as d3Select, - scale as d3Scale, - svg as d3Svg -} from 'd3'; - - -const FORMATTERS = { - value: (value) => value, - percent: formatPercent, - mib: formatMib, - kbps: formatKbps -}; -const DEFAULT_MARGIN = { - top: 5, - right: 20, - bottom: 5, - left: 75 -}; -const DEFAULT_MAX_POINTS = 60; -const DEFAULT_DURATION = 1000; -const DEFAULT_Y_TICKS = 5; -const DEFAULT_HEIGHT = 190; -const DEFAULT_INTERVAL = 1000; - -export default function initGraph(options) { - const { - el, margin, width, height, yTicks, duration, - maxPoints, formatter, fields, gradient, interpolate, min, query, interval - } = getConfig(options); - - const graph = d3Select(el).append('svg') - .attr('width', width + margin.left + margin.right) - .attr('height', height + margin.top + margin.bottom); - - const svg = graph.append('g').attr('transform', `translate(${ margin.left },${ margin.top })`); - - const x = d3Scale.linear().domain([0, maxPoints - 1]).range([0, width]); - const y = d3Scale.linear().domain([0, 1]).range([height, 0]); - - const line = d3Svg.line().interpolate(interpolate) - .defined((d) => typeof d === 'number') - .x((d, i) => x(i)) - .y((d) => y(d)); - const area = d3Svg.area().interpolate(interpolate) - .defined((d) => typeof d === 'number') - .x((d, i) => x(i)) - .y0(height) - .y1((d) => y(d)); - - const yAxis = d3Svg.axis().scale(y).orient('left') - .ticks(yTicks) - .tickFormat(FORMATTERS[formatter]); - const yAxisG = svg.append('g').attr('class', 'y axis').call(yAxis); - - const clipPath = svg.append('defs').append('clipPath').attr('id', 'clip') - .append('rect') - .attr('width', width) - .attr('height', height); - - const stripes = drawStripes(svg, width, y, yTicks); - - const series = getSeries(fields, svg, gradient); - - const tooltip = initTooltip({ - el, - svg, - height, - margin, - maxPoints, - duration, - formatter: FORMATTERS[formatter], - x, - gradient: GRADIENT_COLORS[gradient] - }); - - const params = { - svg, - el, - x, - y, - min, - margin, - height, - line, - area, - yAxisG, - yAxis, - yTicks, - stripes, - options, - maxPoints, - query, - series, - duration, - tooltip, - } - - let intervalId - - return { - start() { - render(params); - intervalId = setInterval(() => { - render(params); - }, interval); - }, - fit() { - fit({ - el, - x, - margin, - graph, - clipPath, - stripes: params.stripes - }); - }, - destory() { - clearInterval(intervalId); - } - }; -} - -function render(params) { - let all = []; - const series = []; - - params.series.forEach((serie) => { - const data = query(params.maxPoints, params.query, serie.field); - - all = all.concat(data) - series.push({ - field: serie.field, - lineChart: serie.lineChart, - areaChart: serie.areaChart, - data - }) - }) - - updateTooltip(series, params) - updateAxis(all.filter((d) => d !== null), params) - updateLines(series, params) -} - -function fit(params) { - const margin = params.margin; - const width = getWidth(params.el, margin); - - params.x.range([0, width]); - params.graph.attr('width', width + margin.left + margin.right); - params.clipPath.attr('width', width); - params.stripes.attr('width', width); -} - -function query(maxPoints, query, field) { - let data = query(field.key) || [] - - if (data.length < maxPoints) { - const len = data.length - - for (let i = 0; i < maxPoints - len; i++) { - data.unshift(null) - } - } else if (data.length > maxPoints) { - data = data.slice(-1 * maxPoints); - } - - return data; -} - -function adjustMax(dataMax, options) { - let optMinMax = options.minMax; - let optMax = options.max; - let optScaleDown = options.scaleDown; - let observedMax = options.observedMax; - let out = dataMax; - - if (optMax) { - out = optMax; - } else if (optMinMax) { - out = Math.max(optMinMax, out); - } - - if (observedMax && !optScaleDown) { - out = Math.max(observedMax, out); - } - - if (!observedMax && out > 0 && options.maxDoubleInital) { - out *= 2; - } - options.observedMax = out; - - return out; -} - -function updateTooltip(series, params) { - params.tooltip.update(series) -} - -function updateAxis(all, params) { - if (all.length === 0) { - return; - } - const min = params.min === null ? d3Min(all) : params.min; - const max = adjustMax(d3Max(all), params.options); - const update = params.y.domain()[0] !== min || params.y.domain()[1] !== max - - params.y.domain([min, max]); - params.y.range([params.height - 2, 2]); - params.y.rangeRound([params.height - 2, 2]); - params.yAxisG.call(params.yAxis); - if (update) { - updateStripes(params); - } -} - -function updateLines(series, params) { - series.forEach((serie) => { - serie.areaChart.attr('d', params.area(serie.data)) - serie.lineChart.attr('d', params.line(serie.data)) - }) -} - -function drawStripes(svg, width, y, yTicks) { - return svg.selectAll('rect.y') - .data(y.ticks(yTicks)) - .enter().append('rect') - .attr('x', 0) - .attr('width', width) - .attr('y', y) - .attr('height', (d, i) => i === 0 ? 0 : y(y.ticks(yTicks)[i - 1]) - y(d)) - .attr('class', (d, i) => i % 2 === 0 ? 'even' : 'odd') - .style('fill-opacity', 0.1); -} - -function updateStripes(params) { - params.stripes.remove(); - params.stripes = drawStripes(params.svg, getWidth(params.el, params.margin), params.y, params.yTicks); -} - -function getWidth(el, margin) { - const width = el.parentNode.offsetWidth - margin.left - margin.right - - return width > 0 ? width : 0; -} - -function getSeries(fields, svg, gradient) { - return fields.map((field, i) => { - return { - field, - lineChart: svg.append('g').attr('clip-path', 'url(#clip)') - .append('path').attr('class', 'line') - .style('stroke', GRADIENT_COLORS[gradient][i]), - areaChart: svg.append('g').attr('clip-path', 'url(#clip)') - .append('path').attr('class', 'area') - .style('fill', `url(${ window.location.pathname }#${ gradient }-${ i }-gradient)`) - } - }); -} - -function getConfig(options) { - const el = options.el; - const chartHeight = options.height || DEFAULT_HEIGHT - const margin = options.margin || DEFAULT_MARGIN - const intl = window.l('service:intl'); - const fields = (options.fields || []).map((field) => { - return { - key: field.key, - displayName: intl.t(field.displayName) - } - }); - - return { - el, - margin, - width: getWidth(el, margin), - height: chartHeight - margin.top - margin.bottom, - yTicks: options.yTicks || DEFAULT_Y_TICKS, - duration: options.duration || DEFAULT_DURATION, - maxPoints: options.maxPoints || DEFAULT_MAX_POINTS, - interval: options.interval || DEFAULT_INTERVAL, - formatter: options.formatter || 'value', - fields, - gradient: options.gradient || 'memory', - interpolate: options.interpolate || 'basis', - min: options.min, - query: options.query, - } -} diff --git a/lib/shared/addon/utils/graph-tooltip.js b/lib/shared/addon/utils/graph-tooltip.js deleted file mode 100644 index 071b5d18b..000000000 --- a/lib/shared/addon/utils/graph-tooltip.js +++ /dev/null @@ -1,135 +0,0 @@ -import { select } from 'd3'; - -export default function initTooltip(options) { - const hoverLineGroup = options.svg.append('g') - .attr('class', 'hover-line'); - - const hoverLine = hoverLineGroup - .append('line') - .attr('x1', 10).attr('x2', 10) - .attr('y1', 0) - .attr('y2', options.height); - - hoverLine.classed('hide', true); - - const tooltip = select(options.el).append('div') - .attr('class', 'hover-label') - .style('opacity', 0); - - const params = { - currentIndex: -1, - maxPoints: options.maxPoints, - duration: options.duration, - formatter: options.formatter, - margin: options.margin, - height: options.height, - el: options.el, - x: options.x, - gradient: options.gradient, - series: [], - tooltip, - hoverLine, - } - - $(options.el).mouseleave(() => handleMouseOutGraph(params)); // eslint-disable-line - $(options.el).mousemove(event => handleMouseOverGraph(event, params)); // eslint-disable-line - - return { - update(series) { - params.series = series; - if (params.currentIndex > -1) { - tooltip.html(getTooltipCotent(params)); - } - } - } -} - -function handleMouseOverGraph(event, params) { - const margin = params.margin; - const mouseX = event.offsetX - margin.left; - const mouseY = event.offsetY - margin.top; - const width = params.el.parentNode.offsetWidth - margin.left - margin.right; - const hoverLine = params.hoverLine; - const tooltip = params.tooltip; - - if (mouseX > 0 && mouseX < width && mouseY > 0 && mouseY < params.height) { - hoverLine.classed('hide', false); - hoverLine.attr('x1', mouseX).attr('x2', mouseX) - tooltip.transition() - .duration(200) - .style('opacity', .9); - - const index = Math.round(params.x.invert(mouseX)); - - params.currentIndex = index; - tooltip.html(getTooltipCotent(params)) - .style('left', `${ event.pageX - margin.left - margin.right + 20 }px`) - .style('top', `${ event.pageY - 70 }px`); - } else { - handleMouseOutGraph(params) - } -} - -function formatValue(params) { - let div = ''; - - params.series.forEach((d, i) => { - const color = params.gradient[i]; - - div += ` - - - ${ d.field.displayName } - - - ${ params.formatter(d.data[params.currentIndex]) } - - ` - }) - - return div; -} - -function formatSecondsAgo(index, maxPoints, duration) { - const ago = Math.max(0, maxPoints - index - 1) * duration / 1000; - const intl = window.l('service:intl'); - - if (ago === 0) { - return intl.t('time.now'); - } - - if (ago >= 60) { - const min = Math.floor(ago / 60); - const sec = ago - 60 * min; - - if (sec > 0) { - return `${ intl.t('time.mins', { mins: min }) }, ${ intl.t('time.secsAgo', { secs: sec }) }}`; - } else { - return intl.t('time.minsAgo', { mins: min }); - } - } - - return intl.t('time.secsAgo', { secs: ago }); -} - -function getTooltipCotent(params) { - let div; - - div = - ` - - - - - ${ formatValue(params) } - -
${ formatSecondsAgo(params.currentIndex, params.maxPoints, params.duration) }
` - - return div; -} - -function handleMouseOutGraph(params) { - params.hoverLine.classed('hide', true); - params.tooltip.transition().duration(200).style('opacity', 0); - params.currentIndex = -1; -} diff --git a/lib/shared/addon/utils/percent-gauge.js b/lib/shared/addon/utils/percent-gauge.js index 6e8cb7965..f6f4b4912 100644 --- a/lib/shared/addon/utils/percent-gauge.js +++ b/lib/shared/addon/utils/percent-gauge.js @@ -267,7 +267,7 @@ function valueToPoint(width, height, margin, value, thickness) { } function getWidth(el) { - const width = el.parentNode.offsetWidth; + const width = el.parentNode.offsetWidth * 0.9; return width > 0 ? width : 0; } diff --git a/lib/shared/addon/utils/util.js b/lib/shared/addon/utils/util.js index f7bbddcca..613fcb09e 100644 --- a/lib/shared/addon/utils/util.js +++ b/lib/shared/addon/utils/util.js @@ -301,10 +301,28 @@ export function formatMib(value) { return formatSi(value, 1024, 'iB', 'B', 2); } +export function formatSecond(value) { + if ( value < 0.1 ) { + return `${ roundValue(value * 1000) } ms` + } else { + return `${ roundValue(value) } s` + } +} + export function formatKbps(value) { return formatSi(value, 1000, 'bps', 'Bps', 1); } +export function roundValue(value) { + if ( value < 1 ) { + return Math.round(value * 100) / 100; + } else if ( value < 10 ) { + return Math.round(value * 10) / 10; + } else { + return Math.round(value); + } +} + export function formatGB(inMB) { return formatSi(inMB, 1000, 'B', 'B', 2); } @@ -445,6 +463,7 @@ var Util = { formatGB, formatKbps, formatMib, + formatSecond, formatPercent, hostname, isBadTld, @@ -458,6 +477,7 @@ var Util = { random32, randomStr, removeEmpty, + roundValue, sortableNumericSuffix, strPad, stripScheme, diff --git a/lib/shared/app/components/annotations-section/component.js b/lib/shared/app/components/annotations-section/component.js new file mode 100644 index 000000000..6977d27c0 --- /dev/null +++ b/lib/shared/app/components/annotations-section/component.js @@ -0,0 +1 @@ +export { default } from 'shared/components/annotations-section/component'; \ No newline at end of file diff --git a/lib/shared/app/components/component-badge/component.js b/lib/shared/app/components/component-badge/component.js deleted file mode 100644 index 4b1a52f3b..000000000 --- a/lib/shared/app/components/component-badge/component.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from 'shared/components/component-badge/component'; diff --git a/lib/shared/app/components/labels-section/component.js b/lib/shared/app/components/labels-section/component.js new file mode 100644 index 000000000..e0f2c894e --- /dev/null +++ b/lib/shared/app/components/labels-section/component.js @@ -0,0 +1 @@ +export { default } from 'shared/components/labels-section/component'; \ No newline at end of file diff --git a/lib/shared/app/components/metrics-action/component.js b/lib/shared/app/components/metrics-action/component.js new file mode 100644 index 000000000..28d1587fd --- /dev/null +++ b/lib/shared/app/components/metrics-action/component.js @@ -0,0 +1 @@ +export { default } from 'shared/components/metrics-action/component'; \ No newline at end of file diff --git a/lib/shared/app/components/metrics-graph/component.js b/lib/shared/app/components/metrics-graph/component.js new file mode 100644 index 000000000..60c37e64c --- /dev/null +++ b/lib/shared/app/components/metrics-graph/component.js @@ -0,0 +1 @@ +export { default } from 'shared/components/metrics-graph/component'; \ No newline at end of file diff --git a/lib/shared/app/components/metrics-summary/component.js b/lib/shared/app/components/metrics-summary/component.js new file mode 100644 index 000000000..177f124d2 --- /dev/null +++ b/lib/shared/app/components/metrics-summary/component.js @@ -0,0 +1 @@ +export { default } from 'shared/components/metrics-summary/component'; \ No newline at end of file diff --git a/lib/shared/app/components/node-ip/component.js b/lib/shared/app/components/node-ip/component.js new file mode 100644 index 000000000..cb5bc1e79 --- /dev/null +++ b/lib/shared/app/components/node-ip/component.js @@ -0,0 +1 @@ +export { default } from 'shared/components/node-ip/component'; \ No newline at end of file diff --git a/lib/shared/app/components/percent-gauge/component.js b/lib/shared/app/components/percent-gauge/component.js new file mode 100644 index 000000000..0f7b21efe --- /dev/null +++ b/lib/shared/app/components/percent-gauge/component.js @@ -0,0 +1 @@ +export { default } from 'shared/components/percent-gauge/component'; \ No newline at end of file diff --git a/lib/shared/app/components/progress-bar/component.js b/lib/shared/app/components/progress-bar/component.js new file mode 100644 index 000000000..e51c0462d --- /dev/null +++ b/lib/shared/app/components/progress-bar/component.js @@ -0,0 +1 @@ +export { default } from 'shared/components/progress-bar/component'; \ No newline at end of file diff --git a/lib/shared/app/components/resource-condition-list/component.js b/lib/shared/app/components/resource-condition-list/component.js new file mode 100644 index 000000000..8f791c08a --- /dev/null +++ b/lib/shared/app/components/resource-condition-list/component.js @@ -0,0 +1 @@ +export { default } from 'shared/components/resource-condition-list/component'; \ No newline at end of file diff --git a/lib/shared/app/grafana/service.js b/lib/shared/app/grafana/service.js new file mode 100644 index 000000000..8d2d39f63 --- /dev/null +++ b/lib/shared/app/grafana/service.js @@ -0,0 +1 @@ +export { default } from 'shared/grafana/service'; \ No newline at end of file diff --git a/lib/shared/app/mixins/grafana.js b/lib/shared/app/mixins/grafana.js new file mode 100644 index 000000000..210b89eb9 --- /dev/null +++ b/lib/shared/app/mixins/grafana.js @@ -0,0 +1 @@ +export { default } from 'shared/mixins/grafana'; diff --git a/lib/shared/app/mixins/metrics.js b/lib/shared/app/mixins/metrics.js new file mode 100644 index 000000000..e5af1e326 --- /dev/null +++ b/lib/shared/app/mixins/metrics.js @@ -0,0 +1 @@ +export { default } from 'shared/mixins/metrics'; diff --git a/lib/shared/app/utils/component-badge.js b/lib/shared/app/utils/component-badge.js deleted file mode 100644 index 07aacc9eb..000000000 --- a/lib/shared/app/utils/component-badge.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from 'shared/utils/component-badge'; diff --git a/lib/shared/app/utils/graph-area.js b/lib/shared/app/utils/graph-area.js deleted file mode 100644 index 906ebb30f..000000000 --- a/lib/shared/app/utils/graph-area.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from 'shared/utils/graph-area'; diff --git a/lib/shared/app/utils/graph-tooltip.js b/lib/shared/app/utils/graph-tooltip.js deleted file mode 100644 index 0e3fcb745..000000000 --- a/lib/shared/app/utils/graph-tooltip.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from 'shared/utils/graph-tooltip'; diff --git a/lib/shared/app/utils/util.js b/lib/shared/app/utils/util.js index 1b75c0253..365dd52dd 100644 --- a/lib/shared/app/utils/util.js +++ b/lib/shared/app/utils/util.js @@ -12,6 +12,7 @@ export { absoluteUrl, addAuthorization, ucFirst, + roundValue, strPad, sortableNumericSuffix, timerFuzz, @@ -19,6 +20,7 @@ export { randomStr, formatPercent, formatMib, + formatSecond, formatKbps, formatGB, constructUrl, diff --git a/lib/shared/package.json b/lib/shared/package.json index faf030e22..c3dd33acd 100644 --- a/lib/shared/package.json +++ b/lib/shared/package.json @@ -5,6 +5,8 @@ ], "dependencies": { "async": "*", + "d3": "*", + "echarts": "*", "ember-api-store": "*", "ember-auto-import": "*", "ember-basic-dropdown": "*", @@ -13,6 +15,7 @@ "ember-cli-htmlbars": "*", "ember-cli-pagination": "*", "ember-math-helpers": "*", + "ember-flatpickr": "*", "ember-truth-helpers": "*", "ember-wormhole": "*", "file-saver": "*", diff --git a/package.json b/package.json index 9db2ec04c..f645e33cc 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "license": "Apache-2.0", "devDependencies": { "ansi_up": "^2.0.2", + "echarts": "4.1.0", "async": "2.1.2", "babel-eslint": "^9.0.0", "broccoli-asset-rev": "^2.7.0", @@ -65,6 +66,7 @@ "ember-load-initializers": "^1.1.0", "ember-math-helpers": "^2.2.3", "ember-maybe-import-regenerator": "^0.1.6", + "ember-flatpickr": "^2.7.0", "ember-resolver": "^4.5.5", "ember-source": "~3.3.0", "ember-truth-helpers": "1.3.0", @@ -115,6 +117,7 @@ "lib/login", "lib/nodes", "lib/pipeline", + "lib/monitoring", "lib/shared" ] } diff --git a/public/assets/images/providers/grafana.svg b/public/assets/images/providers/grafana.svg new file mode 100644 index 000000000..72702223d --- /dev/null +++ b/public/assets/images/providers/grafana.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + diff --git a/public/assets/images/providers/prometheus.svg b/public/assets/images/providers/prometheus.svg new file mode 100644 index 000000000..5c51f66d9 --- /dev/null +++ b/public/assets/images/providers/prometheus.svg @@ -0,0 +1,50 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/translations/en-us.yaml b/translations/en-us.yaml index dab9b20f9..4de296ea2 100644 --- a/translations/en-us.yaml +++ b/translations/en-us.yaml @@ -790,6 +790,8 @@ ingressPage: containerPage: header: 'Container: {name}' + initContainer: + label: initContainer envTab: header: Environment Variables detail: 'Environment Variables that were added at creation.' @@ -811,6 +813,7 @@ containerPage: noData: This container has no volumes mounted noMatch: No volumes match the current search noContainers: No other containers + pod: Pod podPage: header: 'Pod: {name}' nodeIp: Node @@ -833,25 +836,48 @@ containersPage: kubernetesLink: Install Kubernetes clusterDashboard: - title: Dashboard - cpu: CPU - memory: Memory - pods: Pods - subtitle: - reserved: "{used} of {total} reserved" - used: "{used} of {total} used" - node: Nodes - etcd: etcd - scheduler: Scheduler - controllerManager: Controller Manager - value: Value systemProject: This is the system project which has all Kubernetes and {appName} system namespaces. Changes made to resources in the system project may harm the cluster. notActive: This cluster is currently {state}. notReady: This cluster is currently {state}; areas that interact directly with it will not be available until the API is ready. - noNodes: There are no nodes. - alert: - node: "Alert: Node {node} is not active." - component: "Alert: Component {component} is unhealthy." +metricsAction: + description: Expand to see live metrics + sections: + workload: Workload Metrics + pod: Pod Metrics + container: Container Metrics + grafana: + label: Grafana + summary: + label: Summary + detail: + label: Detail + from: + label: From + to: + label: To + periods: + custom: Custom + 5m: Last 5 minutes + 1h: Last 1 hour + 6h: Last 6 hours + 24h: Last 24 hours + 7d: Last 7 days + 30d: Last 30 days + +metrics: + workload-cpu-usage: CPU Utilization + workload-disk-io: Disk I/O + workload-memory-usage-bytes-sum: Memory Utilization + workload-network-io: Network I/O + workload-network-packet: Network Packets + pod-cpu-usage: CPU Utilization + pod-disk-io: Disk I/O + pod-memory-usage-bytes-sum: Memory Utilization + pod-network-io: Network I/O + pod-network-packet: Network Packets + container-cpu-usage: CPU Utilization + container-disk-io: Disk I/O + container-memory-usage-bytes-sum: Memory Utilization dnsPage: noMatch: No records match the current search @@ -1075,7 +1101,7 @@ hostsPage: kubeProxyVersion: Kube Proxy Version dockerVersion: Docker Version kernelVersion: Kernel Version - operatingSystem: Operating System + operatingSystem: OS conditions: diskSpace: Disk Space diskPressure: Disk Pressure @@ -1235,14 +1261,14 @@ configMapsPage: servicePage: header: 'Workload: {name}' globalScale: '{scale} per host' - endpoints: 'Endpoints:' + endpoints: 'Endpoints' multistat: - type: 'Workload Type:' - fqdn: 'FQDN:' - scale: 'Scale:' - image: 'Image:' - namespace: 'Namespace:' - created: 'Created:' + type: 'Workload Type' + fqdn: 'FQDN' + scale: 'Scale' + image: 'Image' + namespace: 'Namespace' + created: 'Created' daemonSetScale: '1 per node' serviceType: deployment: Deployment @@ -3783,6 +3809,20 @@ formNetwork: label: IP Address placeholder: e.g. 192.168.0.1 +formCustomMetrics: + title: Custom Metrics + detail: Expose the endpoints for Prometheus to collect custom metrics + port: + label: Container Port + placeholder: e.g. 8080 + path: + label: Path + placeholder: e.g. /metrics + protocol: + label: Protocol + noPorts: No Endpoints + addActionLabel: Add Metrics Endpoint + formPorts: header: Port Mapping addAction: Add Port @@ -4392,24 +4432,6 @@ hostPod: identityBlock: loading: Loading... -infoMultiStats: - connecting: Connecting... - utilizationStats: Utilization stats are only available while active/running. - cpuSection: - labelText: CPU - system: System - user: User - networkSection: - labelText: Network - transmit: Transmit - receive: Receive - memorySection: - labelText: Memory - used: Used - storageSection: - labelText: Storage - read: Read - write: Write inputAnswers: yaml: Edit as YAML @@ -4442,6 +4464,13 @@ podsSection: title: Pods detail: Pods in this workload +containersSection: + title: Containers + detail: Containers in this pod + noData: No Containers + noMatch: No Containers match the current search + initContainer: Init Container + labelsSection: kind: Kind title: Labels @@ -5862,7 +5891,7 @@ volumesPage: storageClass: label: Storage Class - + vmConsole: header: "Console:" protip: "ProTip: Hold the {key} key when opening shell access to launch a new window." @@ -6009,6 +6038,7 @@ nav: alerts: Alerts notifiers: Notifiers logging: Logging + monitoring: Monitoring pipeline: Pipeline project: none: Grouped Projects/Namespaces @@ -6065,6 +6095,7 @@ action: viewConfig: View Config viewGraph: View Graph viewInApi: View in API + viewInGrafana: Go to Grafana viewInstance: View Instance test: Test mute: Mute diff --git a/translations/zh-hans.yaml b/translations/zh-hans.yaml index cd9b5a7c3..1f865036a 100644 --- a/translations/zh-hans.yaml +++ b/translations/zh-hans.yaml @@ -790,6 +790,8 @@ ingressPage: containerPage: header: '容器: {name}' + initContainer: + label: 初始化容器 envTab: header: 环境变量 detail: '在创建容器时配置到容器内的环境变量。' @@ -833,25 +835,46 @@ containersPage: kubernetesLink: 安装 Kubernetes clusterDashboard: - title: 仪表盘 - cpu: CPU - memory: Memory - pods: Pods - subtitle: - reserved: "已保留{total}中的{used}" - used: "已创建{used}个Pod,最多{total}个" - node: Nodes - etcd: Etcd - scheduler: Scheduler - controllerManager: Controller Manager - value: 值 systemProject: "这是具有所有Kubernetes和{appName}系统命名空间的系统项目,修改系统项目中的资源可能会导致系统无法正常运行。" notActive: 此集群当前为{state} notReady: 此集群当前为{state},在API准备就绪之前,直接与API交互的功能将不可用。 - noNodes: 没有主机 - alert: - node: "警告: 节点{node}未激活" - component: "警告: 组件{component}不健康" + +metricsAction: + description: 查看实时监控指标 + sections: + workload: 工作负载监控 + pod: Pod监控 + container: Container监控 + summary: + label: 概览模式 + detail: + label: 详情模式 + from: + label: 起始时间 + to: + label: 结束时间 + periods: + custom: 自定义时间段 + 5m: 过去5分钟 + 1h: 过去1小时 + 6h: 过去6小时 + 24h: 过去24小时 + 7d: 过去7天 + 30d: 过去30天 + metrics: + workload-cpu-usage: CPU + workload-disk-io: 磁盘I/O + workload-memory-usage-bytes-sum: 内存 + workload-network-io: 网络I/O + workload-network-packet: 网络数据包 + pod-cpu-usage: CPU + pod-disk-io: 磁盘I/O + pod-memory-usage-bytes-sum: 内存 + pod-network-io: 网络I/O + pod-network-packet: 网络数据包 + container-cpu-usage: CPU + container-disk-io: 磁盘I/O + container-memory-usage-bytes-sum: 内存 dnsPage: noMatch: 没有匹配当前搜索的记录 @@ -1235,14 +1258,14 @@ configMapsPage: servicePage: header: '工作负载: {name}' globalScale: '每个主机{scale}' - endpoints: '访问端口:' + endpoints: '访问端口' multistat: - type: '类型:' - fqdn: 'FQDN:' - scale: 'Pod副本数:' - image: '镜像名: ' - namespace: '命名空间:' - created: '创建时间: ' + type: '类型' + fqdn: 'FQDN' + scale: 'Pod副本数' + image: '镜像名' + namespace: '命名空间' + created: '创建时间' daemonSetScale: '每主机1个Pod' serviceType: deployment: Deployment @@ -3783,6 +3806,20 @@ formNetwork: label: IP地址 placeholder: '例如: 192.168.0.1' +formCustomMetrics: + title: 自定义指标 + detail: 配置自定义指标端口,监控系统将通过这些端口采集自定义指标。 + port: + label: 容器端口 + placeholder: 例如:8080 + path: + label: Path + placeholder: 例如:/metrics + protocol: + label: 协议 + noPorts: 没有自定义指标 + addActionLabel: 添加自定义指标 + formPorts: header: 端口映射 addAction: 添加规则 @@ -4386,25 +4423,6 @@ hostPod: identityBlock: loading: 加载中... -infoMultiStats: - connecting: 连接中... - utilizationStats: 使用率统计数据仅在活动/运行时可见 - cpuSection: - labelText: 处理器 - system: 系统 - user: 用户 - networkSection: - labelText: 网络 - transmit: 发送 - receive: 接收 - memorySection: - labelText: 内存 - used: 已使用 - storageSection: - labelText: 存储 - read: 读 - write: 写 - inputAnswers: yaml: 编辑YAML config: 配置选项 @@ -4436,6 +4454,13 @@ podsSection: title: Pods detail: 此工作负载中的Pods +containersSection: + title: 容器 + detail: 当前Pod中包含的容器 + noData: 没有容器 + noMatch: 没有容器匹配当前搜索 + initContainer: 初始化容器 + labelsSection: kind: 类型 title: 标签 @@ -5989,6 +6014,7 @@ nav: alerts: 告警 notifiers: 通知 logging: 日志 + monitoring: 监控 pipeline: 流水线 project: none: 项目/命名空间 @@ -6045,6 +6071,7 @@ action: viewConfig: 查看配置 viewGraph: 查看图形 viewInApi: API查看 + viewInGrafana: 查看Grafana viewInstance: 查看实例 test: 测试 mute: 静默