diff --git a/ui/app/components/topo-viz.js b/ui/app/components/topo-viz.js index 665439f1b..3332983de 100644 --- a/ui/app/components/topo-viz.js +++ b/ui/app/components/topo-viz.js @@ -42,9 +42,11 @@ export default class TopoViz extends Component { if (this.activeTaskGroup === taskGroup && this.activeJobId === jobId) { this.activeTaskGroup = null; this.activeJobId = null; + if (this.args.onAllocationSelect) this.args.onAllocationSelect(null); } else { this.activeTaskGroup = taskGroup; this.activeJobId = jobId; + if (this.args.onAllocationSelect) this.args.onAllocationSelect(allocation); } } } diff --git a/ui/app/controllers/topology.js b/ui/app/controllers/topology.js new file mode 100644 index 000000000..50ea067ca --- /dev/null +++ b/ui/app/controllers/topology.js @@ -0,0 +1,57 @@ +import Controller from '@ember/controller'; +import { computed } from '@ember/object'; +import classic from 'ember-classic-decorator'; +import { reduceToLargestUnit } from 'nomad-ui/helpers/format-bytes'; + +@classic +export default class TopologyControllers extends Controller { + get datacenters() { + return Array.from(new Set(this.model.nodes.mapBy('datacenter'))).compact(); + } + + @computed('model.nodes.@each.resources') + get totalMemory() { + const mibs = this.model.nodes + .mapBy('resources.memory') + .reduce((sum, memory) => sum + (memory || 0), 0); + return mibs * 1024 * 1024; + } + + @computed('model.nodes.@each.resources') + get totalCPU() { + return this.model.nodes.mapBy('resources.cpu').reduce((sum, cpu) => sum + (cpu || 0), 0); + } + + @computed('totalMemory') + get totalMemoryFormatted() { + return reduceToLargestUnit(this.totalMemory)[0].toFixed(2); + } + + @computed('totalCPU') + get totalMemoryUnits() { + return reduceToLargestUnit(this.totalMemory)[1]; + } + + @computed('model.allocations.@each.resources') + get totalReservedMemory() { + const mibs = this.model.allocations + .mapBy('resources.memory') + .reduce((sum, memory) => sum + (memory || 0), 0); + return mibs * 1024 * 1024; + } + + @computed('model.allocations.@each.resources') + get totalReservedCPU() { + return this.model.allocations.mapBy('resources.cpu').reduce((sum, cpu) => sum + (cpu || 0), 0); + } + + @computed('totalMemory', 'totalReservedMemory') + get reservedMemoryPercent() { + return this.totalReservedMemory / this.totalMemory; + } + + @computed('totalCPU', 'totalReservedCPU') + get reservedCPUPercent() { + return this.totalReservedCPU / this.totalCPU; + } +} diff --git a/ui/app/helpers/format-bytes.js b/ui/app/helpers/format-bytes.js index b2c69ed06..ba204fbfc 100644 --- a/ui/app/helpers/format-bytes.js +++ b/ui/app/helpers/format-bytes.js @@ -1,6 +1,6 @@ import Helper from '@ember/component/helper'; -const UNITS = ['Bytes', 'KiB', 'MiB']; +const UNITS = ['Bytes', 'KiB', 'MiB', 'GiB']; /** * Bytes Formatter @@ -10,7 +10,7 @@ const UNITS = ['Bytes', 'KiB', 'MiB']; * Outputs the bytes reduced to the largest supported unit size for which * bytes is larger than one. */ -export function formatBytes([bytes]) { +export function reduceToLargestUnit(bytes) { bytes || (bytes = 0); let unitIndex = 0; while (bytes >= 1024 && unitIndex < UNITS.length - 1) { @@ -18,7 +18,12 @@ export function formatBytes([bytes]) { unitIndex++; } - return `${Math.floor(bytes)} ${UNITS[unitIndex]}`; + return [bytes, UNITS[unitIndex]]; +} + +export function formatBytes([bytes]) { + const [number, unit] = reduceToLargestUnit(bytes); + return `${Math.floor(number)} ${unit}`; } export default Helper.helper(formatBytes); diff --git a/ui/app/styles/components.scss b/ui/app/styles/components.scss index d00d53d0b..40b4e1f7a 100644 --- a/ui/app/styles/components.scss +++ b/ui/app/styles/components.scss @@ -4,6 +4,7 @@ @import './components/codemirror'; @import './components/copy-button'; @import './components/cli-window'; +@import './components/dashboard-metric'; @import './components/dropdown'; @import './components/ember-power-select'; @import './components/empty-message'; diff --git a/ui/app/styles/components/dashboard-metric.scss b/ui/app/styles/components/dashboard-metric.scss new file mode 100644 index 000000000..3b6cfb8cb --- /dev/null +++ b/ui/app/styles/components/dashboard-metric.scss @@ -0,0 +1,31 @@ +.dashboard-metric { + margin-top: 1.5em; + + .metric { + text-align: left; + font-weight: $weight-bold; + font-size: $size-3; + + .metric-units { + font-size: $size-4; + } + + .metric-label { + font-size: $body-size; + font-weight: $weight-normal; + } + } + + .graphic { + padding-bottom: 0; + margin-bottom: 0; + + > .column { + padding: 0.5rem 0.75rem; + } + } + + .annotation { + margin-top: -0.75rem; + } +} diff --git a/ui/app/templates/topology.hbs b/ui/app/templates/topology.hbs index 79d37b172..26abab726 100644 --- a/ui/app/templates/topology.hbs +++ b/ui/app/templates/topology.hbs @@ -24,14 +24,74 @@
-
Cluster Details
+
{{if this.activeAllocation "Allocation" "Cluster"}} Details
- Aggregate metrics go here. + {{#if this.activeAllocation}} + Showing alloc details for {{this.activeAllocation.shortId}} + {{else}} +
+
+

DCs

+

{{this.datacenters.length}}

+
+
+

Clients

+

{{this.model.nodes.length}}

+
+
+

Allocations

+ {{! TODO: make sure that this is only the scheduled allocations }} +

{{this.model.allocations.length}}

+
+
+
+

{{this.totalMemoryFormatted}} {{this.totalMemoryUnits}} of memory

+
+
+
+ + {{this.reservedMemoryPercent}} + +
+
+
+ {{format-percentage this.reservedMemoryPercent total=1}} +
+
+
+ {{format-bytes this.totalReservedMemory}} / {{format-bytes this.totalMemory}} reserved +
+
+
+

{{this.totalCPU}} Mhz of CPU

+
+
+
+ + {{this.reservedCPUPercent}} + +
+
+
+ {{format-percentage this.reservedCPUPercent total=1}} +
+
+
+ {{this.totalReservedCPU}} Mhz / {{this.totalCPU}} Mhz reserved +
+
+ {{/if}}
- +
diff --git a/ui/mirage/scenarios/topo.js b/ui/mirage/scenarios/topo.js index 474ea361e..e75bf7fa5 100644 --- a/ui/mirage/scenarios/topo.js +++ b/ui/mirage/scenarios/topo.js @@ -17,7 +17,7 @@ export function topoSmall(server) { }); const jobResources = [ - ['M: 256, C: 150'], + ['M: 2560, C: 150'], ['M: 128, C: 400'], ['M: 512, C: 100'], ['M: 256, C: 150'],