mirror of https://github.com/rancher/dashboard.git
Merge pull request #968 from mantis-toboggan-md/workload-no-wizard
Update workload create flow
This commit is contained in:
commit
e4e07ef890
|
|
@ -176,13 +176,14 @@ containerResourceLimit:
|
|||
requestsMemory: Memory Reservation
|
||||
|
||||
cruResource:
|
||||
previewYaml: Preview Yaml
|
||||
cancel: Cancelling will destroy your changes.
|
||||
back: Going back will destroy your changes.
|
||||
confirmYaml: "No, Review Yaml"
|
||||
backToForm: Go Back To Form
|
||||
confirmCancel: "Yes, Cancel"
|
||||
cancel: Cancelling will destroy your changes.
|
||||
confirmBack: "Yes, Go Back"
|
||||
confirmCancel: "Yes, Cancel"
|
||||
reviewForm: "No, Review Form"
|
||||
reviewYaml: "No, Review Yaml"
|
||||
previewYaml: Preview Yaml
|
||||
|
||||
footer:
|
||||
docs: Docs
|
||||
|
|
@ -780,6 +781,7 @@ workload:
|
|||
sysctls: Sysctls
|
||||
sysctlsKey: Name
|
||||
titles:
|
||||
container: Define Container
|
||||
command: Command
|
||||
containers: Containers
|
||||
env: Environment Variables
|
||||
|
|
@ -797,6 +799,7 @@ workload:
|
|||
podIP: Pod IP
|
||||
podRestarts: Pod Restarts
|
||||
workload: Workload
|
||||
hideTabs: 'Hide Advanced Options'
|
||||
job:
|
||||
activeDeadlineSeconds:
|
||||
label: Active Deadline
|
||||
|
|
@ -855,6 +858,7 @@ workload:
|
|||
label: Subdomain
|
||||
placeholder: e.g. web
|
||||
replicas: Replicas
|
||||
showTabs: 'Show Advanced Options'
|
||||
scheduling:
|
||||
activeDeadlineSeconds: Pod Active Deadline
|
||||
activeDeadlineSecondsTip: The duration that the pod may be active before the system tries to mark it failed and kill associated containers.
|
||||
|
|
@ -914,6 +918,14 @@ workload:
|
|||
tolerationSeconds: Toleration Seconds
|
||||
value: Value
|
||||
serviceName: Service Name
|
||||
storage:
|
||||
title: 'Storage'
|
||||
typeDescriptions:
|
||||
apps.daemonset: <a href="https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/" target="_blank" rel="noopener nofollow" >DaemonSets</a> run exactly one pod on every eligible node. When new nodes are added to the cluster, DaemonSets automatically deploy to them. Recommended for system-wide or vertically-scalable workloads that never need more than one pod per node.
|
||||
apps.deployment: '<a href="https://kubernetes.io/docs/concepts/workloads/controllers/deployment/" target="_blank" rel="noopener nofollow" >Deployments</a> run a scalable number of replicas of a pod distributed among the eligible nodes. Changes are rolled out incrementally and can be rolled back to the previous revision when needed. Recommended for stateless & horizontally-scalable workloads.'
|
||||
apps.statefulset: <a href="https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/" target="_blank" rel="noopener nofollow" >StatefulSets</a> manage stateful applications and provide guarantees about the ordering and uniqueness of the pods created. Recommended for workloads with persistent storage or strict identity, quorum, or upgrade order requirements.
|
||||
batch.cronjob: <a href="https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/" target="_blank" rel="noopener nofollow" >CronJobs</a> create Jobs, which then run Pods, on a repeating schedule. The schedule is expressed in standard Unix <a href="https://en.wikipedia.org/wiki/Cron" target="_blank" rel="noopener nofollow">cron</a> format, and uses the timezone of the Kubernetes control plane (typically UTC).
|
||||
batch.job: <a href="https://kubernetes.io/docs/concepts/workloads/controllers/job/" target="_blank" rel="noopener nofollow" >Jobs</a> create one or more pods to reliably perform a one-time task by running a pod until it exits successfully. Failed pods are automatically replaced until the specified number of completed runs has been reached. Jobs can also run multiple pods in parallel or function as a batch work queue.
|
||||
upgrading:
|
||||
activeDeadlineSeconds:
|
||||
label: Pod Active Deadline
|
||||
|
|
@ -944,27 +956,7 @@ workload:
|
|||
label: Termination Grace Period
|
||||
tip: The duration the pod needs to terminate successfully.
|
||||
title: upgrading
|
||||
wizard:
|
||||
descriptions:
|
||||
apps.daemonset: <a href="https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/" target="_blank" rel="noopener nofollow" >DaemonSets</a> run exactly one pod on every eligible node. When new nodes are added to the cluster, DaemonSets automatically deploy to them. Recommended for system-wide or vertically-scalable workloads that never need more than one pod per node.
|
||||
apps.deployment: '<a href="https://kubernetes.io/docs/concepts/workloads/controllers/deployment/" target="_blank" rel="noopener nofollow" >Deployments</a> run a scalable number of replicas of a pod distributed among the eligible nodes. Changes are rolled out incrementally and can be rolled back to the previous revision when needed. Recommended for stateless & horizontally-scalable workloads.'
|
||||
apps.statefulset: <a href="https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/" target="_blank" rel="noopener nofollow" >StatefulSets</a> manage stateful applications and provide guarantees about the ordering and uniqueness of the pods created. Recommended for workloads with persistent storage or strict identity, quorum, or upgrade order requirements.
|
||||
batch.cronjob: <a href="https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/" target="_blank" rel="noopener nofollow" >CronJobs</a> create Jobs, which then run Pods, on a repeating schedule. The schedule is expressed in standard Unix <a href="https://en.wikipedia.org/wiki/Cron" target="_blank" rel="noopener nofollow">cron</a> format, and uses the timezone of the Kubernetes control plane (typically UTC).
|
||||
batch.job: <a href="https://kubernetes.io/docs/concepts/workloads/controllers/job/" target="_blank" rel="noopener nofollow" >Jobs</a> create one or more pods to reliably perform a one-time task by running a pod until it exits successfully. Failed pods are automatically replaced until the specified number of completed runs has been reached. Jobs can also run multiple pods in parallel or function as a batch work queue.
|
||||
titles:
|
||||
advanced:
|
||||
title: Advanced
|
||||
subtitle: Define the advanced options. Each pod has additional options that can be defined as part of the container, part of the pod and part of the workload type. We recommends reading the detailed Kubernetes documentation before attempting to configure these.
|
||||
defineContainer:
|
||||
title: Define Container
|
||||
subtitle: Define the container of the pod. Each workload type ultimately deploys pods, which are the smallest unit of compute managed by Kubernetes. The main definition of the pod is the container. These are the basic fields defined on a container, but there are more fields that can be defined in the Advanced step or in the raw yaml.
|
||||
scaling: Scaling/Upgrade Policy
|
||||
selectType:
|
||||
title: Select a Workload Type
|
||||
subtitle: Select the type of <a href="https://kubernetes.io/docs/concepts/workloads/" target="_blank" rel="noopener nofollow">workload</a> to create. Each type has unique properties that define where the pods will be run, how they will be maintained or scaled, and what happens when they fail.
|
||||
storage:
|
||||
title: Storage
|
||||
subtitle: Pods only have their own local ephemeral storage by default, which will be wiped out when the pod dies. To persist data that survives beyond the life of a single pod, you can attach external <a href='https://kubernetes.io/docs/concepts/storage/' target="_blank" rel="noopener nofollow">storage</a> (e.g. block devices or NFS shares) from a provider and make it available to the pod.
|
||||
|
||||
|
||||
##############################
|
||||
# Model Properties
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { SCHEMA } from '@/config/types';
|
|||
import ResourceYaml from '@/components/ResourceYaml';
|
||||
import Banner from '@/components/Banner';
|
||||
import AsyncButton from '@/components/AsyncButton';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
@ -43,6 +44,11 @@ export default {
|
|||
validationPassed: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
|
||||
errors: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -51,7 +57,6 @@ export default {
|
|||
isCancelModal: false,
|
||||
showAsForm: true,
|
||||
resourceYaml: '',
|
||||
errors: []
|
||||
};
|
||||
},
|
||||
|
||||
|
|
@ -68,7 +73,8 @@ export default {
|
|||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
},
|
||||
...mapGetters({ t: 'i18n/t' })
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
|
@ -110,19 +116,6 @@ export default {
|
|||
this.showAsForm = false;
|
||||
},
|
||||
|
||||
yamlCb(success, errors) {
|
||||
success ? this.done() : (this.errors = errors);
|
||||
},
|
||||
|
||||
done() {
|
||||
if (!this.doneRoute) {
|
||||
return;
|
||||
}
|
||||
this.$router.replace({
|
||||
name: this.doneRoute,
|
||||
params: { resource: this.resource.type }
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
@ -137,7 +130,7 @@ export default {
|
|||
:key="subtype.id"
|
||||
class="subtype-banner"
|
||||
:class="{ selected: subtype.id === selectedSubtype }"
|
||||
@click="$emit('selectType', subtype.id)"
|
||||
@click="$emit('select-type', subtype.id)"
|
||||
>
|
||||
<slot name="subtype-logo">
|
||||
<div class="subtype-logo round-image">
|
||||
|
|
@ -150,7 +143,7 @@ export default {
|
|||
<span
|
||||
v-if="$store.getters['i18n/exists'](subtype.bannerAbbrv)"
|
||||
>{{ t(subtype.bannerAbbrv) }}</span>
|
||||
<span v-else>{{ subtype.bannerAbbrv.slice(0, 1).toUpperCase() }}</span>
|
||||
<span v-else>{{ subtype.bannerAbbrv }}</span>
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ subtype.id.slice(0, 1).toUpperCase() }}
|
||||
|
|
@ -171,7 +164,7 @@ export default {
|
|||
<div class="description">
|
||||
<span
|
||||
v-if="$store.getters['i18n/exists'](subtype.description)"
|
||||
v-html="t(subtype.description)"
|
||||
v-html="t(subtype.description, {}, true)"
|
||||
></span>
|
||||
<span v-else>{{ subtype.description }}</span>
|
||||
</div>
|
||||
|
|
@ -186,7 +179,7 @@ export default {
|
|||
</div>
|
||||
<div class="controls-row">
|
||||
<slot name="form-footer">
|
||||
<button type="button" class="btn role-secondary" @click="$emit('cancel')">
|
||||
<button type="button" class="btn role-secondary" @click="checkCancel(true)">
|
||||
<t k="generic.cancel" />
|
||||
</button>
|
||||
<div>
|
||||
|
|
@ -202,7 +195,7 @@ export default {
|
|||
v-if="!showSubtypeSelection"
|
||||
:disabled="!validationPassed"
|
||||
:action-label="mode==='edit' ? t('generic.save') : t('generic.create')"
|
||||
@click="$emit('finish', done)"
|
||||
@click="cb=>$emit('finish', cb)"
|
||||
/>
|
||||
</div>
|
||||
</slot>
|
||||
|
|
@ -218,8 +211,9 @@ export default {
|
|||
:offer-preview="mode==='edit'"
|
||||
:done-route="doneRoute"
|
||||
:done-override="resource.doneOverride"
|
||||
@error="e=>$emit('error', e)"
|
||||
>
|
||||
<template #yamlFooter="{currentYaml, showPreview, yamlSave, yamlPreview, yamlUnpreview}">
|
||||
<template #yamlFooter="{currentYaml, yamlSave, showPreview, yamlPreview, yamlUnpreview}">
|
||||
<div class="controls-row">
|
||||
<slot name="cru-yaml-footer">
|
||||
<div class="controls-right">
|
||||
|
|
@ -237,6 +231,7 @@ export default {
|
|||
<t k="resourceYaml.buttons.continue" />
|
||||
</button>
|
||||
<button
|
||||
v-if="!showPreview"
|
||||
:disabled="resourceYaml === currentYaml"
|
||||
type="button"
|
||||
class="btn role-secondary"
|
||||
|
|
@ -249,9 +244,12 @@ export default {
|
|||
<button type="button" class="btn role-secondary" @click="checkCancel(false)">
|
||||
<t k="generic.back" />
|
||||
</button>
|
||||
<button type="button" class="btn role-primary" @click="yamlSave(yamlCb)">
|
||||
<t k="generic.create" />
|
||||
</button>
|
||||
<AsyncButton
|
||||
v-if="!showSubtypeSelection"
|
||||
:disabled="!validationPassed"
|
||||
:action-label="mode==='edit' ? t('generic.save') : t('generic.create')"
|
||||
@click="cb=>yamlSave(cb)"
|
||||
/>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
|
|
@ -268,7 +266,9 @@ export default {
|
|||
<div class="header">
|
||||
<h4 class="text-default-text">
|
||||
<t v-if="isCancelModal" k="generic.cancel" />
|
||||
<t v-else k="cruResource.backToForm" />
|
||||
<span v-else>
|
||||
{{ t("cruResource.backToForm") }}
|
||||
</span>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="body">
|
||||
|
|
@ -281,14 +281,14 @@ export default {
|
|||
</div>
|
||||
<div class="footer">
|
||||
<button type="button" class="btn role-secondary" @click="$modal.hide('cancel-modal')">
|
||||
<t k="cruResource.confirmYaml" />
|
||||
{{ showAsForm ? t("cruResource.reviewForm") : t("cruResource.reviewYaml") }}
|
||||
</button>
|
||||
<button type="button" class="btn role-primary" @click="confirmCancel(isCancelModal)">
|
||||
<span v-if="isCancelModal">
|
||||
<t k="cruResource.confirmCancel" />
|
||||
{{ t("cruResource.confirmCancel") }}
|
||||
</span>
|
||||
<span v-else>
|
||||
<t k="cruResource.confirmBack" />
|
||||
{{ t("cruResource.confirmBack") }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import { SCHEMA } from '@/config/types';
|
|||
import { createYaml } from '@/utils/create-yaml';
|
||||
import Masthead from '@/components/ResourceDetail/Masthead';
|
||||
import DetailTop from '@/components/DetailTop';
|
||||
import isFunction from 'lodash/isFunction';
|
||||
import GenericResourceDetail from './Generic';
|
||||
|
||||
// Components can't have asyncData, only pages.
|
||||
|
|
@ -53,7 +52,7 @@ function realModeFor(query, id) {
|
|||
|
||||
// You can pass in a resource from a edit/someType.vue to use this but with a different type
|
||||
// e.g. for workload to create a deployment
|
||||
export async function defaultAsyncData(ctx, resource) {
|
||||
export async function defaultAsyncData(ctx, resource, parentOverride) {
|
||||
const { store, params, route } = ctx;
|
||||
|
||||
// eslint-disable-next-line prefer-const
|
||||
|
|
@ -132,6 +131,7 @@ export async function defaultAsyncData(ctx, resource) {
|
|||
* Important: these need to be declared below as props too if you want to use them
|
||||
*******/
|
||||
const out = {
|
||||
parentOverride,
|
||||
hasCustomDetail,
|
||||
hasCustomEdit,
|
||||
resource,
|
||||
|
|
@ -253,14 +253,6 @@ export default {
|
|||
|
||||
return null;
|
||||
},
|
||||
|
||||
showMasthead() {
|
||||
if (isFunction(this.currentValue.showMasthead)) {
|
||||
return this.currentValue.showMasthead(this.mode);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
|
@ -285,7 +277,6 @@ export default {
|
|||
<template>
|
||||
<div>
|
||||
<Masthead
|
||||
v-if="showMasthead"
|
||||
:value="originalModel"
|
||||
:mode="mode"
|
||||
:real-mode="realMode"
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
_UNFLAG,
|
||||
_EDIT,
|
||||
} from '@/config/query-params';
|
||||
import { exceptionToErrorsArray } from '../utils/error';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
@ -263,7 +264,9 @@ export default {
|
|||
} else {
|
||||
this.errors = [err];
|
||||
}
|
||||
buttonDone(false, this.errors);
|
||||
buttonDone(false);
|
||||
|
||||
this.$emit('error', exceptionToErrorsArray(err));
|
||||
}
|
||||
},
|
||||
done() {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ export default {
|
|||
type: Number,
|
||||
default: 0,
|
||||
required: false,
|
||||
},
|
||||
canToggle: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -11,22 +11,52 @@ export default {
|
|||
type: String,
|
||||
default: null,
|
||||
},
|
||||
|
||||
sideTabs: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
// tabs with canToggle will be hidden by default and revealed by clicking this link
|
||||
showMoreLabel: {
|
||||
type: String,
|
||||
default: 'Show Extra'
|
||||
},
|
||||
|
||||
hideMoreLabel: {
|
||||
type: String,
|
||||
default: 'Hide Extra'
|
||||
},
|
||||
|
||||
// whether or not to scroll to the top of the new tab on tab change. This is particularly ugly with side tabs
|
||||
scrollOnChange: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return { tabs: [] };
|
||||
return { tabs: [], showHiddenTabs: false };
|
||||
},
|
||||
|
||||
computed: {
|
||||
sortedTabs() {
|
||||
// keep the tabs list ordered for dynamic tabs
|
||||
// keep the tabs list ordered for dynamic tabs
|
||||
sortedShownTabs() {
|
||||
const { tabs } = this;
|
||||
const shownTabs = tabs.filter(tab => !tab.canToggle);
|
||||
|
||||
return sortBy(tabs, ['weight', 'label', 'name']);
|
||||
return sortBy(shownTabs, ['weight', 'label', 'name']);
|
||||
},
|
||||
|
||||
sortedHiddenTabs() {
|
||||
const { tabs } = this;
|
||||
const hiddenTabs = tabs.filter(tab => tab.canToggle);
|
||||
|
||||
return sortBy(hiddenTabs, ['weight', 'label', 'name']);
|
||||
},
|
||||
|
||||
sortedTabs() {
|
||||
return [...this.sortedShownTabs, ...this.sortedHiddenTabs];
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -37,6 +67,11 @@ export default {
|
|||
$route: { hash }
|
||||
} = this;
|
||||
const activeTab = tabs.find(t => t.active);
|
||||
|
||||
if (activeTab && activeTab.canToggle) {
|
||||
this.showHiddenTabs = true;
|
||||
}
|
||||
|
||||
const windowHash = hash.slice(1);
|
||||
const windowHashTabMatch = tabs.find(t => t.name === windowHash && !t.active);
|
||||
const firstTab = head(tabs) || null;
|
||||
|
|
@ -95,6 +130,13 @@ export default {
|
|||
|
||||
methods: {
|
||||
hashChange() {
|
||||
if (!this.scrollOnChange) {
|
||||
const scrollable = document.getElementsByTagName('main')[0];
|
||||
|
||||
if (scrollable) {
|
||||
scrollable.scrollTop = 0;
|
||||
}
|
||||
}
|
||||
this.select(this.$route.hash);
|
||||
},
|
||||
|
||||
|
|
@ -115,6 +157,10 @@ export default {
|
|||
return;
|
||||
}
|
||||
|
||||
if (selected.canToggle) {
|
||||
this.showHiddenTabs = true;
|
||||
}
|
||||
|
||||
if (routeHash !== hashName) {
|
||||
window.location.hash = `#${ name }`;
|
||||
}
|
||||
|
|
@ -166,7 +212,7 @@ export default {
|
|||
@keydown.left.prevent="selectNext(-1)"
|
||||
>
|
||||
<li
|
||||
v-for="tab in sortedTabs"
|
||||
v-for="tab in sortedShownTabs"
|
||||
:id="tab.name"
|
||||
:key="tab.name"
|
||||
:class="{tab: true, active: tab.active, disabled: tab.disabled}"
|
||||
|
|
@ -181,6 +227,31 @@ export default {
|
|||
{{ tab.label }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="tab toggle">
|
||||
<a @click.prevent="showHiddenTabs = !showHiddenTabs">
|
||||
<i class="icon icon-sm" :class="{'icon-plus': !showHiddenTabs, 'icon-minus':showHiddenTabs}" />
|
||||
{{ showHiddenTabs ? hideMoreLabel : showMoreLabel }}
|
||||
</a>
|
||||
</li>
|
||||
<template v-if="showHiddenTabs">
|
||||
<li
|
||||
v-for="tab in sortedHiddenTabs"
|
||||
:id="tab.name"
|
||||
:key="tab.name"
|
||||
class="can-toggle"
|
||||
:class="{tab: true, active: tab.active, disabled: tab.disabled}"
|
||||
role="presentation"
|
||||
>
|
||||
<a
|
||||
:aria-controls="'#' + tab.name"
|
||||
:aria-selected="tab.active"
|
||||
role="tab"
|
||||
@click.prevent="select(tab.name, $event)"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
<div class="tab-container">
|
||||
<slot />
|
||||
|
|
@ -243,6 +314,14 @@ export default {
|
|||
& .tab {
|
||||
width: 100%;
|
||||
|
||||
&.toggle A {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
&.can-toggle {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
A {
|
||||
color: var(--input-label);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -165,9 +165,11 @@ export default {
|
|||
:selected-subtype="serviceType"
|
||||
:subtypes="defaultServiceTypes"
|
||||
:validation-passed="true"
|
||||
:errors="errors"
|
||||
@error="e=>errors = e"
|
||||
@finish="save"
|
||||
@cancel="done"
|
||||
@selectType="(st) => serviceType = st"
|
||||
@select-type="(st) => serviceType = st"
|
||||
>
|
||||
<template #define>
|
||||
<NameNsDescription v-if="!isView" :value="value" :mode="mode" />
|
||||
|
|
|
|||
|
|
@ -18,24 +18,22 @@ import Upgrading from '@/edit/workload/Upgrading';
|
|||
import Networking from '@/components/form/Networking';
|
||||
import Job from '@/edit/workload/Job';
|
||||
import { defaultAsyncData } from '@/components/ResourceDetail';
|
||||
import { _EDIT, _CREATE } from '@/config/query-params';
|
||||
import { _EDIT } from '@/config/query-params';
|
||||
import WorkloadPorts from '@/components/form/WorkloadPorts';
|
||||
import ContainerResourceLimit from '@/components/ContainerResourceLimit';
|
||||
import Wizard from '@/components/Wizard';
|
||||
import KeyValue from '@/components/form/KeyValue';
|
||||
import Tabbed from '@/components/Tabbed';
|
||||
import { mapGetters } from 'vuex';
|
||||
import ButtonDropdown from '@/components/ButtonDropdown';
|
||||
import ShellInput from '@/components/form/ShellInput';
|
||||
import Checkbox from '@/components/form/Checkbox';
|
||||
import NodeScheduling from '@/components/form/NodeScheduling';
|
||||
import PodScheduling from '@/components/form/PodScheduling';
|
||||
import Tolerations from '@/components/form/Tolerations';
|
||||
import CruResource from '@/components/CruResource';
|
||||
|
||||
export default {
|
||||
name: 'CruWorkload',
|
||||
components: {
|
||||
Wizard,
|
||||
NameNsDescription,
|
||||
LabeledSelect,
|
||||
LabeledInput,
|
||||
|
|
@ -52,10 +50,10 @@ export default {
|
|||
Security,
|
||||
WorkloadPorts,
|
||||
ContainerResourceLimit,
|
||||
ButtonDropdown,
|
||||
PodScheduling,
|
||||
NodeScheduling,
|
||||
Tolerations
|
||||
Tolerations,
|
||||
CruResource
|
||||
},
|
||||
|
||||
mixins: [CreateEditView],
|
||||
|
|
@ -88,23 +86,34 @@ export default {
|
|||
|
||||
asyncData(ctx) {
|
||||
let resource;
|
||||
let parentOverride;
|
||||
|
||||
if ( ctx.params.resource === 'workload') {
|
||||
parentOverride = {
|
||||
displayName: 'Workload',
|
||||
location: {
|
||||
name: 'c-cluster-product-resource',
|
||||
params: { resource: 'workload' },
|
||||
}
|
||||
};
|
||||
resource = WORKLOAD_TYPES.DEPLOYMENT;
|
||||
}
|
||||
|
||||
return defaultAsyncData(ctx, resource);
|
||||
return defaultAsyncData(ctx, resource, parentOverride);
|
||||
},
|
||||
|
||||
data() {
|
||||
const t = this.$store.getters['i18n/t'];
|
||||
let type = this.value._type || this.value.type;
|
||||
let type = this.$route.params.resource;
|
||||
|
||||
if (type === 'workload') {
|
||||
type = WORKLOAD_TYPES.DEPLOYMENT;
|
||||
type = null;
|
||||
}
|
||||
// track spec separately from resource instance so it can be trashed when the wizard is cancelled
|
||||
const spec = { ...this.value.spec };
|
||||
|
||||
if (!this.value.spec) {
|
||||
this.value.spec = {};
|
||||
}
|
||||
|
||||
const spec = this.value.spec;
|
||||
|
||||
if (!spec.replicas) {
|
||||
spec.replicas = 1;
|
||||
|
|
@ -114,36 +123,7 @@ export default {
|
|||
spec.template = { spec: { restartPolicy: this.isJob ? 'Never' : 'Always' } };
|
||||
}
|
||||
|
||||
const steps = [
|
||||
{
|
||||
name: 'type',
|
||||
label: t('workload.wizard.titles.selectType.title'),
|
||||
subtext: 'workload.wizard.titles.selectType.subtitle',
|
||||
ready: !!type,
|
||||
},
|
||||
{
|
||||
name: 'container',
|
||||
label: t('workload.wizard.titles.defineContainer.title'),
|
||||
subtext: 'workload.wizard.titles.defineContainer.subtitle',
|
||||
ready: false
|
||||
},
|
||||
{
|
||||
name: 'storage',
|
||||
label: t('workload.wizard.titles.storage.title'),
|
||||
subtext: 'workload.wizard.titles.storage.subtitle',
|
||||
ready: true
|
||||
},
|
||||
{
|
||||
name: 'advanced',
|
||||
label: t('workload.wizard.titles.advanced.title'),
|
||||
subtext: 'workload.wizard.titles.advanced.subtitle',
|
||||
ready: true
|
||||
}
|
||||
];
|
||||
|
||||
return {
|
||||
showBannerTitle: this.mode !== _CREATE,
|
||||
steps,
|
||||
spec,
|
||||
type,
|
||||
allConfigMaps: [],
|
||||
|
|
@ -155,19 +135,6 @@ export default {
|
|||
},
|
||||
|
||||
computed: {
|
||||
// start wizard at step 2 if type selection isn't relevant (navigating from type-specific page, or editing/cloning)
|
||||
initStepIndex() {
|
||||
if (this.doneParams?.resource !== 'workload' || this.mode !== _CREATE) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
|
||||
currentStepIndex() {
|
||||
return this.$route.query?.step;
|
||||
},
|
||||
|
||||
isEdit() {
|
||||
return this.mode === _EDIT;
|
||||
},
|
||||
|
|
@ -374,6 +341,25 @@ export default {
|
|||
} );
|
||||
},
|
||||
|
||||
// array of id, label, description, initials for type selection step
|
||||
workloadSubTypes() {
|
||||
const out = [];
|
||||
|
||||
for (const prop in this.workloadTypes) {
|
||||
const type = this.workloadTypes[prop];
|
||||
const subtype = {
|
||||
id: type,
|
||||
description: `workload.typeDescriptions.'${ type }'`,
|
||||
label: this.nameDisplayFor(type),
|
||||
bannerAbbrv: this.initialDisplayFor(type)
|
||||
};
|
||||
|
||||
out.push(subtype);
|
||||
}
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
...mapGetters({ t: 'i18n/t' })
|
||||
},
|
||||
|
||||
|
|
@ -406,17 +392,6 @@ export default {
|
|||
this.$set(this.value, 'type', neu);
|
||||
delete this.value.apiVersion;
|
||||
},
|
||||
|
||||
containerIsReady(neu) {
|
||||
this.steps[1].ready = neu;
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
created() {
|
||||
if (this.containerIsReady) {
|
||||
this.steps[1].ready = true;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
|
@ -430,23 +405,14 @@ export default {
|
|||
// show initials of workload type in blue circles for now
|
||||
initialDisplayFor(type) {
|
||||
const typeDisplay = this.nameDisplayFor(type);
|
||||
const eachWord = typeDisplay.split(' ');
|
||||
|
||||
return eachWord.reduce((total, word) => {
|
||||
total += word[0];
|
||||
|
||||
return total;
|
||||
}, '');
|
||||
return typeDisplay.split('').filter(letter => letter.match(/[A-Z]/)).join('');
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this.done();
|
||||
},
|
||||
|
||||
toggleTabs() {
|
||||
this.showTabs = !this.showTabs;
|
||||
},
|
||||
|
||||
saveWorkload(cb) {
|
||||
if (!this.spec.selector && this.type !== WORKLOAD_TYPES.JOB) {
|
||||
this.spec.selector = { matchLabels: this.workloadSelector };
|
||||
|
|
@ -538,190 +504,152 @@ export default {
|
|||
});
|
||||
|
||||
return podAffinity;
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form>
|
||||
<Wizard
|
||||
v-if="mode"
|
||||
<CruResource
|
||||
:validation-passed="containerIsReady"
|
||||
:selected-subtype="type"
|
||||
:resource="value"
|
||||
:mode="mode"
|
||||
:errors="errors"
|
||||
:finish-mode="mode"
|
||||
:init-step-index="initStepIndex"
|
||||
:steps="steps"
|
||||
:initial-title="false"
|
||||
:banner-title="nameDisplayFor(type)"
|
||||
:banner-image="initialDisplayFor(type)"
|
||||
@finish="saveWorkload"
|
||||
@cancel="cancel"
|
||||
:done-route="doneRoute"
|
||||
:subtypes="workloadSubTypes"
|
||||
@finish="cb=>saveWorkload(cb)"
|
||||
@select-type="e=>type=e"
|
||||
@error="e=>errors = e"
|
||||
>
|
||||
<template v-if="currentStepIndex > 1 || mode !=='create'" #bannerTitleImage>
|
||||
<span class="type-placeholder"> {{ initialDisplayFor(type) }} </span>
|
||||
</template>
|
||||
<template v-if="steps[currentStepIndex-1]" #bannerSubtext>
|
||||
<t :k="steps[currentStepIndex-1].subtext" :raw="true" />
|
||||
</template>
|
||||
<template #type>
|
||||
<div class="types-container">
|
||||
<div
|
||||
v-for="workloadType in workloadTypes"
|
||||
:key="workloadType"
|
||||
class="choice-banner"
|
||||
:class="{ selected: type === workloadType }"
|
||||
@click="type = workloadType"
|
||||
>
|
||||
<div :style="{ 'background-color': 'var(--primary)' }" class="round-image">
|
||||
<span class="type-placeholder">{{ initialDisplayFor(workloadType) }}</span>
|
||||
<template #define>
|
||||
<Tabbed :show-more-label="t('workload.showTabs')" :hide-more-label="t('workload.hideTabs')" :side-tabs="true">
|
||||
<Tab :label="t('workload.container.titles.container')" name="container">
|
||||
<div class="bordered-section">
|
||||
<div class="row">
|
||||
<div class="col span-12">
|
||||
<NameNsDescription :value="value" :mode="mode" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isCronJob || isReplicable" class="row">
|
||||
<div v-if="isCronJob" class="col span-6">
|
||||
<LabeledInput v-model="spec.schedule" :mode="mode" :label="t('workload.cronSchedule')" />
|
||||
<span class="cron-hint text-small">{{ cronLabel }}</span>
|
||||
</div>
|
||||
<div v-if="isReplicable" class="col span-6">
|
||||
<LabeledInput v-model.number="spec.replicas" required :mode="mode" :label="t('workload.replicas')" />
|
||||
</div>
|
||||
<div v-if="isStatefulSet" class="col span-6">
|
||||
<LabeledSelect
|
||||
v-model="spec.serviceName"
|
||||
option-label="metadata.name"
|
||||
:reduce="service=>service.metadata.name"
|
||||
:mode="mode"
|
||||
:label="t('workload.serviceName')"
|
||||
:options="headlessServices"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bordered-section">
|
||||
<h3>{{ t('workload.container.titles.image') }}</h3>
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model="container.image"
|
||||
:label="t('workload.container.image')"
|
||||
placeholder="eg nginx:latest"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabeledSelect
|
||||
v-model="container.imagePullPolicy"
|
||||
:label="t('workload.container.imagePullPolicy')"
|
||||
:options="['Always', 'IfNotPresent', 'Never']"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bordered-section">
|
||||
<h3>{{ t('workload.container.titles.ports') }}</h3>
|
||||
<div class="row">
|
||||
<WorkloadPorts v-model="container.ports" :mode="mode" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bordered-section">
|
||||
<h3>{{ t('workload.container.titles.command') }}</h3>
|
||||
<div class="row">
|
||||
<div class="col span-5">
|
||||
<slot name="entrypoint">
|
||||
<ShellInput
|
||||
v-model="container.command"
|
||||
:mode="mode"
|
||||
:label="t('workload.container.command.command')"
|
||||
placeholder="e.g. /bin/sh"
|
||||
/>
|
||||
</slot>
|
||||
</div>
|
||||
<div class="col span-5">
|
||||
<slot name="command">
|
||||
<ShellInput
|
||||
v-model="container.args"
|
||||
:mode="mode"
|
||||
:label="t('workload.container.command.args')"
|
||||
placeholder="e.g. /usr/sbin/httpd -f httpd.conf"
|
||||
/>
|
||||
</slot>
|
||||
</div>
|
||||
<div class="col span-2">
|
||||
<Checkbox v-model="container.tty" label="TTY" :mode="mode" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bordered-section">
|
||||
<h3>{{ t('workload.container.titles.env') }}</h3>
|
||||
<div class="row">
|
||||
<EnvVars v-model="container" :mode="mode" :secrets="namespacedSecrets" :config-maps="namespacedConfigMaps" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bordered-section">
|
||||
<h3>{{ t('resourceDetail.detailTop.labels') }}</h3>
|
||||
<div class="row">
|
||||
<KeyValue
|
||||
key="labels"
|
||||
v-model="value.metadata.labels"
|
||||
:mode="mode"
|
||||
:pad-left="false"
|
||||
:read-allowed="false"
|
||||
:protip="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h5> {{ nameDisplayFor(workloadType) }}</h5>
|
||||
<t class="type-description" :k="`workload.wizard.descriptions.'${workloadType}'`" :raw="true" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #container>
|
||||
<div class="bordered-section">
|
||||
<div class="row">
|
||||
<div class="col span-12">
|
||||
<NameNsDescription :value="value" :mode="mode" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isCronJob || isReplicable" class="row">
|
||||
<div v-if="isCronJob" class="col span-6">
|
||||
<LabeledInput v-model="spec.schedule" :mode="mode" :label="t('workload.cronSchedule')" />
|
||||
<span class="cron-hint text-small">{{ cronLabel }}</span>
|
||||
</div>
|
||||
<div v-if="isReplicable" class="col span-6">
|
||||
<LabeledInput v-model.number="spec.replicas" required :mode="mode" :label="t('workload.replicas')" />
|
||||
</div>
|
||||
<div v-if="isStatefulSet" class="col span-6">
|
||||
<LabeledSelect
|
||||
v-model="spec.serviceName"
|
||||
option-label="metadata.name"
|
||||
:reduce="service=>service.metadata.name"
|
||||
:mode="mode"
|
||||
:label="t('workload.serviceName')"
|
||||
:options="headlessServices"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bordered-section">
|
||||
<h3>{{ t('workload.container.titles.image') }}</h3>
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model="container.image"
|
||||
:label="t('workload.container.image')"
|
||||
placeholder="eg nginx:latest"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabeledSelect
|
||||
v-model="container.imagePullPolicy"
|
||||
:label="t('workload.container.imagePullPolicy')"
|
||||
:options="['Always', 'IfNotPresent', 'Never']"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bordered-section">
|
||||
<h3>{{ t('workload.container.titles.ports') }}</h3>
|
||||
<div class="row">
|
||||
<WorkloadPorts v-model="container.ports" :mode="mode" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bordered-section">
|
||||
<h3>{{ t('workload.container.titles.command') }}</h3>
|
||||
<div class="row">
|
||||
<div class="col span-5">
|
||||
<slot name="entrypoint">
|
||||
<ShellInput
|
||||
v-model="container.command"
|
||||
<h3>{{ t('resourceDetail.detailTop.annotations') }}</h3>
|
||||
<div class="row">
|
||||
<KeyValue
|
||||
key="annotations"
|
||||
v-model="value.metadata.annotations"
|
||||
:mode="mode"
|
||||
:label="t('workload.container.command.command')"
|
||||
placeholder="e.g. /bin/sh"
|
||||
:pad-left="false"
|
||||
:read-allowed="false"
|
||||
:protip="false"
|
||||
/>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col span-5">
|
||||
<slot name="command">
|
||||
<ShellInput
|
||||
v-model="container.args"
|
||||
:mode="mode"
|
||||
:label="t('workload.container.command.args')"
|
||||
placeholder="e.g. /usr/sbin/httpd -f httpd.conf"
|
||||
/>
|
||||
</slot>
|
||||
</div>
|
||||
<div class="col span-2">
|
||||
<Checkbox v-model="container.tty" label="TTY" :mode="mode" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bordered-section">
|
||||
<h3>{{ t('workload.container.titles.env') }}</h3>
|
||||
<div class="row">
|
||||
<EnvVars v-model="container" :mode="mode" :secrets="namespacedSecrets" :config-maps="namespacedConfigMaps" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bordered-section">
|
||||
<h3>{{ t('resourceDetail.detailTop.labels') }}</h3>
|
||||
<div class="row">
|
||||
<KeyValue
|
||||
key="labels"
|
||||
v-model="value.metadata.labels"
|
||||
:mode="mode"
|
||||
:pad-left="false"
|
||||
:read-allowed="false"
|
||||
:protip="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3>{{ t('resourceDetail.detailTop.annotations') }}</h3>
|
||||
<div class="row">
|
||||
<KeyValue
|
||||
key="annotations"
|
||||
v-model="value.metadata.annotations"
|
||||
:mode="mode"
|
||||
:pad-left="false"
|
||||
:read-allowed="false"
|
||||
:protip="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #storage>
|
||||
<t k="generic.comingSoon" />
|
||||
</template>
|
||||
<template #advanced>
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model="container.workingDir"
|
||||
:mode="mode"
|
||||
:label="t('workload.container.command.workingDir')"
|
||||
placeholder="e.g. /myapp"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabeledSelect v-model="stdinSelect" label="Stdin" :options="container.tty ? ['Yes', 'Once'] : ['Yes', 'Once', 'No']" :mode="mode" />
|
||||
</div>
|
||||
</div>
|
||||
<Tabbed :side-tabs="true">
|
||||
<Tab :label="t('workload.container.titles.resources')" name="resources">
|
||||
</Tab>
|
||||
<Tab :label="t('workload.storage.title')" name="storage">
|
||||
<t k="generic.comingSoon" />
|
||||
</Tab>
|
||||
<Tab :can-toggle="true" :label="t('workload.container.titles.resources')" name="resources">
|
||||
<ContainerResourceLimit v-model="flatResources" class="bordered-section" :mode="mode" :show-tip="false" />
|
||||
<div class="bordered-section">
|
||||
<h3 class="mb-10">
|
||||
|
|
@ -746,49 +674,28 @@ export default {
|
|||
</div>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab :label="t('workload.container.titles.podScheduling')" name="podScheduling">
|
||||
<Tab :can-toggle="true" :label="t('workload.container.titles.podScheduling')" name="podScheduling">
|
||||
<PodScheduling :mode="mode" :value="podTemplateSpec" />
|
||||
</Tab>
|
||||
<Tab :label="t('workload.container.titles.nodeScheduling')" name="nodeScheduling">
|
||||
<Tab :can-toggle="true" :label="t('workload.container.titles.nodeScheduling')" name="nodeScheduling">
|
||||
<NodeScheduling :mode="mode" :value="podTemplateSpec" />
|
||||
</Tab>
|
||||
<Tab label="Scaling/Upgrade Policy" name="upgrading">
|
||||
<Tab :can-toggle="true" label="Scaling/Upgrade Policy" name="upgrading">
|
||||
<Job v-if="isJob || isCronJob" v-model="spec" :mode="mode" :type="type" />
|
||||
<Upgrading v-else v-model="spec" :mode="mode" :type="type" />
|
||||
</Tab>
|
||||
<Tab :label="t('workload.container.titles.healthCheck')" name="healthCheck">
|
||||
<Tab :can-toggle="true" :label="t('workload.container.titles.healthCheck')" name="healthCheck">
|
||||
<HealthCheck v-model="healthCheck" :mode="mode" />
|
||||
</Tab>
|
||||
<Tab :label="t('workload.container.titles.securityContext')" name="securityContext">
|
||||
<Tab :can-toggle="true" :label="t('workload.container.titles.securityContext')" name="securityContext">
|
||||
<Security v-model="container.securityContext" :mode="mode" />
|
||||
</Tab>
|
||||
<Tab :label="t('workload.container.titles.networking')" name="networking">
|
||||
<Tab :can-toggle="true" :label="t('workload.container.titles.networking')" name="networking">
|
||||
<Networking v-model="podTemplateSpec" :mode="mode" />
|
||||
</Tab>
|
||||
</tabbed>
|
||||
</Tabbed>
|
||||
</template>
|
||||
<template #cancel>
|
||||
<button type="button" class="btn role-secondary" @click="cancel">
|
||||
<t k="generic.cancel" />
|
||||
</button>
|
||||
</template>
|
||||
<template #next="{next}">
|
||||
<ButtonDropdown :disabled="!(steps[currentStepIndex-1]||{}).ready" class="next-dropdown">
|
||||
<template #button-content>
|
||||
<button :disabled="!(steps[currentStepIndex-1]||{}).ready" type="button" :class="{'text-primary': !!(steps[currentStepIndex-1]||{}).ready}" class="btn bg-transparent" @click="next">
|
||||
{{ t('wizard.next') }}
|
||||
</button>
|
||||
</template>
|
||||
<template v-if="containerIsReady" #popover-content>
|
||||
<ul class="list-unstyled menu">
|
||||
<li v-close-popover.all @click="()=>saveWorkload(()=>{})">
|
||||
{{ t("generic.create") }}
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</ButtonDropdown>
|
||||
</template>
|
||||
</Wizard>
|
||||
</CruResource>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export default {
|
|||
|
||||
// keep label and annotation filters in data so each resource CRUD page can alter individiaully
|
||||
return {
|
||||
errors: null,
|
||||
errors: [],
|
||||
labelPrefixToIgnore: LABEL_PREFIX_TO_IGNORE,
|
||||
annotationsToIgnoreContains: ANNOTATIONS_TO_IGNORE_CONTAINS,
|
||||
annotationsToIgnorePrefix: ANNOTATIONS_TO_IGNORE_PREFIX
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { insertAt } from '@/utils/array';
|
||||
import { TIMESTAMP } from '@/config/labels-annotations';
|
||||
import { WORKLOAD_TYPES } from '@/config/types';
|
||||
import { _VIEW } from '@/config/query-params';
|
||||
|
||||
export default {
|
||||
// remove clone as yaml/edit as yaml until API supported
|
||||
|
|
@ -74,15 +73,4 @@ export default {
|
|||
this.setAnnotation(TIMESTAMP, now);
|
||||
this.save();
|
||||
},
|
||||
|
||||
// Hide resource detail masthead in create/edit/clone because it's redundant with the wizard
|
||||
showMasthead() {
|
||||
return (mode) => {
|
||||
if (mode !== _VIEW) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
import ResourceDetail, { watchQuery, asyncData } from '@/components/ResourceDetail';
|
||||
|
||||
export default {
|
||||
name: 'ClusterResourcedId',
|
||||
components: { ResourceDetail },
|
||||
name: 'ClusterResourcedId',
|
||||
components: { ResourceDetail },
|
||||
asyncData,
|
||||
watchQuery,
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue