Add pkg/harvester components + shell portings - 1

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 f8408469f7
commit 4f2688f6ab
265 changed files with 42316 additions and 82 deletions
@@ -0,0 +1,12 @@
<script>
import ResourceDetail from '@shell/components/ResourceDetail/index.vue';
export default {
name: 'HarvesterResourcedId',
components: { ResourceDetail },
};
</script>
<template>
<ResourceDetail />
</template>
@@ -0,0 +1,12 @@
<script>
import ResourceDetail from '@shell/components/ResourceDetail/index.vue';
export default {
name: 'HarvesterResourcedNamespaceId',
components: { ResourceDetail },
};
</script>
<template>
<ResourceDetail />
</template>
@@ -0,0 +1,12 @@
<script>
import ResourceDetail from '@shell/components/ResourceDetail/index.vue';
export default {
name: 'HarvesterResourceCreate',
components: { ResourceDetail },
};
</script>
<template>
<ResourceDetail />
</template>
@@ -0,0 +1,12 @@
<script>
import ResourceList from '@shell/components/ResourceList/index.vue';
export default {
name: 'HarvesterResourcedList',
components: { ResourceList },
};
</script>
<template>
<ResourceList />
</template>
@@ -0,0 +1,328 @@
<script>
import CruResource from '@shell/components/CruResource';
import { RadioGroup } from '@components/Form/Radio';
import { Checkbox } from '@components/Form/Checkbox';
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import UpgradeInfo from '../../../../components/UpgradeInfo';
import { HCI } from '../../../../types';
import { exceptionToErrorsArray } from '@shell/utils/error';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../../config/harvester';
const IMAGE_METHOD = {
NEW: 'new',
EXIST: 'exist'
};
const DOWNLOAD = 'download';
const UPLOAD = 'upload';
export default {
name: 'HarvesterAirgapUpgrade',
components: {
Checkbox, CruResource, LabeledSelect, LabeledInput, RadioGroup, UpgradeInfo
},
async fetch() {
await this.$store.dispatch('harvester/findAll', { type: HCI.IMAGE });
const value = await this.$store.dispatch('harvester/create', {
type: HCI.UPGRADE,
metadata: {
generateName: 'hvst-upgrade-',
namespace: 'harvester-system',
},
spec: { image: '' }
});
const imageValue = await this.$store.dispatch('harvester/create', {
type: HCI.IMAGE,
metadata: {
name: '',
namespace: 'harvester-system',
generateName: 'image-',
annotations: {}
},
spec: {
sourceType: UPLOAD,
displayName: '',
checksum: ''
},
});
this.value = value;
this.imageValue = imageValue;
},
data() {
return {
value: null,
file: {},
imageId: '',
imageSource: IMAGE_METHOD.NEW,
sourceType: UPLOAD,
imageValue: null,
errors: [],
enableLogging: true,
IMAGE_METHOD
};
},
computed: {
doneRoute() {
return `${ HARVESTER_PRODUCT }-c-cluster-resource`;
},
osImageOptions() {
return this.$store.getters['harvester/all'](HCI.IMAGE)
.filter(I => I.isOSImage)
.map((I) => {
return {
label: I.spec.displayName,
value: I.id,
disabled: !I.isReady
};
});
},
uploadImage() {
return this.imageSource === IMAGE_METHOD.NEW;
},
fileName() {
return this.file?.name || '';
},
canEnableLogging() {
return this.$store.getters['harvester/schemaFor'](HCI.UPGRADE_LOG);
},
},
methods: {
done() {
this.$router.push({
name: this.doneRoute,
params: { resource: HCI.SETTING, product: 'harvester' }
});
},
async save(buttonCb) {
let res = null;
this.errors = [];
if (!this.imageValue.spec.displayName && this.uploadImage) {
this.errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('generic.name') }));
buttonCb(false);
return;
}
try {
if (this.imageSource === IMAGE_METHOD.NEW) {
this.imageValue.metadata.annotations[HCI_ANNOTATIONS.OS_UPGRADE_IMAGE] = 'True';
if (this.sourceType === UPLOAD) {
this.imageValue.spec.sourceType = UPLOAD;
const file = this.file;
if (!file.name) {
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.selectExitImage'));
buttonCb(false);
return;
}
this.imageValue.spec.url = '';
this.imageValue.metadata.annotations[HCI_ANNOTATIONS.IMAGE_NAME] = file.name;
res = await this.imageValue.save();
res.uploadImage(file);
} else if (this.sourceType === DOWNLOAD) {
this.imageValue.spec.sourceType = DOWNLOAD;
if (!this.imageValue.spec.url) {
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.imageUrl'));
buttonCb(false);
return;
}
res = await this.imageValue.save();
}
this.value.spec.image = res.id;
} else if (this.imageSource === IMAGE_METHOD.EXIST) {
if (!this.imageId) {
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.chooseFile'));
return;
}
this.value.spec.image = this.imageId;
}
if (this.canEnableLogging) {
this.value.spec.logEnabled = this.enableLogging;
}
await this.value.save();
this.done();
buttonCb(true);
} catch (e) {
this.errors = exceptionToErrorsArray(e);
buttonCb(false);
}
},
handleFileUpload() {
this.file = this.$refs.file.files[0];
},
selectFile() {
this.$refs.file.value = null;
this.$refs.file.click();
},
},
watch: {
'imageValue.spec.url': {
handler(neu) {
const suffixName = neu?.split('/')?.pop();
const splitName = suffixName?.split('.') || [];
const fileSuffix = splitName?.pop()?.toLowerCase();
if (splitName.length > 1 && fileSuffix === 'iso' && !this.imageValue.spec.displayName) {
this.imageValue.spec.displayName = suffixName;
}
},
deep: true
},
file(neu) {
if (!this.imageValue.spec.displayName && neu.name) {
this.imageValue.spec.displayName = neu.name;
}
}
}
};
</script>
<template>
<div v-if="value" id="air-gap">
<h3 class="mb-20">
{{ t('harvester.upgradePage.osUpgrade') }}
</h3>
<CruResource
:done-route="doneRoute"
:resource="value"
mode="create"
:errors="errors"
:can-yaml="false"
finish-button-mode="upgrade"
:cancel-event="true"
@finish="save"
@cancel="done"
>
<RadioGroup
v-model="imageSource"
class="image-group"
name="image"
:options="[
IMAGE_METHOD.NEW,
IMAGE_METHOD.EXIST,
]"
:labels="[
t('harvester.upgradePage.uploadNew'),
t('harvester.upgradePage.selectExisting'),
]"
/>
<UpgradeInfo />
<div v-if="uploadImage">
<LabeledInput v-model.trim="imageValue.spec.displayName" class="mb-20" label-key="harvester.fields.name" required />
<LabeledInput v-model="imageValue.spec.checksum" class="mb-10" label-key="harvester.setting.upgrade.checksum" />
<Checkbox
v-if="canEnableLogging"
v-model="enableLogging"
class="check mb-20"
type="checkbox"
:label="t('harvester.upgradePage.enableLogging')"
/>
<RadioGroup
v-model="sourceType"
class="mb-20 image-group"
name="sourceType"
:options="[
'upload',
'download',
]"
:labels="[
t('harvester.image.sourceType.upload'),
t('harvester.image.sourceType.download')
]"
/>
<LabeledInput
v-if="sourceType === 'download'"
v-model.trim="imageValue.spec.url"
class="labeled-input--tooltip"
required
label-key="harvester.image.url"
/>
<div v-else class="chooseFile">
<button
type="button"
class="btn role-primary"
@click="selectFile"
>
{{ t('harvester.image.uploadFile') }}
<input
v-show="false"
id="file"
ref="file"
type="file"
accept=".iso"
@change="handleFileUpload()"
/>
</button>
<span :class="{ 'text-muted': !fileName }" class="ml-20">
{{ fileName ? fileName : t('harvester.generic.noFileChosen') }}
</span>
</div>
</div>
<LabeledSelect
v-else
v-model="imageId"
:options="osImageOptions"
required
class="mb-20"
label-key="harvester.fields.image"
/>
</CruResource>
</div>
</template>
<style lang="scss" scoped>
#air-gap {
::v-deep .image-group .radio-group {
display: flex;
.radio-container {
margin-right: 30px;
}
}
.chooseFile {
display: flex;
align-items: center;
}
}
</style>
@@ -0,0 +1,12 @@
<script>
import ResourceDetail from '@shell/components/ResourceDetail';
export default {
name: 'AlertmanagerConfig',
components: { ResourceDetail },
};
</script>
<template>
<ResourceDetail />
</template>
@@ -0,0 +1,304 @@
<script>
import ActionMenu from '@shell/components/ActionMenu';
import ReceiverConfig from '@shell/edit/monitoring.coreos.com.alertmanagerconfig/receiverConfig';
import ButtonGroup from '@shell/components/ButtonGroup';
import ResourceYaml from '@shell/components/ResourceYaml';
import { createYaml } from '@shell/utils/create-yaml';
import { EDITOR_MODES } from '@shell/components/YamlEditor';
import { MONITORING, SCHEMA } from '@shell/config/types';
import {
_CREATE, _EDIT, _VIEW, _CONFIG, _YAML, _DETAIL
} from '@shell/config/query-params';
import { clone } from '@shell/utils/object';
export default {
name: 'AlertmanagerConfigReceiverCreateEdit',
components: {
ActionMenu,
ButtonGroup,
ReceiverConfig,
ResourceYaml
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
this.receiverName = this.$route.query.receiverName;
const alertmanagerConfigId = this.$route.params.alertmanagerconfigid;
const originalAlertmanagerConfigResource = await this.$store.dispatch(`${ inStore }/find`, { type: MONITORING.ALERTMANAGERCONFIG, id: alertmanagerConfigId });
const alertmanagerConfigResource = await this.$store.dispatch(`${ inStore }/clone`, { resource: originalAlertmanagerConfigResource });
const mode = this.$route.query.mode;
if (mode !== _CREATE) {
const existingReceiverData = (alertmanagerConfigResource.spec.receivers || []).find((receiverData) => {
return receiverData.name === this.receiverName;
});
if (existingReceiverData) {
this.receiverValue = existingReceiverData;
}
}
this.alertmanagerConfigId = alertmanagerConfigResource.id;
this.alertmanagerConfigResource = alertmanagerConfigResource;
this.alertmanagerConfigDetailRoute = alertmanagerConfigResource._detailLocation;
},
// take edit link and edit request from AlertmanagerConfig resource
// and pass it to ReceiverConfig as a prop
data() {
return {
actionMenuTargetElement: null,
actionMenuTargetEvent: null,
alertmanagerConfigId: '',
alertmanagerConfigResource: null,
alertmanagerConfigDetailRoute: null,
config: _CONFIG,
create: _CREATE,
detail: _DETAIL,
edit: _EDIT,
receiverActionMenuIsOpen: false,
receiverName: '',
receiverValue: {},
showPreview: false,
view: _VIEW,
viewOptions: [
{
labelKey: 'resourceDetail.masthead.config',
value: 'config',
}, {
labelKey: 'resourceDetail.masthead.yaml',
value: _YAML,
}
],
yaml: _YAML
};
},
computed: {
currentView() {
return this.$route.query.currentView;
},
receiverActions() {
const alertmanagerConfigActions = this.alertmanagerConfigResource?.availableActions;
if (!alertmanagerConfigActions) {
return [];
}
// Receivers are not a separate resource, so they
// should only have a subset of the AlertmanagerConfig
// actions. So we take AlertmanagerConfig's actions and filter
// out any that don't apply.
// Example action data:
// {
// "action": "goToEdit",
// "label": "Edit Config",
// "icon": "icon icon-edit",
// "enabled": true
// },
return this.alertmanagerConfigResource.getReceiverActions(alertmanagerConfigActions);
},
resourceYaml() {
const resource = this.alertmanagerConfigResource;
const inStore = this.$store.getters['currentStore'](resource);
const schemas = this.$store.getters[`${ inStore }/all`](SCHEMA);
const clonedResource = clone(resource);
const out = createYaml(schemas, MONITORING.ALERTMANAGERCONFIG, clonedResource);
return out;
},
mode() {
// Use the route as a dependency of the
// computed property so that the component
// updates when you navigate between edit
// and view.
return this.$route.query.mode;
},
editorMode() {
if ( this.mode === this.view ) {
return EDITOR_MODES.VIEW_CODE;
} else if ( this.showPreview ) {
return EDITOR_MODES.DIFF_CODE;
}
return EDITOR_MODES.EDIT_CODE;
},
heading() {
switch (this.$route.query.mode) {
case this.create:
return this.t('monitoring.alertmanagerConfig.receiverFormNames.create');
case this.edit:
if (this.currentView === this.yaml || this.$route.query.as === this.yaml) {
// When you edit as YAML, you edit the whole AlertmanagerConfig
// at once, so the header is just "Edit AlertmanagerConfig"
return this.t('monitoring.alertmanagerConfig.receiverFormNames.editYaml');
}
// When you edit as a form, you edit only the receiver,
// so the form header is "Edit Receiver in AlertmanagerConfig"
return this.t('monitoring.alertmanagerConfig.receiverFormNames.edit');
default:
return this.t('monitoring.alertmanagerConfig.receiverFormNames.detail');
}
},
},
methods: {
// When creating or editing a receiver, in both cases
// it is actually the one existing AlertmanagerConfig
// being saved. Therefore we take the save from the
// AlertmanagerConfig resource and pass it into the
// receiver config form.
saveOverride(buttonDone) {
if (this.alertmanagerConfigResource.yamlError) {
this.alertmanagerConfigResource.errors = this.alertmanagerConfigResource.errors || [];
this.alertmanagerConfigResource.errors.push(this.alertmanagerConfigResource.yamlError);
buttonDone(false);
return;
}
this.alertmanagerConfigResource.save(...arguments);
this.redirectToAlertmanagerConfigDetail();
},
handleButtonGroupClick(event) {
if (event === this.yaml) {
this.goToEditYaml(this.view);
}
if (event === this.config) {
this.goToEdit(this.view);
}
},
toggleReceiverActionMenu() {
this.receiverActionMenuIsOpen = !this.receiverActionMenuIsOpen;
},
handleReceiverActionMenuClick(event) {
this.actionMenuTargetElement = this.$refs.actions;
this.actionMenuTargetEvent = event;
this.toggleReceiverActionMenu();
},
goToEdit() {
// 'goToEdit' is the exact name of an action for AlertmanagerConfig
// and this method executes the action.
this.$router.push(this.alertmanagerConfigResource.getEditReceiverConfigRoute(this.receiverValue.name, _EDIT));
},
goToEditYaml() {
// 'goToEditYaml' is the exact name of an action for AlertmanagerConfig
// and this method executes the action.
this.$router.push(this.alertmanagerConfigResource.getEditReceiverYamlRoute(this.receiverValue.name, _EDIT));
},
promptRemove(actionData) {
// 'promptRemove' is the exact name of an action for AlertmanagerConfig
// and this method executes the action.
// Get the name of the receiver to delete from the action info.
const nameOfReceiverToDelete = actionData.route.query.receiverName;
// Remove it from the configuration of the parent AlertmanagerConfig
// resource.
const existingReceivers = this.alertmanagerConfigResource.spec.receivers || [];
const receiversMinusDeletedItem = existingReceivers.filter((receiver) => {
return receiver.name !== nameOfReceiverToDelete;
});
this.alertmanagerConfigResource.spec.receivers = receiversMinusDeletedItem;
// After saving the AlertmanagerConfig, the resource has been deleted.
this.alertmanagerConfigResource.save(...arguments);
this.$router.push(this.alertmanagerConfigResource._detailLocation);
},
redirectToReceiverDetail(receiverName) {
return this.alertmanagerConfigResource.getReceiverDetailLink(receiverName);
},
redirectToAlertmanagerConfigDetail() {
const route = this.alertmanagerConfigResource._detailLocation;
this.$router.push(route);
}
}
};
</script>
<template>
<div>
<header class="header">
<div class="title">
<div class="primaryheader">
<h1>
{{ heading }}
</h1>
</div>
</div>
<div class="actions-container">
<div class="actions">
<ButtonGroup
v-if="viewOptions && mode === view"
:value="currentView"
:options="viewOptions"
@input="handleButtonGroupClick"
/>
<button
v-if="mode === view"
ref="actions"
aria-haspopup="true"
type="button"
class="btn role-multi-action actions"
@click="handleReceiverActionMenuClick"
>
<i class="icon icon-actions" />
</button>
</div>
</div>
</header>
<ResourceYaml
v-if="currentView === yaml && alertmanagerConfigResource"
ref="resourceyaml"
:value="alertmanagerConfigResource"
:mode="mode"
:initial-yaml-for-diff="null"
:yaml="resourceYaml"
:offer-preview="mode === edit"
:done-route="JSON.stringify(redirectToReceiverDetail(receiverName))"
:done-override="alertmanagerConfigDetailRoute"
:apply-hooks="alertmanagerConfigResource.applyHooks"
@error="e=>$emit('error', e)"
/>
<ReceiverConfig
v-if="(currentView === config || currentView === detail) && alertmanagerConfigResource"
:value="receiverValue"
:mode="mode"
:alertmanager-config-id="alertmanagerConfigId"
:alertmanager-config-resource="alertmanagerConfigResource"
:save-override="saveOverride"
/>
<ActionMenu
:custom-actions="receiverActions"
:open="receiverActionMenuIsOpen"
:use-custom-target-element="true"
:custom-target-element="actionMenuTargetElement"
:custom-target-event="actionMenuTargetEvent"
@close="receiverActionMenuIsOpen = false"
@goToEdit="goToEdit"
@goToEditYaml="goToEditYaml"
@promptRemove="promptRemove"
/>
</div>
</template>
<style lang='scss' scoped>
.header{
H1{
flex: 1;
}
border-bottom: 1px solid var(--border);
margin-bottom: 0;
padding-bottom: 20px;
}
</style>
@@ -0,0 +1,24 @@
<script>
export default {
name: 'AlertmanagerConfig',
data() {
return {};
},
};
</script>
<template>
<div>AlertmanagerConfig</div>
</template>
<style lang='scss' scoped>
.header{
display: flex;
H1{
flex: 1;
}
}
</style>
@@ -0,0 +1,376 @@
<script>
import { LabeledInput } from '@components/Form/LabeledInput';
import ColorInput from '@shell/components/form/ColorInput';
import TypeDescription from '@shell/components/TypeDescription';
import { Checkbox } from '@components/Form/Checkbox';
import FileSelector from '@shell/components/form/FileSelector';
import SimpleBox from '@shell/components/SimpleBox';
import Loading from '@shell/components/Loading';
import AsyncButton from '@shell/components/AsyncButton';
import { Banner } from '@components/Banner';
import { allHash } from '@shell/utils/promise';
import { MANAGEMENT } from '@shell/config/types';
import { getVendor, setVendor } from '@shell/config/private-label';
import { fetchOrCreateSetting } from '@shell/utils/settings';
import { SETTING } from '@shell/config/settings';
import { _EDIT, _VIEW } from '@shell/config/query-params';
import { setFavIcon } from '@shell/utils/favicon';
const Color = require('color');
export default {
components: {
LabeledInput, Checkbox, FileSelector, Loading, SimpleBox, AsyncButton, Banner, ColorInput, TypeDescription
},
async fetch() {
const hash = await allHash({
uiPLSetting: this.$store.dispatch('management/find', { type: MANAGEMENT.SETTING, id: SETTING.PL }),
uiLogoDarkSetting: fetchOrCreateSetting(this.$store, SETTING.LOGO_DARK, ''),
uiLogoLightSetting: fetchOrCreateSetting(this.$store, SETTING.LOGO_LIGHT, ''),
uiColorSetting: fetchOrCreateSetting(this.$store, SETTING.PRIMARY_COLOR, ''),
uiLinkColorSetting: fetchOrCreateSetting(this.$store, SETTING.LINK_COLOR, ''),
uiFaviconSetting: fetchOrCreateSetting(this.$store, SETTING.FAVICON, ''),
});
Object.assign(this, hash);
if (hash.uiLogoDarkSetting.value) {
try {
this.uiLogoDark = hash.uiLogoDarkSetting.value;
this.customizeLogo = true;
} catch {}
}
if (hash.uiLogoLightSetting.value) {
try {
this.uiLogoLight = hash.uiLogoLightSetting.value;
this.customizeLogo = true;
} catch {}
}
if (hash.uiFaviconSetting.value) {
try {
this.uiFavicon = hash.uiFaviconSetting.value;
this.customizeFavicon = true;
} catch {}
}
if (hash.uiColorSetting.value) {
this.uiColor = Color(hash.uiColorSetting.value).hex();
this.customizeColor = true;
}
if (hash.uiLinkColorSetting.value) {
this.uiLinkColor = Color(hash.uiLinkColorSetting.value).hex();
this.customizeLinkColor = true;
}
},
data() {
return {
vendor: getVendor(),
uiPLSetting: {},
uiLogoDarkSetting: {},
uiLogoDark: '',
uiLogoLightSetting: {},
uiLogoLight: '',
customizeLogo: false,
uiFaviconSetting: {},
uiFavicon: '',
customizeFavicon: false,
uiColorSetting: {},
uiColor: null,
customizeColor: false,
uiLinkColorSetting: {},
uiLinkColor: null,
customizeLinkColor: false,
errors: [],
};
},
computed: {
mode() {
const schema = this.$store.getters[`management/schemaFor`](MANAGEMENT.SETTING);
return schema?.resourceMethods?.includes('PUT') ? _EDIT : _VIEW;
}
},
mounted() {
let uiColor = getComputedStyle(document.body).getPropertyValue('--primary');
let uiLinkColor = getComputedStyle(document.body).getPropertyValue('--link');
const suse = document.querySelector('.suse');
if (suse) {
uiColor = getComputedStyle(suse).getPropertyValue('--primary');
uiLinkColor = getComputedStyle(suse).getPropertyValue('--link');
}
// Only set the color to the default if not already set from the custom color
this.uiColor = this.uiColor || uiColor.trim();
this.uiLinkColor = this.uiLinkColor || uiLinkColor.trim();
},
methods: {
updateLogo(img, key) {
this[key] = img;
},
setError(e) {
this.errors = [];
this.errors.push(e);
},
async save(btnCB) {
this.uiPLSetting.value = this.uiPLSetting.value.replaceAll(/[\<>&=#()"]/gm, '');
if (this.customizeLogo) {
this.uiLogoLightSetting.value = this.uiLogoLight;
this.uiLogoDarkSetting.value = this.uiLogoDark;
} else {
this.uiLogoLightSetting.value = '';
this.uiLogoDarkSetting.value = '';
}
if (this.customizeFavicon) {
this.uiFaviconSetting.value = this.uiFavicon;
} else {
this.uiFaviconSetting.value = '';
}
if (this.customizeColor) {
this.uiColorSetting.value = Color(this.uiColor).rgb().string();
} else {
this.uiColorSetting.value = null;
}
if (this.customizeLinkColor) {
this.uiLinkColorSetting.value = Color(this.uiLinkColor).rgb().string();
} else {
this.uiLinkColorSetting.value = null;
}
this.errors = [];
try {
await Promise.all([
this.uiPLSetting.save(),
this.uiLogoDarkSetting.save(),
this.uiLogoLightSetting.save(),
this.uiColorSetting.save(),
this.uiLinkColorSetting.save(),
this.uiFaviconSetting.save()
]);
if (this.uiPLSetting.value !== this.vendor) {
setVendor(this.uiPLSetting.value);
}
setFavIcon(this.$store);
btnCB(true);
} catch (err) {
this.errors.push(err);
btnCB(false);
}
},
}
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<div v-else>
<h1 class="mb-20">
{{ t('branding.label') }}
</h1>
<TypeDescription resource="branding" />
<div>
<div class="row mb-20">
<div class="col span-6">
<LabeledInput
v-model="uiPLSetting.value"
:label="t('branding.uiPL.label')"
:mode="mode"
:maxlength="100"
/>
</div>
</div>
<h3 class="mt-20 mb-5 pb-5">
{{ t('branding.logos.label') }}
</h3>
<label class="text-label">
{{ t('branding.logos.tip', {}, true) }}
</label>
<div class="row mt-10 mb-20">
<Checkbox
v-model="customizeLogo"
:label="t('branding.logos.useCustom')"
:mode="mode"
/>
</div>
<div
v-if="customizeLogo"
class="row mb-20"
>
<div class="col logo-container span-6">
<div class="mb-10">
<FileSelector
:byte-limit="20000"
:read-as-data-url="true"
class="role-secondary"
:label="t('branding.logos.uploadLight')"
:mode="mode"
@error="setError"
@selected="updateLogo($event, 'uiLogoLight')"
/>
</div>
<SimpleBox
v-if="uiLogoLight || uiLogoDark"
class="theme-light mb-10"
>
<label class="text-muted">{{ t('branding.logos.lightPreview') }}</label>
<img
class="logo-preview"
:src="uiLogoLight ? uiLogoLight : uiLogoDark"
>
</SimpleBox>
</div>
<div class="col logo-container span-6">
<div class="mb-10">
<FileSelector
:byte-limit="20000"
:read-as-data-url="true"
class="role-secondary"
:label="t('branding.logos.uploadDark')"
:mode="mode"
@error="setError"
@selected="updateLogo($event, 'uiLogoDark')"
/>
</div>
<SimpleBox
v-if="uiLogoDark || uiLogoLight"
class="theme-dark mb-10"
>
<label class="text-muted">{{ t('branding.logos.darkPreview') }}</label>
<img
class="logo-preview"
:src="uiLogoDark ? uiLogoDark : uiLogoLight"
>
</SimpleBox>
</div>
</div>
<h3 class="mt-20 mb-5 pb-5">
{{ t('branding.favicon.label') }}
</h3>
<label class="text-label">
{{ t('branding.favicon.tip', {}, true) }}
</label>
<div class="row mt-10 mb-20">
<Checkbox
v-model="customizeFavicon"
:label="t('branding.favicon.useCustom')"
:mode="mode"
/>
</div>
<div
v-if="customizeFavicon"
class="row mb-20"
>
<div class="col logo-container span-12">
<div class="mb-10">
<FileSelector
:byte-limit="20000"
:read-as-data-url="true"
class="role-secondary"
:label="t('branding.favicon.upload')"
:mode="mode"
@error="setError"
@selected="updateLogo($event, 'uiFavicon')"
/>
</div>
<SimpleBox v-if="uiFavicon">
<label class="text-muted">{{ t('branding.favicon.preview') }}</label>
<img
class="logo-preview"
:src="uiFavicon"
>
</SimpleBox>
</div>
</div>
<h3 class="mt-40 mb-5 pb-0">
{{ t('branding.color.label') }}
</h3>
<label class="text-label">
{{ t('branding.color.tip', {}, true) }}
</label>
<div class="row mt-20">
<Checkbox
v-model="customizeColor"
:label="t('branding.color.useCustom')"
:mode="mode"
/>
</div>
<div
v-if="customizeColor"
class="row mt-20 mb-20"
>
<ColorInput
v-model="uiColor"
component-testid="primary"
/>
</div>
<h3 class="mt-40 mb-5 pb-0">
{{ t('branding.linkColor.label') }}
</h3>
<label class="text-label">
{{ t('branding.linkColor.tip', {}, true) }}
</label>
<div class="row mt-20">
<Checkbox
v-model="customizeLinkColor"
:label="t('branding.linkColor.useCustom')"
:mode="mode"
/>
</div>
<div
v-if="customizeLinkColor"
class="row mt-20 mb-20"
>
<ColorInput
v-model="uiLinkColor"
class="col"
component-testid="link"
/>
<span class="col link-example">
<a>
{{ t('branding.linkColor.example') }}
</a>
</span>
</div>
</div>
<template v-for="err in errors">
<Banner
:key="err"
color="error"
:label="err"
/>
</template>
<div v-if="mode === 'edit'">
<AsyncButton
component-testid="branding-apply"
class="pull-right mt-20"
mode="apply"
@click="save"
/>
</div>
</div>
</template>
<style scoped lang='scss'>
.link-example {
display: flex;
align-content: center;
a {
margin: auto;
}
}
.logo-container {
display: flex;
flex-direction: column;
::v-deep.simple-box {
position: relative;
flex: 1;
max-height: 120px;
.content {
height: 100%;
display: flex;
}
.logo-preview {
max-width: 100%;
}
}
& LABEL {
position: absolute;
top: 10px;
left: 10px;
}
}
</style>
@@ -0,0 +1,53 @@
<script>
import { HCI } from '../../../../../types';
import SerialConsole from '../../../../../components/SerialConsole';
import Loading from '@shell/components/Loading';
export default {
components: { SerialConsole, Loading },
async fetch() {
this.rows = await this.$store.dispatch('harvester/findAll', { type: HCI.VMI });
},
data() {
return { uid: this.$route.params.uid };
},
computed: {
vmi() {
const vmiList = this.$store.getters['harvester/all'](HCI.VMI) || [];
const vmi = vmiList.find( (VMI) => {
return VMI?.metadata?.ownerReferences?.[0]?.uid === this.uid;
});
return vmi;
},
},
mounted() {
window.addEventListener('beforeunload', () => {
this.$refs.serialConsole.close();
});
},
head() {
return { title: this.vmi?.metadata?.name };
},
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<SerialConsole
v-else
ref="serialConsole"
v-model="vmi"
/>
</template>
<style lang="scss" scoped>
body, #__nuxt, #__layout, main {
height: 100%;
}
</style>
@@ -0,0 +1,55 @@
<script>
import { HCI } from '../../../../../types';
import NovncConsoleWrapper from '../../../../../components/novnc/NovncConsoleWrapper.vue';
import Loading from '@shell/components/Loading';
export default {
components: { NovncConsoleWrapper, Loading },
async fetch() {
this.rows = await this.$store.dispatch('harvester/findAll', { type: HCI.VMI });
},
data() {
return { uid: this.$route.params.uid };
},
computed: {
vmi() {
const vmiList = this.$store.getters['harvester/all'](HCI.VMI) || [];
const vmi = vmiList.find( (VMI) => {
return VMI?.metadata?.ownerReferences?.[0]?.uid === this.uid;
});
return vmi;
},
},
mounted() {
window.addEventListener('beforeunload', () => {
this.$refs.console.close();
});
},
head() {
return { title: this.vmi?.metadata?.name };
},
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<NovncConsoleWrapper
v-else
ref="console"
v-model="vmi"
class="novnc-wrapper"
/>
</template>
<style>
HTML, BODY, MAIN, #__nuxt, #__layout, #app, .vm-console, .vm-console > DIV, .vm-console > DIV > DIV {
height: 100%;
}
</style>
+23
View File
@@ -0,0 +1,23 @@
<script>
import { PRODUCT_NAME } from '../../../config/harvester';
import { HCI } from '../../../types';
export default {
middleware({ redirect, route }) {
return redirect({
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: {
...route.params,
product: PRODUCT_NAME,
resource: HCI.DASHBOARD
}
});
}
};
</script>
<template>
<div>
This is harvester Dashboard.
</div>
</template>
@@ -0,0 +1,22 @@
<script>
import ExplorerMembers from '@shell/components/ExplorerMembers';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../../config/harvester';
import { MANAGEMENT } from '@shell/config/types';
export default {
components: { ExplorerMembers },
computed: {
createLocationOverride() {
return {
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`,
params: {
product: HARVESTER_PRODUCT,
cluster: this.$store.getters['currentCluster']?.id,
resource: MANAGEMENT.CLUSTER_ROLE_TEMPLATE_BINDING
}
};
}
}
};
</script>
<template><ExplorerMembers :create-location-override="createLocationOverride" v-bind="$attrs" /></template>
@@ -0,0 +1,35 @@
<script lang="ts">
import Vue from 'vue';
import { Location } from 'vue-router';
import ExplorerProjectsNamespaces from '@shell/components/ExplorerProjectsNamespaces.vue';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../config/harvester';
import { MANAGEMENT, NAMESPACE } from '@shell/config/types';
interface Data {
createProjectLocation: Location,
createNamespaceLocation: Location
}
export default Vue.extend<Data, any, any, any>({
components: { ExplorerProjectsNamespaces },
data() {
return {
createProjectLocation: {
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`,
params: {
product: HARVESTER_PRODUCT,
cluster: this.$store.getters['currentCluster']?.id,
resource: MANAGEMENT.PROJECT
},
},
createNamespaceLocation: {
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`,
params: {
product: HARVESTER_PRODUCT,
resource: NAMESPACE
},
}
};
}
});
</script>
<template><ExplorerProjectsNamespaces v-bind="$attrs" :create-project-location-override="createProjectLocation" :create-namespace-location-override="createNamespaceLocation" /></template>
@@ -0,0 +1,251 @@
<script>
import { mapGetters } from 'vuex';
import { PLUGIN_DEVELOPER, DEV } from '@shell/store/prefs';
import BannerGraphic from '@shell/components/BannerGraphic';
import IndentedPanel from '@shell/components/IndentedPanel';
import HarvesterSupportBundle from '../../../../dialog/HarvesterSupportBundle';
import CommunityLinks from '@shell/components/CommunityLinks';
import { SCHEMA } from '@shell/config/types';
import { HCI } from '../../../../types';
export default {
components: {
BannerGraphic,
IndentedPanel,
CommunityLinks,
HarvesterSupportBundle
},
data() {
return {
options: {
'footer.docs': 'https://docs.harvesterhci.io',
'footer.forums': 'https://forums.rancher.com/c/harvester/',
'footer.slack': 'https://slack.rancher.io',
'footer.issue': 'https://github.com/harvester/harvester/issues/new/choose'
}
};
},
computed: {
...mapGetters(['currentCluster']),
dev() {
try {
return this.$store.getters['prefs/get'](PLUGIN_DEVELOPER);
} catch {
return this.$store.getters['prefs/get'](DEV);
}
},
title() {
return 'harvester.support.title';
},
showSupportBundle() {
const inStore = this.$store.getters['currentProduct'].inStore;
return !!this.$store.getters[`${ inStore }/byId`](
SCHEMA,
HCI.SUPPORT_BUNDLE
);
},
internalPrefix() {
const host = window.location.host;
const prefix = window.location.pathname.replace(this.$route.path, '');
const params = this.$route?.params;
return {
host, prefix, params
};
},
rancherLink() {
const { host, prefix, params } = this.internalPrefix;
return `https://${ host }${ prefix }/c/${ params.cluster }/explorer`;
},
longhornLink() {
const { host, params } = this.internalPrefix;
return `https://${ host }/k8s/clusters/${ params.cluster }/api/v1/namespaces/longhorn-system/services/http:longhorn-frontend:80/proxy/#/dashboard`;
}
},
methods: {
open() {
this.$store.commit('harvester-common/toggleBundleModal', true);
}
}
};
</script>
<template>
<div>
<BannerGraphic :title="t(title, {}, true)" />
<IndentedPanel>
<div class="content mt-20">
<div class="promo">
<div
v-if="showSupportBundle"
class="box mb-20 box-primary"
>
<h2>
{{ t('harvester.modal.bundle.title') }}
</h2>
<div>
<p class="pb-10">
{{ t('harvester.modal.bundle.titleDescription') }}
</p>
<button
class="btn role-secondary btn-sm"
type="button"
@click="open"
>
{{ t('harvester.modal.bundle.title') }}
</button>
</div>
</div>
<div
class="box box-primary"
:class="{'mb-20': dev }"
>
<h2>
{{ t('harvester.support.kubeconfig.title') }}
</h2>
<div>
<p class="pb-10">
{{ t('harvester.support.kubeconfig.titleDescription') }}
</p>
<button
class="btn role-secondary btn-sm"
type="button"
@click="currentCluster.downloadKubeConfig()"
>
{{ t('harvester.support.kubeconfig.title') }}
</button>
</div>
</div>
<div
v-if="dev"
class="row"
>
<div class="col span-6 box box-primary">
<h2>
<a
rel="nofollow noopener noreferrer"
target="_blank"
:href="rancherLink"
>{{ t('harvester.support.internal.rancher.title') }} <i class="icon icon-external-link" /></a>
</h2>
<div>
<p class="warning">
<t
k="harvester.support.internal.rancher.titleDescription"
:raw="true"
/>
</p>
</div>
</div>
<div class="col span-6 box box-primary">
<h2>
<a
rel="nofollow noopener noreferrer"
target="_blank"
:href="longhornLink"
>{{ t('harvester.support.internal.longhorn.title') }} <i class="icon icon-external-link" /></a>
</h2>
<div>
<p class="warning">
<t
k="harvester.support.internal.longhorn.titleDescription"
:raw="true"
/>
</p>
</div>
</div>
</div>
</div>
<div class="community">
<CommunityLinks :link-options="options" />
</div>
<div class="external">
<a
href="https://www.suse.com/suse-harvester/support-matrix/all-supported-versions"
target="_blank"
rel="noopener noreferrer nofollow"
>{{ t('harvester.support.community.learnMore') }} <i class="icon icon-external-link" /></a>
or
<a
href="https://www.suse.com/products/harvester/"
target="_blank"
rel="noopener noreferrer nofollow"
>{{ t('harvester.support.community.pricing') }} <i class="icon icon-external-link" /></a>
</div>
</div>
</IndentedPanel>
<HarvesterSupportBundle v-if="showSupportBundle" />
</div>
</template>
<style lang="scss" scoped>
.content {
display: grid;
grid-column-gap: 20px;
grid-row-gap: 20px;
grid-template-columns: 70% 30%;
}
.only-community {
display: grid;
grid-template-columns: 100%;
}
.community {
border-left: 1px solid var(--border);
padding-left: 20px;
> h2 {
font-size: 18px;
font-weight: 300;
margin-bottom: 20px;
}
.support-link {
margin: 10px 0;
}
}
.box {
padding: 20px;
border: 1px solid var(--border);
&.box-primary {
border-color: var(--primary);
}
> h2 {
font-size: 20px;
font-weight: 300;
}
> div {
font-weight: 300;
line-height: 18px;
opacity: 0.8;
}
}
.role-secondary {
&:focus {
background-color: transparent;
}
}
.warning {
margin: 0 -5px 0 -5px;
padding: 5px;
background-color: var(--warning-banner-bg);
}
</style>
-8
View File
@@ -1,8 +0,0 @@
<script>
export default { name: 'HarvesterDashboard' };
</script>
<template>
<span>Hello harvester!</span>
</template>