mirror of https://github.com/rancher/dashboard.git
Merge pull request #4588 from rancher/workload-health-scaling
Show Workload Pod/Job State and Replica Scale buttons
This commit is contained in:
commit
ea5592322c
|
|
@ -4195,6 +4195,7 @@ tableHeaders:
|
|||
one { Host }
|
||||
other { Hosts }
|
||||
}
|
||||
health: Health
|
||||
id: ID
|
||||
image: Image
|
||||
imageSize: Size
|
||||
|
|
@ -4732,6 +4733,8 @@ workload:
|
|||
label: Successful Job History Limit
|
||||
tip: The number of successful finished jobs to retain.
|
||||
suspend: Suspend
|
||||
list:
|
||||
errorCannotScale: Failed to scale {workloadName} {direction, select, up { up } down { down } }
|
||||
metrics:
|
||||
pod: Pod Metrics
|
||||
metricsView: Metrics View
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ export default {
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.growl-container {
|
||||
z-index: 15;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
|
|
|||
|
|
@ -63,6 +63,15 @@ export default {
|
|||
type: String,
|
||||
default: 'resourceTable.groupBy.namespace',
|
||||
},
|
||||
|
||||
overflowX: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
overflowY: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
|
@ -263,6 +272,8 @@ export default {
|
|||
:paging-params="pagingParams"
|
||||
:paging-label="pagingLabel"
|
||||
:table-actions="_showBulkActions"
|
||||
:overflow-x="overflowX"
|
||||
:overflow-y="overflowY"
|
||||
key-field="_key"
|
||||
:sort-generation-fn="sortGenerationFn"
|
||||
v-on="$listeners"
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ export default {
|
|||
:class="{ sortable: col.sort, [col.breakpoint]: !!col.breakpoint}"
|
||||
@click.prevent="changeSort($event, col)"
|
||||
>
|
||||
<span v-if="col.sort" @click="$router.applyQuery(queryFor(col))">
|
||||
<span v-if="col.sort" v-tooltip="col.tooltip" @click="$router.applyQuery(queryFor(col))">
|
||||
<span v-html="labelFor(col)" />
|
||||
<span class="icon-stack">
|
||||
<i class="icon icon-sort icon-stack-1x faded" />
|
||||
|
|
@ -143,7 +143,7 @@ export default {
|
|||
<i v-if="isCurrent(col) && descending" class="icon icon-sort-up icon-stack-1x" />
|
||||
</span>
|
||||
</span>
|
||||
<span v-else>{{ labelFor(col) }}</span>
|
||||
<span v-else v-tooltip="col.tooltip">{{ labelFor(col) }}</span>
|
||||
</th>
|
||||
<th v-if="rowActions" :width="rowActionsWidth">
|
||||
</th>
|
||||
|
|
|
|||
|
|
@ -173,6 +173,15 @@ export default {
|
|||
default: false
|
||||
},
|
||||
|
||||
overflowX: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
overflowY: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
/**
|
||||
* If pagination of the data is enabled or not
|
||||
*/
|
||||
|
|
@ -391,12 +400,15 @@ export default {
|
|||
classObject() {
|
||||
return {
|
||||
'top-divider': this.topDivider,
|
||||
'body-dividers': this.bodyDividers
|
||||
'body-dividers': this.bodyDividers,
|
||||
'overflow-y': this.overflowY,
|
||||
'overflow-x': this.overflowX,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
get,
|
||||
dasherize,
|
||||
|
||||
|
|
@ -675,7 +687,7 @@ export default {
|
|||
:key="col.name"
|
||||
:data-title="labelFor(col)"
|
||||
:align="col.align || 'left'"
|
||||
:class="{['col-'+dasherize(col.formatter||'')]: !!col.formatter, [col.breakpoint]: !!col.breakpoint}"
|
||||
:class="{['col-'+dasherize(col.formatter||'')]: !!col.formatter, [col.breakpoint]: !!col.breakpoint, ['skip-select']: col.skipSelect}"
|
||||
:width="col.width"
|
||||
>
|
||||
<slot :name="'cell:' + col.name" :row="row" :col="col" :value="valueFor(row,col)">
|
||||
|
|
@ -686,6 +698,7 @@ export default {
|
|||
:row="row"
|
||||
:col="col"
|
||||
v-bind="col.formatterOpts"
|
||||
:row-key="get(row,keyField)"
|
||||
/>
|
||||
<template v-else-if="valueFor(row,col) !== ''">
|
||||
{{ formatValue(row,col) }}
|
||||
|
|
@ -831,6 +844,13 @@ $spacing: 10px;
|
|||
background: var(--sortable-table-bg);
|
||||
border-radius: 4px;
|
||||
|
||||
&.overflow-x {
|
||||
overflow-x: visible;
|
||||
}
|
||||
&.overflow-y {
|
||||
overflow-y: visible;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 8px 5px;
|
||||
border: 0;
|
||||
|
|
|
|||
|
|
@ -179,6 +179,11 @@ export default {
|
|||
async onRowClick(e) {
|
||||
const node = this.nodeForEvent(e);
|
||||
const td = $(e.target).closest('TD');
|
||||
const skipSelect = td.hasClass('skip-select');
|
||||
|
||||
if (skipSelect) {
|
||||
return;
|
||||
}
|
||||
const selection = this.selectedNodes;
|
||||
const isCheckbox = this.isSelectionCheckbox(e.target) || td.hasClass('row-check');
|
||||
const isExpand = td.hasClass('row-expand');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
minusTooltip: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
plusTooltip: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
min: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
canMinus() {
|
||||
return (this.min !== undefined && this.min !== null) ? this.value > this.min : true;
|
||||
},
|
||||
canPlus() {
|
||||
return (this.max !== undefined && this.max !== null) ? this.value < this.max : true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="plus-minus">
|
||||
<button v-tooltip="minusTooltip" :disabled="disabled || !canMinus" type="button" class="btn btn-sm role-secondary" @click="$emit('minus')">
|
||||
<i class="icon icon-sm icon-minus" />
|
||||
</button>
|
||||
<div class="value">
|
||||
{{ value }}
|
||||
</div>
|
||||
<button v-tooltip="plusTooltip" :disabled="disabled || !canPlus" type="button" class="btn btn-sm role-secondary" @click="$emit('plus')">
|
||||
<i class="icon icon-sm icon-plus" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.plus-minus {
|
||||
min-width: 70px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
button:hover {
|
||||
background-color: var(--accent-btn);
|
||||
}
|
||||
.value {
|
||||
width: 25px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,243 @@
|
|||
<script>
|
||||
import Vue from 'vue';
|
||||
import ProgressBarMulti from '@/components/ProgressBarMulti';
|
||||
import PlusMinus from '@/components/form/PlusMinus';
|
||||
import { SCALABLE_WORKLOAD_TYPES } from '~/config/types';
|
||||
import { ucFirst } from '~/utils/string';
|
||||
|
||||
const SCALABLE_TYPES = Object.values(SCALABLE_WORKLOAD_TYPES);
|
||||
|
||||
export default {
|
||||
components: { PlusMinus, ProgressBarMulti },
|
||||
|
||||
props: {
|
||||
row: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
col: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
rowKey: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
document.addEventListener('click', this.onClickOutside);
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('click', this.onClickOutside);
|
||||
},
|
||||
|
||||
data() {
|
||||
return { disabled: false, expanded: false };
|
||||
},
|
||||
|
||||
computed: {
|
||||
id() {
|
||||
return `${ this.rowKey }-workload-health-scale`.replaceAll('-', '');
|
||||
},
|
||||
|
||||
canScale() {
|
||||
return !!SCALABLE_TYPES.includes(this.row.type) && this.row.canUpdate;
|
||||
},
|
||||
|
||||
parts() {
|
||||
return Object.entries(this.row.jobGauges || this.row.podGauges || [])
|
||||
.map(([name, value]) => ({
|
||||
color: `bg-${ value.color }`,
|
||||
value: value.count || 0,
|
||||
label: ucFirst(name)
|
||||
})).filter(x => x.value > 0);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onClickOutside(event) {
|
||||
const { [`root-${ this.id }`]: component } = this.$refs;
|
||||
|
||||
if (!component || component.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
this.expanded = false;
|
||||
},
|
||||
|
||||
async scaleDown() {
|
||||
await this.scale(false);
|
||||
},
|
||||
async scaleUp() {
|
||||
await this.scale(true);
|
||||
},
|
||||
async scale(isUp) {
|
||||
Vue.set(this, 'disabled', true);
|
||||
try {
|
||||
if (isUp) {
|
||||
await this.row.scaleUp();
|
||||
} else {
|
||||
await this.row.scaleDown();
|
||||
}
|
||||
} catch (err) {
|
||||
this.$store.dispatch('growl/fromError', {
|
||||
title: this.t('workload.list.errorCannotScale', { direction: isUp ? 'up' : 'down', workloadName: this.row.name }),
|
||||
err
|
||||
},
|
||||
{ root: true });
|
||||
}
|
||||
Vue.set(this, 'disabled', false);
|
||||
},
|
||||
|
||||
insideBounds(bounding, bounds) {
|
||||
return bounding.top >= bounds.top &&
|
||||
bounding.left >= bounds.left &&
|
||||
bounding.right <= bounds.right &&
|
||||
bounding.bottom <= bounds.bottom;
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
watch: {
|
||||
expanded(neu) {
|
||||
// If the drop down content appears outside of the window then move it to be above the trigger
|
||||
// Do this is three steps
|
||||
// expanded: false & expanded-checked = false - Content does not appear in DOM
|
||||
// expanded: true & expanded-checked = false - Content appears in DOM (so it's location can be calcualated to be in or out of an area) but isn't visible (user doesn't see content blip from below to above trigger)
|
||||
// expanded: true & expanded-checked = true - Content appears in DOM and is visible (it's final location is known so user can see)
|
||||
setTimeout(() => { // There be beasts without this (classes don't get applied... so drop down never gets shown)
|
||||
const dropdown = document.getElementById(this.id);
|
||||
|
||||
if (!neu) {
|
||||
dropdown.classList.remove('expanded-checked');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensire drop down will be inside of the window, otherwise show above the trigger
|
||||
const bounding = dropdown.getBoundingClientRect();
|
||||
const insideWindow = this.insideBounds(bounding, {
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: window.innerWidth || document.documentElement.clientWidth,
|
||||
bottom: window.innerHeight || document.documentElement.clientHeight,
|
||||
});
|
||||
|
||||
if (insideWindow) {
|
||||
dropdown.classList.remove('out-of-view');
|
||||
} else {
|
||||
dropdown.classList.add('out-of-view');
|
||||
}
|
||||
|
||||
// This will trigger the actual display of the drop down (after we've calculated if it goes below or above trigger)
|
||||
dropdown.classList.add('expanded-checked');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :id="`root-${id}`" :ref="`root-${id}`" class="hs-popover">
|
||||
<div id="trigger" class="hs-popover__trigger" :class="{expanded}" @click="expanded = !expanded">
|
||||
<ProgressBarMulti v-if="parts" class="health" :values="parts" :show-zeros="true" />
|
||||
<i :class="{icon: true, 'icon-chevron-up': expanded, 'icon-chevron-down': !expanded}" />
|
||||
</div>
|
||||
<div :id="id" class="hs-popover__content" :class="{expanded, [id]:true}">
|
||||
<div>
|
||||
<div v-for="obj in parts" :key="obj.label" class="counts">
|
||||
<span>{{ obj.label }}</span>
|
||||
<span>{{ obj.value }}</span>
|
||||
</div>
|
||||
<div v-if="canScale" class="text-center scale">
|
||||
<span>{{ t('tableHeaders.scale') }} </span>
|
||||
<PlusMinus :value="row.spec.replicas" :disabled="disabled" @minus="scaleDown" @plus="scaleUp" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
$height: 30px;
|
||||
$width: 150px;
|
||||
|
||||
.hs-popover {
|
||||
position: relative;
|
||||
|
||||
&__trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
width: $width;
|
||||
height: $height;
|
||||
|
||||
border: solid thin var(--sortable-table-top-divider);
|
||||
|
||||
&.expanded {
|
||||
background-color: var(--sortable-table-row-bg);
|
||||
}
|
||||
&:not(.expanded):hover {
|
||||
background-color: var(--accent-btn);
|
||||
.icon {
|
||||
color: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.health {
|
||||
width: $width - $height; // height is width of icon;
|
||||
margin-left: 5px;
|
||||
}
|
||||
.icon {
|
||||
font-size: $height;
|
||||
width: $height;
|
||||
color: var(--primary);
|
||||
margin-top: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
z-index: 14;
|
||||
width: $width;
|
||||
|
||||
border: solid thin var(--sortable-table-top-divider);
|
||||
background-color: var(--sortable-table-row-bg);
|
||||
|
||||
position: absolute;
|
||||
margin-top: -1px;
|
||||
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
&.expanded {
|
||||
display: inline;
|
||||
}
|
||||
&.expanded-checked {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
&.out-of-view {
|
||||
// Flip to show drop down above trigger
|
||||
bottom: 0;
|
||||
margin-bottom: $height - 1px;
|
||||
}
|
||||
|
||||
& > div {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.counts {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.scale {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -16,7 +16,7 @@ import {
|
|||
USER_ID, USERNAME, USER_DISPLAY_NAME, USER_PROVIDER, WORKLOAD_ENDPOINTS, STORAGE_CLASS_DEFAULT,
|
||||
STORAGE_CLASS_PROVISIONER, PERSISTENT_VOLUME_SOURCE,
|
||||
HPA_REFERENCE, MIN_REPLICA, MAX_REPLICA, CURRENT_REPLICA,
|
||||
ACCESS_KEY, DESCRIPTION, EXPIRES, EXPIRY_STATE, SUB_TYPE, AGE_NORMAN, SCOPE_NORMAN, PERSISTENT_VOLUME_CLAIM, RECLAIM_POLICY, PV_REASON
|
||||
ACCESS_KEY, DESCRIPTION, EXPIRES, EXPIRY_STATE, SUB_TYPE, AGE_NORMAN, SCOPE_NORMAN, PERSISTENT_VOLUME_CLAIM, RECLAIM_POLICY, PV_REASON, WORKLOAD_HEALTH_SCALE
|
||||
} from '@/config/table-headers';
|
||||
|
||||
import { DSL } from '@/store/type-map';
|
||||
|
|
@ -168,14 +168,14 @@ export function init(store) {
|
|||
headers(SERVICE, [STATE, NAME_COL, NAMESPACE_COL, TARGET_PORT, SELECTOR, SPEC_TYPE, AGE]);
|
||||
headers(HPA, [STATE, NAME_COL, HPA_REFERENCE, MIN_REPLICA, MAX_REPLICA, CURRENT_REPLICA, AGE]);
|
||||
|
||||
headers(WORKLOAD, [STATE, NAME_COL, NAMESPACE_COL, WORKLOAD_IMAGES, WORKLOAD_ENDPOINTS, TYPE, 'Ready', AGE]);
|
||||
headers(WORKLOAD_TYPES.DEPLOYMENT, [STATE, NAME_COL, NAMESPACE_COL, WORKLOAD_IMAGES, WORKLOAD_ENDPOINTS, 'Ready', 'Up-to-date', 'Available', AGE]);
|
||||
headers(WORKLOAD_TYPES.DAEMON_SET, [STATE, NAME_COL, NAMESPACE_COL, WORKLOAD_IMAGES, WORKLOAD_ENDPOINTS, 'Ready', 'Current', 'Desired', AGE]);
|
||||
headers(WORKLOAD_TYPES.REPLICA_SET, [STATE, NAME_COL, NAMESPACE_COL, WORKLOAD_IMAGES, WORKLOAD_ENDPOINTS, 'Ready', 'Current', 'Desired', AGE]);
|
||||
headers(WORKLOAD_TYPES.STATEFUL_SET, [STATE, NAME_COL, NAMESPACE_COL, WORKLOAD_IMAGES, WORKLOAD_ENDPOINTS, 'Ready', AGE]);
|
||||
headers(WORKLOAD_TYPES.JOB, [STATE, NAME_COL, NAMESPACE_COL, WORKLOAD_IMAGES, WORKLOAD_ENDPOINTS, 'Completions', 'Duration', AGE]);
|
||||
headers(WORKLOAD_TYPES.CRON_JOB, [STATE, NAME_COL, NAMESPACE_COL, WORKLOAD_IMAGES, WORKLOAD_ENDPOINTS, 'Schedule', 'Last Schedule', AGE]);
|
||||
headers(WORKLOAD_TYPES.REPLICATION_CONTROLLER, [STATE, NAME_COL, NAMESPACE_COL, WORKLOAD_IMAGES, WORKLOAD_ENDPOINTS, 'Ready', 'Current', 'Desired', AGE]);
|
||||
headers(WORKLOAD, [STATE, NAME_COL, NAMESPACE_COL, TYPE, WORKLOAD_IMAGES, WORKLOAD_ENDPOINTS, AGE, WORKLOAD_HEALTH_SCALE]);
|
||||
headers(WORKLOAD_TYPES.DEPLOYMENT, [STATE, NAME_COL, NAMESPACE_COL, WORKLOAD_IMAGES, WORKLOAD_ENDPOINTS, 'Ready', 'Up-to-date', 'Available', AGE, WORKLOAD_HEALTH_SCALE]);
|
||||
headers(WORKLOAD_TYPES.DAEMON_SET, [STATE, NAME_COL, NAMESPACE_COL, WORKLOAD_IMAGES, WORKLOAD_ENDPOINTS, 'Ready', 'Current', 'Desired', AGE, WORKLOAD_HEALTH_SCALE]);
|
||||
headers(WORKLOAD_TYPES.REPLICA_SET, [STATE, NAME_COL, NAMESPACE_COL, WORKLOAD_IMAGES, WORKLOAD_ENDPOINTS, 'Ready', 'Current', 'Desired', AGE, WORKLOAD_HEALTH_SCALE]);
|
||||
headers(WORKLOAD_TYPES.STATEFUL_SET, [STATE, NAME_COL, NAMESPACE_COL, WORKLOAD_IMAGES, WORKLOAD_ENDPOINTS, 'Ready', AGE, WORKLOAD_HEALTH_SCALE]);
|
||||
headers(WORKLOAD_TYPES.JOB, [STATE, NAME_COL, NAMESPACE_COL, WORKLOAD_IMAGES, WORKLOAD_ENDPOINTS, 'Completions', 'Duration', AGE, WORKLOAD_HEALTH_SCALE]);
|
||||
headers(WORKLOAD_TYPES.CRON_JOB, [STATE, NAME_COL, NAMESPACE_COL, WORKLOAD_IMAGES, WORKLOAD_ENDPOINTS, 'Schedule', 'Last Schedule', AGE, WORKLOAD_HEALTH_SCALE]);
|
||||
headers(WORKLOAD_TYPES.REPLICATION_CONTROLLER, [STATE, NAME_COL, NAMESPACE_COL, WORKLOAD_IMAGES, WORKLOAD_ENDPOINTS, 'Ready', 'Current', 'Desired', AGE, WORKLOAD_HEALTH_SCALE]);
|
||||
headers(POD, [STATE, NAME_COL, NAMESPACE_COL, POD_IMAGES, 'Ready', 'Restarts', 'IP', NODE_COL, AGE]);
|
||||
headers(STORAGE_CLASS, [STATE, NAME_COL, STORAGE_CLASS_PROVISIONER, STORAGE_CLASS_DEFAULT, AGE]);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { CATTLE_PUBLIC_ENDPOINTS } from '@/config/labels-annotations';
|
||||
import { NODE as NODE_TYPE } from '@/config/types';
|
||||
import { COLUMN_BREAKPOINTS } from '@/components/SortableTable/index.vue';
|
||||
|
||||
// Note: 'id' is always the last sort, so you don't have to specify it here.
|
||||
|
||||
|
|
@ -602,7 +603,11 @@ export const WORKSPACE = {
|
|||
sort: ['metadata.namespace', 'nameSort'],
|
||||
};
|
||||
|
||||
export const WORKLOAD_IMAGES = { ...POD_IMAGES, value: '' };
|
||||
export const WORKLOAD_IMAGES = {
|
||||
...POD_IMAGES,
|
||||
value: '',
|
||||
breakpoint: COLUMN_BREAKPOINTS.LAPTOP
|
||||
};
|
||||
|
||||
export const WORKLOAD_ENDPOINTS = {
|
||||
name: 'workloadEndpoints',
|
||||
|
|
@ -610,6 +615,15 @@ export const WORKLOAD_ENDPOINTS = {
|
|||
value: `$['metadata']['annotations']['${ CATTLE_PUBLIC_ENDPOINTS }']`,
|
||||
formatter: 'Endpoints',
|
||||
dashIfEmpty: true,
|
||||
breakpoint: COLUMN_BREAKPOINTS.DESKTOP
|
||||
};
|
||||
|
||||
export const WORKLOAD_HEALTH_SCALE = {
|
||||
name: 'workloadHealthScale',
|
||||
labelKey: 'tableHeaders.health',
|
||||
formatter: 'WorkloadHealthScale',
|
||||
width: 150,
|
||||
skipSelect: true
|
||||
};
|
||||
|
||||
export const FLEET_SUMMARY = {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import Loading from '@/components/Loading';
|
|||
import ResourceTabs from '@/components/form/ResourceTabs';
|
||||
import CountGauge from '@/components/CountGauge';
|
||||
import { allHash } from '@/utils/promise';
|
||||
import { get } from '@/utils/object';
|
||||
import DashboardMetrics from '@/components/DashboardMetrics';
|
||||
import V1WorkloadMetrics from '@/mixins/v1-workload-metrics';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
|
@ -72,16 +71,7 @@ export default {
|
|||
|
||||
computed: {
|
||||
...mapGetters(['currentCluster']),
|
||||
pods() {
|
||||
const relationships = get(this.value, 'metadata.relationships') || [];
|
||||
const podRelationship = relationships.filter(relationship => relationship.toType === POD)[0];
|
||||
|
||||
if (podRelationship) {
|
||||
return this.$store.getters['cluster/matching'](POD, podRelationship.selector).filter(pod => pod?.metadata?.namespace === this.value.metadata.namespace);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
isJob() {
|
||||
return this.value.type === WORKLOAD_TYPES.JOB;
|
||||
},
|
||||
|
|
@ -108,24 +98,6 @@ export default {
|
|||
return this.podTemplateSpec?.containers[0];
|
||||
},
|
||||
|
||||
jobRelationships() {
|
||||
if (!this.isCronJob) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (get(this.value, 'metadata.relationships') || []).filter(relationship => relationship.toType === WORKLOAD_TYPES.JOB);
|
||||
},
|
||||
|
||||
jobs() {
|
||||
if (!this.isCronJob) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.jobRelationships.map((obj) => {
|
||||
return this.$store.getters['cluster/byId'](WORKLOAD_TYPES.JOB, obj.toId );
|
||||
}).filter(x => !!x);
|
||||
},
|
||||
|
||||
jobSchema() {
|
||||
return this.$store.getters['cluster/schemaFor'](WORKLOAD_TYPES.JOB);
|
||||
},
|
||||
|
|
@ -134,38 +106,12 @@ export default {
|
|||
return this.$store.getters['type-map/headersFor'](this.jobSchema);
|
||||
},
|
||||
|
||||
jobGauges() {
|
||||
const out = {
|
||||
succeeded: { color: 'success', count: 0 }, running: { color: 'info', count: 0 }, failed: { color: 'error', count: 0 }
|
||||
};
|
||||
|
||||
if (this.value.type === WORKLOAD_TYPES.CRON_JOB) {
|
||||
this.jobs.forEach((job) => {
|
||||
const { status = {} } = job;
|
||||
|
||||
out.running.count += status.active || 0;
|
||||
out.succeeded.count += status.succeeded || 0;
|
||||
out.failed.count += status.failed || 0;
|
||||
});
|
||||
} else if (this.value.type === WORKLOAD_TYPES.JOB) {
|
||||
const { status = {} } = this.value;
|
||||
|
||||
out.running.count = status.active || 0;
|
||||
out.succeeded.count = status.succeeded || 0;
|
||||
out.failed.count = status.failed || 0;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
totalRuns() {
|
||||
if (!this.jobs) {
|
||||
if (!this.value.jobs) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.jobs.reduce((total, job) => {
|
||||
return this.value.jobs.reduce((total, job) => {
|
||||
const { status = {} } = job;
|
||||
|
||||
total += (status.active || 0);
|
||||
|
|
@ -177,7 +123,7 @@ export default {
|
|||
},
|
||||
|
||||
podRestarts() {
|
||||
return this.pods.reduce((total, pod) => {
|
||||
return this.value.pods.reduce((total, pod) => {
|
||||
const { status:{ containerStatuses = [] } } = pod;
|
||||
|
||||
if (containerStatuses.length) {
|
||||
|
|
@ -192,39 +138,6 @@ export default {
|
|||
}, 0);
|
||||
},
|
||||
|
||||
podGauges() {
|
||||
const out = {
|
||||
active: { color: 'success' }, transitioning: { color: 'info' }, warning: { color: 'warning' }, error: { color: 'error' }
|
||||
};
|
||||
|
||||
if (!this.pods) {
|
||||
return out;
|
||||
}
|
||||
|
||||
this.pods.map((pod) => {
|
||||
const { status:{ phase } } = pod;
|
||||
let group;
|
||||
|
||||
switch (phase) {
|
||||
case 'Running':
|
||||
group = 'active';
|
||||
break;
|
||||
case 'Pending':
|
||||
group = 'transitioning';
|
||||
break;
|
||||
case 'Failed':
|
||||
group = 'error';
|
||||
break;
|
||||
default:
|
||||
group = 'warning';
|
||||
}
|
||||
|
||||
out[group].count ? out[group].count++ : out[group].count = 1;
|
||||
});
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
podHeaders() {
|
||||
return [
|
||||
STATE,
|
||||
|
|
@ -255,12 +168,12 @@ export default {
|
|||
<h3>
|
||||
{{ isJob || isCronJob ? t('workload.detailTop.runs') :t('workload.detailTop.pods') }}
|
||||
</h3>
|
||||
<div v-if="pods || jobGauges" class="gauges mb-20">
|
||||
<template v-if="jobGauges">
|
||||
<div v-if="value.pods || value.jobGauges" class="gauges mb-20">
|
||||
<template v-if="value.jobGauges">
|
||||
<CountGauge
|
||||
v-for="(group, key) in jobGauges"
|
||||
v-for="(group, key) in value.jobGauges"
|
||||
:key="key"
|
||||
:total="isCronJob? totalRuns : pods.length"
|
||||
:total="isCronJob? totalRuns : value.pods.length"
|
||||
:useful="group.count || 0"
|
||||
:primary-color-var="`--sizzle-${group.color}`"
|
||||
:name="t(`workload.gaugeStates.${key}`)"
|
||||
|
|
@ -268,9 +181,9 @@ export default {
|
|||
</template>
|
||||
<template v-else>
|
||||
<CountGauge
|
||||
v-for="(group, key) in podGauges"
|
||||
v-for="(group, key) in value.podGauges"
|
||||
:key="key"
|
||||
:total="pods.length"
|
||||
:total="value.pods.length"
|
||||
:useful="group.count || 0"
|
||||
:primary-color-var="`--sizzle-${group.color}`"
|
||||
:name="t(`workload.gaugeStates.${key}`)"
|
||||
|
|
@ -280,7 +193,7 @@ export default {
|
|||
<ResourceTabs :value="value">
|
||||
<Tab v-if="isCronJob" name="jobs" :label="t('tableHeaders.jobs')" :weight="4">
|
||||
<SortableTable
|
||||
:rows="jobs"
|
||||
:rows="value.jobs"
|
||||
:headers="jobHeaders"
|
||||
key-field="id"
|
||||
:schema="jobSchema"
|
||||
|
|
@ -290,8 +203,8 @@ export default {
|
|||
</Tab>
|
||||
<Tab v-else name="pods" :label="t('tableHeaders.pods')" :weight="4">
|
||||
<SortableTable
|
||||
v-if="pods"
|
||||
:rows="pods"
|
||||
v-if="value.pods"
|
||||
:rows="value.pods"
|
||||
:headers="podHeaders"
|
||||
key-field="id"
|
||||
:table-actions="false"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import ResourceTable from '@/components/ResourceTable';
|
||||
import { WORKLOAD_TYPES, SCHEMA, NODE } from '@/config/types';
|
||||
import { WORKLOAD_TYPES, SCHEMA, NODE, POD } from '@/config/types';
|
||||
import Loading from '@/components/Loading';
|
||||
|
||||
const schema = {
|
||||
|
|
@ -28,6 +28,8 @@ export default {
|
|||
|
||||
let resources;
|
||||
|
||||
this.loadHeathResources();
|
||||
|
||||
if ( this.allTypes ) {
|
||||
resources = await Promise.all(Object.values(WORKLOAD_TYPES).map((type) => {
|
||||
// You may not have RBAC to see some of the types
|
||||
|
|
@ -88,6 +90,29 @@ export default {
|
|||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
loadHeathResources() {
|
||||
// Fetch these in the background to populate workload health
|
||||
if ( this.allTypes ) {
|
||||
this.$store.dispatch('cluster/findAll', { type: POD });
|
||||
this.$store.dispatch('cluster/findAll', { type: WORKLOAD_TYPES.JOB });
|
||||
} else {
|
||||
const type = this.$route.params.resource;
|
||||
|
||||
if (type === WORKLOAD_TYPES.JOB) {
|
||||
// Ignore job (we're fetching this anyway, plus they contain their own state)
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === WORKLOAD_TYPES.CRON_JOB) {
|
||||
this.$store.dispatch('cluster/findAll', { type: WORKLOAD_TYPES.JOB });
|
||||
} else {
|
||||
this.$store.dispatch('cluster/findAll', { type: POD });
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
typeDisplay() {
|
||||
const { params:{ resource:type } } = this.$route;
|
||||
let paramSchema = schema;
|
||||
|
|
@ -103,5 +128,5 @@ export default {
|
|||
|
||||
<template>
|
||||
<Loading v-if="$fetchState.pending" />
|
||||
<ResourceTable v-else :schema="schema" :rows="rows" />
|
||||
<ResourceTable v-else :schema="schema" :rows="rows" :overflow-y="true" />
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ export default class ClusterRepo extends SteveModel {
|
|||
get stateObj() {
|
||||
return this.metadata?.state ? {
|
||||
...this.metadata.state,
|
||||
transitioning: this.metadata.generation > this.status.observedGeneration ? false : this.metadata.state.transitioning
|
||||
transitioning: this.metadata.generation > this.status?.observedGeneration ? false : this.metadata.state.transitioning
|
||||
} : undefined;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { findBy, insertAt } from '@/utils/array';
|
||||
import { TARGET_WORKLOADS, TIMESTAMP, UI_MANAGED } from '@/config/labels-annotations';
|
||||
import { WORKLOAD_TYPES, SERVICE } from '@/config/types';
|
||||
import { POD, WORKLOAD_TYPES, SERVICE } from '@/config/types';
|
||||
import { clone, get, set } from '@/utils/object';
|
||||
import day from 'dayjs';
|
||||
import SteveModel from '@/plugins/steve/steve-class';
|
||||
|
|
@ -127,6 +127,20 @@ export default class Workload extends SteveModel {
|
|||
this.save();
|
||||
}
|
||||
|
||||
async scaleDown() {
|
||||
const newScale = this.spec.replicas - 1;
|
||||
|
||||
if (newScale >= 0) {
|
||||
set(this.spec, 'replicas', newScale);
|
||||
await this.save();
|
||||
}
|
||||
}
|
||||
|
||||
async scaleUp() {
|
||||
set(this.spec, 'replicas', this.spec.replicas + 1);
|
||||
await this.save();
|
||||
}
|
||||
|
||||
get state() {
|
||||
if ( this.spec?.paused === true ) {
|
||||
return 'paused';
|
||||
|
|
@ -612,4 +626,93 @@ export default class Workload extends SteveModel {
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
get pods() {
|
||||
const relationships = get(this, 'metadata.relationships') || [];
|
||||
const podRelationship = relationships.filter(relationship => relationship.toType === POD)[0];
|
||||
|
||||
if (podRelationship) {
|
||||
return this.$getters['matching'](POD, podRelationship.selector).filter(pod => pod?.metadata?.namespace === this.metadata.namespace);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
get podGauges() {
|
||||
const out = {
|
||||
active: { color: 'success' }, transitioning: { color: 'info' }, warning: { color: 'warning' }, error: { color: 'error' }
|
||||
};
|
||||
|
||||
if (!this.pods) {
|
||||
return out;
|
||||
}
|
||||
|
||||
this.pods.map((pod) => {
|
||||
const { status:{ phase } } = pod;
|
||||
let group;
|
||||
|
||||
switch (phase) {
|
||||
case 'Running':
|
||||
group = 'active';
|
||||
break;
|
||||
case 'Pending':
|
||||
group = 'transitioning';
|
||||
break;
|
||||
case 'Failed':
|
||||
group = 'error';
|
||||
break;
|
||||
default:
|
||||
group = 'warning';
|
||||
}
|
||||
|
||||
out[group].count ? out[group].count++ : out[group].count = 1;
|
||||
});
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// Job Specific
|
||||
get jobRelationships() {
|
||||
if (this.type !== WORKLOAD_TYPES.CRON_JOB) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (get(this, 'metadata.relationships') || []).filter(relationship => relationship.toType === WORKLOAD_TYPES.JOB);
|
||||
}
|
||||
|
||||
get jobs() {
|
||||
if (this.type !== WORKLOAD_TYPES.CRON_JOB) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.jobRelationships.map((obj) => {
|
||||
return this.$getters['byId'](WORKLOAD_TYPES.JOB, obj.toId );
|
||||
}).filter(x => !!x);
|
||||
}
|
||||
|
||||
get jobGauges() {
|
||||
const out = {
|
||||
succeeded: { color: 'success', count: 0 }, running: { color: 'info', count: 0 }, failed: { color: 'error', count: 0 }
|
||||
};
|
||||
|
||||
if (this.type === WORKLOAD_TYPES.CRON_JOB) {
|
||||
this.jobs.forEach((job) => {
|
||||
const { status = {} } = job;
|
||||
|
||||
out.running.count += status.active || 0;
|
||||
out.succeeded.count += status.succeeded || 0;
|
||||
out.failed.count += status.failed || 0;
|
||||
});
|
||||
} else if (this.type === WORKLOAD_TYPES.JOB) {
|
||||
const { status = {} } = this;
|
||||
|
||||
out.running.count = status.active || 0;
|
||||
out.succeeded.count = status.succeeded || 0;
|
||||
out.failed.count = status.failed || 0;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -932,15 +932,18 @@ export const getters = {
|
|||
const exists = rootGetters['i18n/exists'];
|
||||
const t = rootGetters['i18n/t'];
|
||||
const labelKey = `tableHeaders.${ col.name }`;
|
||||
const description = col.description || '';
|
||||
const tooltip = description && description[description.length - 1] === '.' ? description.slice(0, -1) : description;
|
||||
|
||||
return {
|
||||
name: col.name.toLowerCase(),
|
||||
label: exists(labelKey) ? t(labelKey) : col.name,
|
||||
value: col.field.startsWith('.') ? `$${ col.field }` : col.field,
|
||||
sort: [col.field],
|
||||
name: col.name.toLowerCase(),
|
||||
label: exists(labelKey) ? t(labelKey) : col.name,
|
||||
value: col.field.startsWith('.') ? `$${ col.field }` : col.field,
|
||||
sort: [col.field],
|
||||
formatter,
|
||||
formatterOpts,
|
||||
width,
|
||||
tooltip
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue