Add Dashboard page

Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
This commit is contained in:
Francesco Torchia
2024-10-23 17:00:46 +02:00
parent 9b5bdeb85c
commit f8408469f7
27 changed files with 2680 additions and 3 deletions
@@ -0,0 +1,235 @@
<script>
import { mapGetters } from 'vuex';
import { allHash } from '@shell/utils/promise';
import { Checkbox } from '@components/Form/Checkbox';
import ModalWithCard from '@shell/components/ModalWithCard';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { Banner } from '@components/Banner';
import { HCI } from '../types';
import UpgradeInfo from './UpgradeInfo';
export default {
name: 'HarvesterUpgrade',
components: {
Checkbox, ModalWithCard, LabeledSelect, Banner, UpgradeInfo
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
const res = await allHash({
upgradeVersion: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SETTING }),
versions: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VERSION }),
upgrade: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.UPGRADE }),
});
this.upgrade = res.upgrade;
},
data() {
return {
upgrade: [],
upgradeMessage: [],
errors: '',
selectMode: true,
version: '',
enableLogging: true,
readyReleaseNote: false
};
},
computed: {
...mapGetters(['currentCluster']),
versionOptions() {
const versions = this.$store.getters['harvester/all'](HCI.VERSION);
return versions.map(V => V.metadata.name);
},
currentVersion() {
const serverVersion = this.$store.getters['harvester/byId'](HCI.SETTING, 'server-version');
return serverVersion.currentVersion || '';
},
canEnableLogging() {
return this.$store.getters['harvester/schemaFor'](HCI.UPGRADE_LOG);
},
releaseLink() {
return `https://github.com/harvester/harvester/releases/tag/${ this.version }`;
}
},
watch: {
upgrade: {
handler(neu) {
let upgradeMessage = [];
const list = neu || [];
const currentResource = list.find( O => !!O.isLatestUpgrade);
upgradeMessage = currentResource ? currentResource.upgradeMessage : [];
this.$set(this, 'upgradeMessage', upgradeMessage);
},
deep: true
},
version() {
this.readyReleaseNote = false;
}
},
methods: {
async handleUpgrade() {
const upgradeValue = {
type: HCI.UPGRADE,
metadata: {
generateName: 'hvst-upgrade-',
namespace: 'harvester-system'
},
spec: { version: this.version }
};
if (this.canEnableLogging) {
upgradeValue.spec.logEnabled = this.enableLogging;
}
const proxyResource = await this.$store.dispatch('harvester/create', upgradeValue);
try {
await proxyResource.save();
this.cancel();
} catch (err) {
if (err?.message !== '') {
this.errors = err.message;
}
}
},
cancel() {
this.$refs.deleteTip.hide();
this.errors = '';
},
open() {
this.$refs.deleteTip.open();
},
}
};
</script>
<template>
<div v-if="currentCluster">
<header class="header-layout header mb-0">
<h1>
<t
k="harvester.dashboard.header"
:cluster="currentCluster.nameDisplay"
/>
</h1>
<button v-if="versionOptions.length" type="button" class="btn bg-warning btn-sm" @click="open">
<t k="harvester.upgradePage.upgrade" />
</button>
</header>
<ModalWithCard ref="deleteTip" name="deleteTip" :width="850">
<template #title>
<t k="harvester.upgradePage.upgradeApp" />
</template>
<template #content>
<UpgradeInfo :version="version" />
<div class="currentVersion mb-15">
<span> <t k="harvester.upgradePage.currentVersion" /> </span>
<span class="version">{{ currentVersion }}</span>
</div>
<div>
<LabeledSelect
v-model="version"
class="mb-10"
:label="t('harvester.upgradePage.versionLabel')"
:options="versionOptions"
:clearable="true"
/>
<div v-if="canEnableLogging" class="mb-5">
<Checkbox v-model="enableLogging" class="check" type="checkbox" :label="t('harvester.upgradePage.enableLogging')" />
</div>
<div v-if="version">
<p v-clean-html="t('harvester.upgradePage.releaseTip', {url: releaseLink}, true)" class="mb-10"></p>
<Checkbox v-model="readyReleaseNote" class="check" type="checkbox" label-key="harvester.upgradePage.checkReady" />
</div>
<Banner v-if="errors.length" color="warning">
{{ errors }}
</Banner>
</div>
</template>
<template #footer>
<div class="footer">
<button class="btn role-secondary mr-20" @click.prevent="cancel">
<t k="generic.close" />
</button>
<button :disabled="!readyReleaseNote" class="btn role-tertiary bg-primary" @click.prevent="handleUpgrade">
<t k="harvester.upgradePage.upgrade" />
</button>
</div>
</template>
</ModalWithCard>
</div>
</template>
<style lang="scss" scoped>
.header {
display: flex;
justify-content: space-between;
align-items: center;
}
.footer {
width: 100%;
display: flex;
justify-content: flex-end;
}
.banner-icon {
display: flex;
align-items: center;
}
.banner-content {
display: flex;
}
.banner-message {
display: flex;
flex-direction: column;
justify-content: center;
padding: 0 15px;
}
.icon {
font-size: 20px;
width: 20px;
line-height: 23px;
}
.currentVersion {
margin-top: 10px;
display: flex;
justify-content: space-between;
.version {
font-size: 16px;
font-weight: bold;
}
}
</style>
@@ -0,0 +1,348 @@
<script>
import { NODE } from '@shell/config/types';
import { allHash } from '@shell/utils/promise';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import PercentageBar from '@shell/components/PercentageBar';
import BadgeStateFormatter from '@shell/components/formatter/BadgeStateFormatter';
import { mapGetters } from 'vuex';
import { PRODUCT_NAME as HARVESTER } from '../config/harvester';
import { HCI } from '../types';
import ProgressBarList from './HarvesterUpgradeProgressBarList';
export default {
name: 'HarvesterUpgradeHeader',
components: {
PercentageBar, ProgressBarList, BadgeStateFormatter
},
async fetch() {
const hash = {};
if (this.$store.getters['harvester/schemaFor'](HCI.IMAGE)) {
hash.images = this.$store.dispatch('harvester/findAll', { type: HCI.IMAGE });
}
if (this.$store.getters['harvester/schemaFor'](HCI.UPGRADE)) {
hash.upgrades = this.$store.dispatch('harvester/findAll', { type: HCI.UPGRADE });
}
if (this.$store.getters['harvester/schemaFor'](NODE)) {
hash.nodes = this.$store.dispatch('harvester/findAll', { type: NODE });
}
if (this.$store.getters['harvester/schemaFor'](HCI.UPGRADE_LOG)) {
hash.upgradeLogs = this.$store.dispatch('harvester/findAll', { type: HCI.UPGRADE_LOG });
}
await allHash(hash);
},
data() {
return {
filename: '',
logDownloading: false
};
},
computed: {
...mapGetters(['currentProduct', 'isVirtualCluster']),
enabled() {
return this.isVirtualCluster && this.currentProduct.name === HARVESTER;
},
latestResource() {
return this.$store.getters['harvester/all'](HCI.UPGRADE).find( U => U.isLatestUpgrade);
},
latestUpgradeLogResource() {
const upgradeLogId = `${ this.latestResource.id }-upgradelog`;
return this.$store.getters['harvester/all'](HCI.UPGRADE_LOG).find( U => U.id === upgradeLogId);
},
downloadLogFailReason() {
if (!this.filename) {
const filename = this.latestUpgradeLogResource?.latestArchivesFileName;
return this.latestUpgradeLogResource?.downloadArchivesStatus(filename);
}
return this.latestUpgradeLogResource?.downloadArchivesStatus(this.filename);
},
canStartedDownload() {
return this.latestUpgradeLogResource?.canStartedDownload || false;
},
overallMessage() {
return this.latestResource?.overallMessage;
},
upgradeImage() {
const id = this.latestResource?.upgradeImage;
return this.$store.getters['harvester/all'](HCI.IMAGE).find(I => I.id === id);
},
imageProgress() {
return this.upgradeImage?.progress || 0;
},
showImage() {
return !this.latestResource.isUpgradeSucceeded;
},
imageMessage() {
return this.latestResource?.upgradeImageMessage;
},
repoReady() {
return this.latestResource.createRepo;
},
isShow() {
return this.latestResource && !this.latestResource.hasReadMessage;
},
nodesStatus() {
return this.latestResource?.nodeUpgradeMessage;
},
sysServiceUpgradeMessage() {
return this.latestResource?.sysServiceUpgradeMessage;
},
sysServiceTotal() {
return this.sysServiceUpgradeMessage?.[0].percent || 0;
},
nodesPercent() {
return this.latestResource?.nodeTotalPercent || 0;
},
repoInfo() {
return this.latestResource.repoInfo;
},
releaseLink() {
return `https://github.com/harvester/harvester/releases/tag/${ this.latestResource?.spec?.version }`;
},
upgradeVersion() {
return this.latestResource?.spec?.version;
}
},
methods: {
ignoreMessage() {
this.latestResource.setLabel(HCI_ANNOTATIONS.REAY_MESSAGE, 'true');
this.latestResource.save();
},
async generateLogFileName() {
const res = await this.latestUpgradeLogResource.doActionGrowl('generate');
this.filename = res?.data;
},
waitFileGeneratedReady() {
const id = this.latestUpgradeLogResource.id;
return new Promise((resolve) => {
let log;
const timer = setInterval(async() => {
log = await this.$store.dispatch('harvester/find', {
type: HCI.UPGRADE_LOG,
id,
opt: { force: true }
}, { root: true });
if (log.fileIsReady(this.filename)) {
this.logDownloading = false;
clearInterval(timer);
resolve();
}
}, 2500);
});
},
async downloadLog() {
this.logDownloading = true;
await this.generateLogFileName();
this.waitFileGeneratedReady().then(() => {
if (!this.downloadLogFailReason) {
this.latestUpgradeLogResource.downloadLog(this.filename);
}
this.logDownloading = false;
});
}
}
};
</script>
<template>
<div v-if="enabled && isShow" class="upgrade">
<v-popover
v-clean-tooltip="{
placement: 'bottom-left',
}"
class="hand"
>
<slot name="button-content">
<i class="warning icon-fw icon icon-dot-open dot-icon" />
</slot>
<template slot="popover">
<div class="upgrade-info mb-10">
<div v-if="repoInfo" class="repoInfo">
<div class="row">
<div class="col span-12">
<a :href="releaseLink" target="_blank">{{ upgradeVersion }}</a>
</div>
</div>
<div v-if="latestResource" class="row mb-5">
<div class="col span-12">
<p class="state">
{{ t('harvester.upgradePage.repoInfo.upgradeStatus') }}: <BadgeStateFormatter class="ml-5" :row="latestResource" />
</p>
</div>
</div>
<div v-if="downloadLogFailReason" class="row mb-5">
<div class="col span-12">
<p class="state">
{{ t('harvester.upgradePage.repoInfo.logStatus') }}: <span class="error ml-5">{{ downloadLogFailReason }}</span>
</p>
</div>
</div>
<p class="bordered-section"></p>
<div class="row mb-5">
<div class="col span-6">
{{ t('harvester.upgradePage.repoInfo.os') }}: <span class="text-muted">{{ repoInfo.release.os }}</span>
</div>
<div class="col span-6">
{{ t('harvester.productLabel') }}: <span class="text-muted">{{ repoInfo.release.harvester }}</span>
</div>
</div>
<div class="row mb-5">
<div class="col span-6">
{{ t('harvester.upgradePage.repoInfo.harvesterChart') }}: <span class="text-muted">{{ repoInfo.release.harvesterChart }}</span>
</div>
<div class="col span-6">
{{ t('harvester.upgradePage.repoInfo.monitoringChart') }}: <span class="text-muted">{{ repoInfo.release.monitoringChart }}</span>
</div>
</div>
<div class="row mb-5">
<div class="col span-6">
{{ t('harvester.upgradePage.repoInfo.kubernetes') }}: <span class="text-muted">{{ repoInfo.release.kubernetes }}</span>
</div>
<div class="col span-6">
{{ t('product.rancher') }}: <span class="text-muted">{{ repoInfo.release.rancher }}</span>
</div>
</div>
<p class="bordered-section"></p>
</div>
<p v-if="overallMessage" class="text-warning mb-20">
{{ overallMessage }}
</p>
<div v-if="showImage">
<h4>{{ t('harvester.upgradePage.upgradeImage') }}<span class="float-r text-info">{{ imageProgress }}%</span></h4>
<PercentageBar :value="imageProgress" preferred-direction="MORE" />
<p class="text-warning">
{{ imageMessage }}
</p>
<p class="bordered-section"></p>
</div>
<h4>{{ t('harvester.upgradePage.createRepository') }} <span class="float-r text-info">{{ repoReady.isReady ? t('harvester.upgradePage.succeeded') : t('harvester.upgradePage.pending') }}</span></h4>
<p v-if="repoReady.message" class="text-warning">
{{ repoReady.message }}
</p>
<p class="bordered-section"></p>
<ProgressBarList :title="t('harvester.upgradePage.upgradeNode')" :precent="nodesPercent" :list="nodesStatus" />
<p class="bordered-section"></p>
<ProgressBarList :title="t('harvester.upgradePage.upgradeSysService')" :precent="sysServiceTotal" :list="sysServiceUpgradeMessage" />
</div>
<div class="footer">
<button v-if="canStartedDownload" :disabled="logDownloading" class="btn role-primary mr-10" @click="downloadLog()">
<i class="icon mr-10" :class="[logDownloading ? 'icon-spinner icon-spin' : 'icon-download']"></i> {{ t('harvester.upgradePage.repoInfo.downloadLog') }}
</button>
<button v-if="latestResource.isUpgradeSucceeded" class="btn role-primary" @click="ignoreMessage()">
{{ t('harvester.upgradePage.dismissMessage') }}
</button>
</div>
</template>
</v-popover>
</div>
</template>
<style lang="scss" scoped>
a {
float: right;
color: var(--link) !important;
text-decoration: none;
}
.upgrade {
height: 100%;
min-width: 40px;
display: flex;
align-items: center;
.dot-icon {
font-size: 24px;
vertical-align: middle;
color: #00a483;
}
}
.upgrade-info {
min-width: 550px;
max-height: 90vh;
overflow-y: scroll;
.repoInfo {
.col span {
word-break: break-all
}
p.state {
display: flex;
align-items: center;
}
}
.error {
color: var(--error);
}
.float-r {
float: right;
}
p {
word-break: break-word;
margin-top: 5px;
}
}
.footer {
display: flex;
justify-content: flex-end;
}
</style>
@@ -0,0 +1,109 @@
<script>
import Collapse from '@shell/components/Collapse';
import PercentageBar from '@shell/components/PercentageBar';
export default {
name: 'HarvesterUpgradeProgressList',
components: { PercentageBar, Collapse },
props: {
title: {
type: String,
default: ''
},
precent: {
type: Number,
default: 0
},
list: {
type: Array,
default: () => {
return [];
}
}
},
data() {
return { open: true };
},
methods: {
handleSwitch() {
this.open = !this.open;
}
}
};
</script>
<template>
<div class="bar-list">
<h4>{{ title }} <span class="float-r text-info">{{ precent }}%</span></h4>
<div>
<div>
<Collapse :open.sync="open">
<template #title>
<div class="total-bar">
<span class="bar"><PercentageBar :value="precent" preferred-direction="MORE" /></span>
<span class="on-off" @click="handleSwitch"> {{ open ? t('harvester.generic.close') : t('harvester.generic.open') }}</span>
</div>
</template>
<template>
<div class="custom-content">
<div v-for="item in list" :key="item.name" class="item">
<p>{{ item.name }} <span class="status" :class="{ [item.state]: true }">{{ item.state }}</span></p>
<PercentageBar :value="item.percent" preferred-direction="MORE" />
<p class="warning">
{{ item.message }}
</p>
</div>
</div>
</template>
</Collapse>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.bar-list {
.float-r {
float: right;
}
.total-bar {
display: flex;
user-select: none;
.bar {
width: 85%;
}
.on-off {
margin-left: 10px;
cursor: pointer;
}
}
.custom-content {
.item {
margin-bottom: 14px;
p {
margin-bottom: 4px;
}
.status {
float: right;
}
.Succeeded, .Upgrading, .Pending {
color: var(--success);
}
.failed {
color: var(--error)
}
.warning {
color: var(--error);
}
}
}
}
</style>
+42
View File
@@ -0,0 +1,42 @@
<script>
import { Banner } from '@components/Banner';
export default {
name: 'HarvesterUpgradeInfo',
components: { Banner },
props: {
version: {
type: String,
default: ''
}
},
computed: {
releaseVersion() {
return !!this.version ? `https://github.com/harvester/harvester/releases/tag/${ this.version }` : `https://github.com/harvester/harvester/releases`;
}
},
};
</script>
<template>
<div>
<Banner color="warning">
<div>
<strong>{{ t('harvester.upgradePage.upgradeInfo.warning') }}:</strong>
<p v-clean-html="t('harvester.upgradePage.upgradeInfo.doc', {}, true)" class="mb-5">
</p>
<p class="mb-5">
{{ t('harvester.upgradePage.upgradeInfo.tip') }}
</p>
<p class="mb-5">
{{ t('harvester.upgradePage.upgradeInfo.moreNotes') }} <a :href="releaseVersion" target="_blank">{{ t('generic.moreInfo') }} </a>
</p>
</div>
</Banner>
</div>
</template>