mirror of https://github.com/rancher/dashboard.git
Merge branch 'master' of github.com:rancher/dashboard into 11289-undefined-chart-version
This commit is contained in:
commit
1603dd2551
|
|
@ -52,7 +52,7 @@ describe('Bundles', { testIsolation: 'off', tags: ['@fleet', '@adminUser'] }, ()
|
|||
fleetBundleeDetailsPage.waitForPage();
|
||||
|
||||
// check table headers
|
||||
const expectedHeadersDetailsViewEvents = ['Name'];
|
||||
const expectedHeadersDetailsViewEvents = ['State', 'API Version', 'Kind', 'Name', 'Namespace'];
|
||||
|
||||
fleetBundleeDetailsPage.list().resourceTable().sortableTable()
|
||||
.tableHeaderRow()
|
||||
|
|
|
|||
|
|
@ -319,13 +319,17 @@ describe('Settings', { testIsolation: 'off' }, () => {
|
|||
it('can update telemetry-opt', { tags: ['@globalSettings', '@adminUser'] }, () => {
|
||||
// Update setting: Prompt
|
||||
SettingsPagePo.navTo();
|
||||
|
||||
settingsPage.waitForPage();
|
||||
settingsPage.settingsValue('telemetry-opt').contains('Opt-out of Telemetry');
|
||||
|
||||
settingsPage.editSettingsByLabel('telemetry-opt');
|
||||
|
||||
const settingsEdit = settingsPage.editSettings('local', 'telemetry-opt');
|
||||
|
||||
settingsEdit.waitForPage();
|
||||
settingsEdit.title().contains('Setting: telemetry-opt').should('be.visible');
|
||||
settingsEdit.useDefaultButton().should('be.disabled'); // button should be disabled for this settings option
|
||||
settingsEdit.useDefaultButton().should('not.be.disabled');
|
||||
settingsEdit.selectSettingsByLabel('Prompt');
|
||||
settingsEdit.saveAndWait('telemetry-opt', 'prompt').then(({ request, response }) => {
|
||||
expect(response?.statusCode).to.eq(200);
|
||||
|
|
|
|||
|
|
@ -1475,6 +1475,7 @@ cluster:
|
|||
placeholder: A unique name for the cluster
|
||||
directoryConfig:
|
||||
title: Data directory configuration
|
||||
banner: Data directory configuration can not be changed once the cluster has been created
|
||||
radioInput:
|
||||
defaultLabel: Use default data directory configuration
|
||||
commonLabel: Use a common base directory for data directory configuration (sub-directories will be used for the system-agent, provisioning and distro paths)
|
||||
|
|
@ -3237,6 +3238,9 @@ logging:
|
|||
certificate: Connection
|
||||
labels: Labels
|
||||
configuration: Configuration
|
||||
tips:
|
||||
singleProvider: This output is configured with multiple providers. We currently only support a single provider per output. You can view or edit the YAML.
|
||||
multipleProviders: This output is configured with providers we do not support yet. You can view or edit the YAML.
|
||||
outputProviders:
|
||||
elasticsearch: Elasticsearch
|
||||
opensearch: OpenSearch
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ export default {
|
|||
dark: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
supportCustomLogo: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
|
@ -75,7 +79,7 @@ export default {
|
|||
},
|
||||
|
||||
pathToBrandedImage() {
|
||||
if (this.fileName === 'rancher-logo.svg') {
|
||||
if (this.fileName === 'rancher-logo.svg' || this.supportCustomLogo) {
|
||||
if (this.theme === 'dark' && this.uiLogoDark) {
|
||||
return this.uiLogoDark;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ export default {
|
|||
|
||||
<template>
|
||||
<a
|
||||
v-if="text"
|
||||
class="copy-to-clipboard-text"
|
||||
:class="{ 'copied': copied, 'plain': plain}"
|
||||
href="#"
|
||||
|
|
@ -59,6 +60,7 @@ export default {
|
|||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.copy-to-clipboard-text {
|
||||
white-space: nowrap;
|
||||
&.plain {
|
||||
color: var(--body-text);
|
||||
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@ export default {
|
|||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
z-index: 40;
|
||||
|
||||
.btn {
|
||||
margin-left: 20px;
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ export default {
|
|||
},
|
||||
|
||||
labels() {
|
||||
if (!this.showFilteredSystemLabels) {
|
||||
if (this.showAllLabels || !this.showFilteredSystemLabels) {
|
||||
return this.value?.labels || {};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import SortableTable from '@shell/components/SortableTable';
|
|||
import { mapGetters } from 'vuex';
|
||||
import { canViewProjectMembershipEditor } from '@shell/components/form/Members/ProjectMembershipEditor.vue';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { HARVESTER_NAME as HARVESTER } from '@shell/config/features';
|
||||
|
||||
/**
|
||||
* Explorer members page.
|
||||
|
|
@ -227,6 +228,9 @@ export default {
|
|||
canEditClusterMembers() {
|
||||
return this.normanClusterRTBSchema?.collectionMethods.find((x) => x.toLowerCase() === 'post');
|
||||
},
|
||||
isHarvester() {
|
||||
return this.$store.getters['currentProduct'].inStore === HARVESTER;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getMgmtProjectId(group) {
|
||||
|
|
@ -324,7 +328,7 @@ export default {
|
|||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
v-if="canManageProjectMembers"
|
||||
v-if="canManageProjectMembers && !isHarvester"
|
||||
name="project-membership"
|
||||
:label="t('members.projectMembership')"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,21 @@
|
|||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import ResourceTable, { defaultTableSortGenerationFn } from '@shell/components/ResourceTable';
|
||||
import { STATE, AGE, NAME } from '@shell/config/table-headers';
|
||||
import { STATE, AGE, NAME, NS_SNAPSHOT_QUOTA } from '@shell/config/table-headers';
|
||||
import { uniq } from '@shell/utils/array';
|
||||
import { MANAGEMENT, NAMESPACE, VIRTUAL_TYPES } from '@shell/config/types';
|
||||
import { MANAGEMENT, NAMESPACE, VIRTUAL_TYPES, HCI } from '@shell/config/types';
|
||||
import { PROJECT_ID, FLAT_VIEW } from '@shell/config/query-params';
|
||||
import { PanelLocation, ExtensionPoint } from '@shell/core/types';
|
||||
import ExtensionPanel from '@shell/components/ExtensionPanel';
|
||||
import Masthead from '@shell/components/ResourceList/Masthead';
|
||||
import { mapPref, GROUP_RESOURCES, ALL_NAMESPACES } from '@shell/store/prefs';
|
||||
import { mapPref, GROUP_RESOURCES, ALL_NAMESPACES, DEV } from '@shell/store/prefs';
|
||||
import MoveModal from '@shell/components/MoveModal';
|
||||
import ButtonMultiAction from '@shell/components/ButtonMultiAction.vue';
|
||||
|
||||
import { NAMESPACE_FILTER_ALL_ORPHANS } from '@shell/utils/namespace-filter';
|
||||
import ResourceFetch from '@shell/mixins/resource-fetch';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { HARVESTER_NAME as HARVESTER } from '@shell/config/features';
|
||||
|
||||
export default {
|
||||
name: 'ListProjectNamespace',
|
||||
|
|
@ -42,6 +43,7 @@ export default {
|
|||
async fetch() {
|
||||
const inStore = this.$store.getters['currentStore'](NAMESPACE);
|
||||
|
||||
this.harvesterResourceQuotaSchema = this.$store.getters[`${ inStore }/schemaFor`](HCI.RESOURCE_QUOTA);
|
||||
this.schema = this.$store.getters[`${ inStore }/schemaFor`](NAMESPACE);
|
||||
this.projectSchema = this.$store.getters[`management/schemaFor`](MANAGEMENT.PROJECT);
|
||||
|
||||
|
|
@ -60,6 +62,7 @@ export default {
|
|||
return {
|
||||
loadResources: [NAMESPACE],
|
||||
loadIndeterminate: true,
|
||||
harvesterResourceQuotaSchema: null,
|
||||
schema: null,
|
||||
projects: [],
|
||||
projectSchema: null,
|
||||
|
|
@ -93,20 +96,33 @@ export default {
|
|||
isNamespaceCreatable() {
|
||||
return (this.schema?.collectionMethods || []).includes('POST');
|
||||
},
|
||||
isHarvester() {
|
||||
return this.$store.getters['currentProduct'].inStore === HARVESTER;
|
||||
},
|
||||
headers() {
|
||||
const project = {
|
||||
name: 'project',
|
||||
label: this.t('tableHeaders.project'),
|
||||
value: 'project.nameDisplay',
|
||||
sort: ['projectNameSort', 'nameSort'],
|
||||
};
|
||||
|
||||
return [
|
||||
const headers = [
|
||||
STATE,
|
||||
NAME,
|
||||
this.groupPreference === 'none' ? project : null,
|
||||
AGE
|
||||
].filter((h) => h);
|
||||
];
|
||||
|
||||
if (this.groupPreference === 'none') {
|
||||
const projectHeader = {
|
||||
name: 'project',
|
||||
label: this.t('tableHeaders.project'),
|
||||
value: 'project.nameDisplay',
|
||||
sort: ['projectNameSort', 'nameSort'],
|
||||
};
|
||||
|
||||
headers.push(projectHeader);
|
||||
}
|
||||
|
||||
if (this.isHarvester && this.harvesterResourceQuotaSchema) {
|
||||
headers.push(NS_SNAPSHOT_QUOTA);
|
||||
}
|
||||
|
||||
headers.push(AGE);
|
||||
|
||||
return headers;
|
||||
},
|
||||
projectIdsWithNamespaces() {
|
||||
const ids = this.rows
|
||||
|
|
@ -211,7 +227,15 @@ export default {
|
|||
return this.groupPreference === 'none' ? this.rows : this.rowsWithFakeNamespaces;
|
||||
},
|
||||
rows() {
|
||||
if (this.$store.getters['prefs/get'](ALL_NAMESPACES)) {
|
||||
let isDev;
|
||||
|
||||
try {
|
||||
isDev = this.$store.getters['prefs/get'](ALL_NAMESPACES);
|
||||
} catch {
|
||||
isDev = this.$store.getters['prefs/get'](DEV);
|
||||
}
|
||||
|
||||
if (isDev) {
|
||||
// If all namespaces options are turned on in the user preferences,
|
||||
// return all namespaces including system namespaces and RBAC
|
||||
// management namespaces.
|
||||
|
|
|
|||
|
|
@ -20,9 +20,19 @@ export default {
|
|||
default: null
|
||||
},
|
||||
|
||||
usedTitle: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
|
||||
reserved: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
|
||||
reservedTitle: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -78,7 +88,7 @@ export default {
|
|||
>
|
||||
<template #title>
|
||||
<span>
|
||||
{{ t('clusterIndexPage.hardwareResourceGauge.reserved') }}
|
||||
{{ reservedTitle ?? t('clusterIndexPage.hardwareResourceGauge.reserved') }}
|
||||
<span class="values text-muted">
|
||||
<span v-if="reserved.formattedUseful">
|
||||
{{ reserved.formattedUseful }}
|
||||
|
|
@ -112,7 +122,7 @@ export default {
|
|||
>
|
||||
<template #title>
|
||||
<span>
|
||||
{{ t('clusterIndexPage.hardwareResourceGauge.used') }}
|
||||
{{ usedTitle ?? t('clusterIndexPage.hardwareResourceGauge.used') }}
|
||||
<span class="values text-muted">
|
||||
<span v-if="used.formattedUseful">
|
||||
{{ used.formattedUseful }}
|
||||
|
|
|
|||
|
|
@ -39,10 +39,14 @@ export default {
|
|||
class="label"
|
||||
>
|
||||
<div class="text-label">
|
||||
{{ $slots.name || name }}
|
||||
<slot name="name">
|
||||
{{ name }}
|
||||
</slot>
|
||||
</div>
|
||||
<div class="value">
|
||||
{{ $slots.value || displayValue }}
|
||||
<slot name="value">
|
||||
{{ displayValue }}
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<slot v-else />
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
export default {
|
||||
props: {
|
||||
to: {
|
||||
type: String,
|
||||
type: [String, Object],
|
||||
required: true
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -101,6 +101,10 @@ export default {
|
|||
},
|
||||
|
||||
computed: {
|
||||
dev() {
|
||||
return this.$store.getters['prefs/dev'];
|
||||
},
|
||||
|
||||
schema() {
|
||||
const inStore = this.storeOverride || this.$store.getters['currentStore'](this.resource);
|
||||
|
||||
|
|
@ -383,6 +387,10 @@ export default {
|
|||
hideNamespaceLocation() {
|
||||
return this.$store.getters['currentProduct'].hideNamespaceLocation || this.value.namespaceLocation === null;
|
||||
},
|
||||
|
||||
resourceExternalLink() {
|
||||
return this.value.resourceExternalLink;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
|
@ -462,6 +470,16 @@ export default {
|
|||
class="icon icon-sm icon-istio"
|
||||
/>
|
||||
</span>
|
||||
<a
|
||||
v-if="dev && !!resourceExternalLink"
|
||||
v-clean-tooltip="t(resourceExternalLink.tipsKey || 'generic.resourceExternalLinkTips')"
|
||||
class="resource-external"
|
||||
rel="nofollow noopener noreferrer"
|
||||
target="_blank"
|
||||
:href="resourceExternalLink.url"
|
||||
>
|
||||
<i class="icon icon-external-link" />
|
||||
</a>
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -642,4 +660,7 @@ export default {
|
|||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.resource-external {
|
||||
font-size: 18px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -168,7 +168,8 @@ export default {
|
|||
},
|
||||
bundle: {
|
||||
inStoreType: 'management',
|
||||
type: FLEET.BUNDLE
|
||||
type: FLEET.BUNDLE,
|
||||
opt: { excludeFields: ['metadata.managedFields', 'spec.resources'] },
|
||||
},
|
||||
|
||||
bundleDeployment: {
|
||||
|
|
|
|||
|
|
@ -143,7 +143,6 @@ export default {
|
|||
type: Function,
|
||||
default: null
|
||||
},
|
||||
|
||||
ignoreFilter: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
|
|
@ -183,7 +182,12 @@ export default {
|
|||
externalPaginationResult: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
|
||||
rowsPerPage: {
|
||||
type: Number,
|
||||
default: null, // Default comes from the user preference
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
|
|
@ -457,13 +461,16 @@ export default {
|
|||
tooltipKey: 'resourceTable.groupBy.none',
|
||||
icon: 'icon-list-flat',
|
||||
value: 'none',
|
||||
},
|
||||
{
|
||||
}
|
||||
];
|
||||
|
||||
if (!this.options?.hiddenNamespaceGroupButton) {
|
||||
standard.push( {
|
||||
tooltipKey: this.groupTooltip,
|
||||
icon: 'icon-folder',
|
||||
value: 'namespace',
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
// SUPPLEMENT (instead of REPLACE) defaults with listGroups (given listGroupsWillOverride is false)
|
||||
if (!!this.options?.listGroups?.length) {
|
||||
|
|
@ -571,6 +578,7 @@ export default {
|
|||
:paging="true"
|
||||
:paging-params="parsedPagingParams"
|
||||
:paging-label="pagingLabel"
|
||||
:rows-per-page="rowsPerPage"
|
||||
:row-actions="rowActions"
|
||||
:table-actions="_showBulkActions"
|
||||
:overflow-x="overflowX"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import {
|
|||
FLEET_REPO_PER_CLUSTER_STATE
|
||||
|
||||
} from '@shell/config/table-headers';
|
||||
import { FLEET } from '@shell/config/labels-annotations';
|
||||
import { STATES_ENUM } from '@shell/plugins/dashboard-store/resource-class';
|
||||
|
||||
// i18n-ignore repoDisplay
|
||||
|
|
@ -118,12 +117,6 @@ export default {
|
|||
parseTargetMode(row) {
|
||||
return row.targetInfo?.mode === 'clusterGroup' ? this.t('fleet.gitRepo.warningTooltip.clusterGroup') : this.t('fleet.gitRepo.warningTooltip.cluster');
|
||||
},
|
||||
|
||||
clusterViewResourceStatus(row) {
|
||||
return row.clusterResourceStatus.find((c) => {
|
||||
return c.metadata?.labels[FLEET.CLUSTER_NAME] === this.clusterId;
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ export default {
|
|||
type: Array,
|
||||
// we only want functions in the rules array
|
||||
validator: (rules) => rules.every((rule) => ['function'].includes(typeof rule))
|
||||
}
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const input = (Array.isArray(this.value) ? this.value : []).slice();
|
||||
|
|
@ -407,4 +407,8 @@ export default {
|
|||
padding: 5px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.required {
|
||||
color: var(--error);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@ export default {
|
|||
type: Object,
|
||||
default: null
|
||||
},
|
||||
enableDefaultAddValue: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
|
|
@ -43,7 +47,7 @@ export default {
|
|||
},
|
||||
|
||||
defaultAddValue() {
|
||||
return this.options[0]?.value;
|
||||
return this.enableDefaultAddValue ? this.options[0]?.value : '';
|
||||
},
|
||||
|
||||
getOptionLabel() {
|
||||
|
|
|
|||
|
|
@ -717,7 +717,7 @@ export default {
|
|||
@onFocus="onFocusMarkdownMultiline(i, $event)"
|
||||
/>
|
||||
<TextAreaAutoGrow
|
||||
v-else-if="valueMultiline"
|
||||
v-else-if="valueMultiline && row[valueName] !== undefined"
|
||||
v-model:value="row[valueName]"
|
||||
data-testid="value-multiline"
|
||||
:class="{'conceal': valueConcealed}"
|
||||
|
|
|
|||
|
|
@ -7,8 +7,7 @@ import VueSelectOverrides from '@shell/mixins/vue-select-overrides';
|
|||
import { onClickOption, calculatePosition } from '@shell/utils/select';
|
||||
import LabeledSelectPagination from '@shell/components/form/labeled-select-utils/labeled-select-pagination';
|
||||
import { LABEL_SELECT_NOT_OPTION_KINDS } from '@shell/types/components/labeledSelect';
|
||||
|
||||
// In theory this would be nicer as LabeledSelect/index.vue, however that would break a lot of places where we import this (which includes extensions)
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'LabeledSelect',
|
||||
|
|
@ -23,7 +22,7 @@ export default {
|
|||
LabeledSelectPagination
|
||||
],
|
||||
|
||||
emits: ['on-open', 'on-close', 'selecting', 'update:validation', 'update:value'],
|
||||
emits: ['on-open', 'on-close', 'selecting', 'deselecting', 'update:validation', 'update:value'],
|
||||
|
||||
props: {
|
||||
appendToBody: {
|
||||
|
|
@ -122,6 +121,7 @@ export default {
|
|||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
hasLabel() {
|
||||
return this.isCompact ? false : !!this.label || !!this.labelKey || !!this.$slots.label;
|
||||
},
|
||||
|
|
@ -144,6 +144,11 @@ export default {
|
|||
|
||||
return rest;
|
||||
},
|
||||
|
||||
// update placeholder text to inform user they can add their own opts when none are found
|
||||
showTagPrompts() {
|
||||
return !this.options.length && this.$attrs.taggable;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
|
@ -320,10 +325,16 @@ export default {
|
|||
@search="onSearch"
|
||||
@open="onOpen"
|
||||
@close="onClose"
|
||||
@option:selected="$emit('selecting', $event)"
|
||||
@option:selecting="$emit('selecting', $event)"
|
||||
@option:deselecting="$emit('deselecting', $event)"
|
||||
>
|
||||
<template #option="option">
|
||||
<template v-if="option.kind === 'group'">
|
||||
<template v-if="showTagPrompts">
|
||||
<div class="only-user-opts">
|
||||
{{ t('labeledSelect.pressEnter', {input:getOptionLabel(option.label)}) }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="option.kind === 'group'">
|
||||
<div class="vs__option-kind-group">
|
||||
<i
|
||||
v-if="option.icon"
|
||||
|
|
@ -395,8 +406,11 @@ export default {
|
|||
</template>
|
||||
<template #no-options="{ search }">
|
||||
<div class="no-options-slot">
|
||||
<template v-if="showTagPrompts">
|
||||
<span v-if="!searching">{{ t('labeledSelect.startTyping') }}</span>
|
||||
</template>
|
||||
<div
|
||||
v-if="paginating"
|
||||
v-else-if="paginating"
|
||||
class="paginating"
|
||||
>
|
||||
<i class="icon icon-spinner icon-spin" />
|
||||
|
|
@ -674,4 +688,10 @@ $icon-size: 18px;
|
|||
}
|
||||
}
|
||||
|
||||
.vs__dropdown-menu .vs__dropdown-option .only-user-opts{
|
||||
color: var(--dropdown-text);
|
||||
background-color: var(--dropdown-bg);
|
||||
margin: 0px calc(-#{$input-padding-sm}/2);
|
||||
padding: 3px 20px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { mapGetters } from 'vuex';
|
|||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import { CHARSET, randomStr } from '@shell/utils/string';
|
||||
import { copyTextToClipboard } from '@shell/utils/clipboard';
|
||||
import { _CREATE } from '@shell/config/query-params';
|
||||
|
||||
export default {
|
||||
emits: ['update:value', 'blur'],
|
||||
|
|
@ -36,7 +37,11 @@ export default {
|
|||
ignorePasswordManagers: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
}
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: _CREATE,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return { reveal: false };
|
||||
|
|
@ -104,6 +109,7 @@ export default {
|
|||
:required="required"
|
||||
:disabled="isRandom"
|
||||
:ignore-password-managers="ignorePasswordManagers"
|
||||
:mode="mode"
|
||||
@blur="$emit('blur', $event)"
|
||||
>
|
||||
<template #suffix>
|
||||
|
|
|
|||
|
|
@ -128,7 +128,12 @@ export default {
|
|||
delay: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
|
||||
positive: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
|
@ -196,6 +201,10 @@ export default {
|
|||
update(inputValue) {
|
||||
let out = inputValue === '' ? null : inputValue;
|
||||
|
||||
if (this.positive && inputValue < 0) {
|
||||
out = 0;
|
||||
}
|
||||
|
||||
if (this.outputModifier) {
|
||||
out = out === null ? null : `${ inputValue }${ this.unit }`;
|
||||
} else if ( this.outputAs === 'string' ) {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ describe('component: UnitInput', () => {
|
|||
|
||||
await input.setValue(2);
|
||||
await input.setValue(4);
|
||||
input.trigger(event);
|
||||
|
||||
expect(wrapper.emitted('update:value')).toBeTruthy();
|
||||
expect(wrapper.emitted('update:value')[2]).toStrictEqual([4]);
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export default {
|
|||
},
|
||||
|
||||
completed() {
|
||||
return Number.parseFloat(this.value) === 100 && !this.failed;
|
||||
return Number.parseFloat(this.value) === 100;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -38,5 +38,9 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<span>{{ formattedValue }}</span>
|
||||
<span v-if="value">{{ formattedValue }}</span>
|
||||
<span
|
||||
v-else
|
||||
class="text-muted"
|
||||
>—</span>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export default {
|
|||
|
||||
computed: {
|
||||
text() {
|
||||
return this.$store.getters['i18n/withFallback'](`${ this.prefix }.${ this.row.id }`, null, this.value);
|
||||
return this.$store.getters['i18n/withFallback'](`${ this.prefix }.${ this.value || this.row.id }`, null, this.value);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { NORMAN, STEVE } from '@shell/config/types';
|
||||
import { MANAGEMENT, NORMAN, STEVE } from '@shell/config/types';
|
||||
import { HARVESTER_NAME as HARVESTER } from '@shell/config/features';
|
||||
import { ucFirst } from '@shell/utils/string';
|
||||
import { isAlternate, isMac } from '@shell/utils/platform';
|
||||
import Import from '@shell/components/Import';
|
||||
import BrandImage from '@shell/components/BrandImage';
|
||||
import { getProduct } from '@shell/config/private-label';
|
||||
import { getProduct, getVendor } from '@shell/config/private-label';
|
||||
import ClusterProviderIcon from '@shell/components/ClusterProviderIcon';
|
||||
import ClusterBadge from '@shell/components/ClusterBadge';
|
||||
import AppModal from '@shell/components/AppModal';
|
||||
|
|
@ -14,6 +15,7 @@ import { LOGGED_OUT, IS_SSO } from '@shell/config/query-params';
|
|||
import NamespaceFilter from './NamespaceFilter';
|
||||
import WorkspaceSwitcher from './WorkspaceSwitcher';
|
||||
import TopLevelMenu from './TopLevelMenu';
|
||||
|
||||
import Jump from './Jump';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { ActionLocation, ExtensionPoint } from '@shell/core/types';
|
||||
|
|
@ -86,7 +88,8 @@ export default {
|
|||
'pageActions',
|
||||
'isSingleProduct',
|
||||
'isRancherInHarvester',
|
||||
'showTopLevelMenu'
|
||||
'showTopLevelMenu',
|
||||
'isMultiCluster'
|
||||
]),
|
||||
|
||||
samlAuthProviderEnabled() {
|
||||
|
|
@ -110,6 +113,12 @@ export default {
|
|||
return getProduct();
|
||||
},
|
||||
|
||||
vendor() {
|
||||
this.$store.getters['management/all'](MANAGEMENT.SETTING)?.find((setting) => setting.id === 'ui-pl');
|
||||
|
||||
return getVendor();
|
||||
},
|
||||
|
||||
authEnabled() {
|
||||
return this.$store.getters['auth/enabled'];
|
||||
},
|
||||
|
|
@ -219,6 +228,9 @@ export default {
|
|||
};
|
||||
},
|
||||
|
||||
isHarvester() {
|
||||
return this.$store.getters['currentProduct'].inStore === HARVESTER;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
|
@ -379,7 +391,7 @@ export default {
|
|||
data-testid="header"
|
||||
>
|
||||
<div>
|
||||
<TopLevelMenu v-if="showTopLevelMenu" />
|
||||
<TopLevelMenu v-if="isRancherInHarvester || isMultiCluster || !isSingleProduct" />
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer"
|
||||
|
|
@ -389,7 +401,14 @@ export default {
|
|||
v-if="isSingleProduct && !isRancherInHarvester"
|
||||
:to="singleProductLogoRoute"
|
||||
>
|
||||
<BrandImage
|
||||
v-if="isSingleProduct.supportCustomLogo && isHarvester"
|
||||
class="side-menu-logo"
|
||||
file-name="harvester.svg"
|
||||
:support-custom-logo="true"
|
||||
/>
|
||||
<img
|
||||
v-else
|
||||
class="side-menu-logo"
|
||||
:src="isSingleProduct.logo"
|
||||
>
|
||||
|
|
@ -409,7 +428,12 @@ export default {
|
|||
v-if="isSingleProduct && !isRancherInHarvester"
|
||||
class="product-name"
|
||||
>
|
||||
{{ t(isSingleProduct.productNameKey) }}
|
||||
<template v-if="isSingleProduct.supportCustomLogo">
|
||||
{{ vendor }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ t(isSingleProduct.productNameKey) }}
|
||||
</template>
|
||||
</div>
|
||||
<template v-else>
|
||||
<ClusterProviderIcon
|
||||
|
|
|
|||
|
|
@ -140,9 +140,11 @@ export default {
|
|||
options() {
|
||||
const t = this.$store.getters['i18n/t'];
|
||||
let out = [];
|
||||
const inStore = this.$store.getters['currentStore'](NAMESPACE);
|
||||
|
||||
const params = { ...this.$route.params };
|
||||
const resource = params.resource;
|
||||
|
||||
// Sometimes, different pages may have different namespaces to filter
|
||||
const notFilterNamespaces = this.$store.getters[`type-map/optionsFor`](resource).notFilterNamespace || [];
|
||||
|
||||
|
|
@ -198,8 +200,6 @@ export default {
|
|||
divider(out);
|
||||
}
|
||||
|
||||
const inStore = this.$store.getters['currentStore'](NAMESPACE);
|
||||
|
||||
if (!inStore) {
|
||||
return out;
|
||||
}
|
||||
|
|
@ -893,9 +893,6 @@ export default {
|
|||
width: 280px;
|
||||
display: inline-block;
|
||||
|
||||
$glass-z-index: 2;
|
||||
$dropdown-z-index: 1000;
|
||||
|
||||
.ns-glass {
|
||||
height: 100vh;
|
||||
left: 0;
|
||||
|
|
@ -903,7 +900,7 @@ export default {
|
|||
position: absolute;
|
||||
top: 0;
|
||||
width: 100vw;
|
||||
z-index: $glass-z-index;
|
||||
z-index: z-index('overContent');
|
||||
}
|
||||
|
||||
.ns-controls {
|
||||
|
|
@ -955,7 +952,7 @@ export default {
|
|||
margin-top: -1px;
|
||||
padding-bottom: 10px;
|
||||
position: relative;
|
||||
z-index: $dropdown-z-index;
|
||||
z-index: z-index('dropdownOverlay');
|
||||
|
||||
.ns-options {
|
||||
max-height: 50vh;
|
||||
|
|
@ -1067,7 +1064,7 @@ export default {
|
|||
height: 40px;
|
||||
padding: 0 10px;
|
||||
position: relative;
|
||||
z-index: $dropdown-z-index;
|
||||
z-index: z-index('dropdownOverlay');
|
||||
|
||||
&.ns-open {
|
||||
border-bottom-left-radius: 0;
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ export const FLEET = {
|
|||
CLUSTER_NAME: 'management.cattle.io/cluster-name',
|
||||
BUNDLE_ID: 'fleet.cattle.io/bundle-id',
|
||||
MANAGED: 'fleet.cattle.io/managed',
|
||||
CLUSTER_NAMESPACE: 'fleet.cattle.io/cluster-namespace',
|
||||
CLUSTER: 'fleet.cattle.io/cluster'
|
||||
};
|
||||
|
||||
|
|
@ -145,6 +146,7 @@ export const HCI = {
|
|||
NETWORK_ROUTE: 'network.harvesterhci.io/route',
|
||||
IMAGE_NAME: 'harvesterhci.io/image-name',
|
||||
NETWORK_TYPE: 'network.harvesterhci.io/type',
|
||||
CLUSTER_NETWORK: 'network.harvesterhci.io/clusternetwork',
|
||||
PRIMARY_SERVICE: 'cloudprovider.harvesterhci.io/primary-service',
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -267,6 +267,21 @@ export const DESCRIPTION = {
|
|||
width: 300,
|
||||
};
|
||||
|
||||
export const NS_SNAPSHOT_QUOTA = {
|
||||
name: 'NamespaceSnapshotQuota',
|
||||
labelKey: 'harvester.tableHeaders.totalSnapshotQuota',
|
||||
value: 'snapshotSizeQuota',
|
||||
sort: 'snapshotSizeQuota',
|
||||
align: 'center',
|
||||
formatter: 'Si',
|
||||
formatterOpts: {
|
||||
opts: {
|
||||
increment: 1024, addSuffix: true, suffix: 'i',
|
||||
},
|
||||
needParseSi: false
|
||||
},
|
||||
};
|
||||
|
||||
export const DURATION = {
|
||||
name: 'duration',
|
||||
labelKey: 'tableHeaders.duration',
|
||||
|
|
|
|||
|
|
@ -172,6 +172,8 @@ export const LONGHORN = {
|
|||
};
|
||||
|
||||
export const LONGHORN_DRIVER = 'driver.longhorn.io';
|
||||
export const LONGHORN_VERSION_V1 = 'LonghornV1';
|
||||
export const LONGHORN_VERSION_V2 = 'LonghornV2';
|
||||
|
||||
export const SNAPSHOT = 'rke.cattle.io.etcdsnapshot';
|
||||
|
||||
|
|
@ -313,6 +315,7 @@ export const HCI = {
|
|||
IMAGE: 'harvesterhci.io.virtualmachineimage',
|
||||
VGPU_DEVICE: 'devices.harvesterhci.io.vgpudevice',
|
||||
SETTING: 'harvesterhci.io.setting',
|
||||
RESOURCE_QUOTA: 'harvesterhci.io.resourcequota',
|
||||
HARVESTER_CONFIG: 'rke-machine-config.cattle.io.harvesterconfig',
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,77 +1,25 @@
|
|||
<script>
|
||||
import { FLEET } from '@shell/config/types';
|
||||
import FleetBundleResources from '@shell/components/fleet/FleetBundleResources.vue';
|
||||
import SortableTable from '@shell/components/SortableTable';
|
||||
import FleetUtils from '@shell/utils/fleet';
|
||||
|
||||
export default {
|
||||
name: 'FleetBundleDetail',
|
||||
|
||||
components: {
|
||||
FleetBundleResources,
|
||||
SortableTable,
|
||||
},
|
||||
props: {
|
||||
components: { FleetBundleResources },
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return { repo: null };
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const { namespace, labels } = this.value.metadata;
|
||||
const repoName = `${ namespace }/${ labels['fleet.cattle.io/repo-name'] }`;
|
||||
|
||||
if (this.hasRepoLabel) {
|
||||
this.repo = await this.$store.dispatch('management/find', { type: FLEET.GIT_REPO, id: repoName });
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
hasRepoLabel() {
|
||||
return !!(this.value?.metadata?.labels && this.value?.metadata?.labels['fleet.cattle.io/repo-name']);
|
||||
},
|
||||
bundleResources() {
|
||||
if (this.hasRepoLabel) {
|
||||
const bundleResourceIds = this.bundleResourceIds;
|
||||
|
||||
return this.repo?.status?.resources?.filter((resource) => {
|
||||
return bundleResourceIds.includes(resource.name);
|
||||
});
|
||||
} else if (this.value?.spec?.resources?.length) {
|
||||
return this.value?.spec?.resources.map((item) => {
|
||||
return {
|
||||
content: item.content,
|
||||
name: item.name.includes('.') ? item.name.split('.')[0] : item.name
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return [];
|
||||
},
|
||||
resourceHeaders() {
|
||||
return [
|
||||
{
|
||||
name: 'name',
|
||||
value: 'name',
|
||||
sort: ['name'],
|
||||
labelKey: 'tableHeaders.name',
|
||||
},
|
||||
];
|
||||
return FleetUtils.resourcesFromBundleStatus(this.value?.status);
|
||||
},
|
||||
resourceCount() {
|
||||
return (this.bundleResources && this.bundleResources.length) || this.value?.spec?.resources?.length;
|
||||
return this.bundleResources.length;
|
||||
},
|
||||
bundleResourceIds() {
|
||||
if (this.value.status?.resourceKey) {
|
||||
return this.value?.status?.resourceKey.map((item) => item.name);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -84,19 +32,8 @@ export default {
|
|||
<span>{{ resourceCount }}</span>
|
||||
</div>
|
||||
<FleetBundleResources
|
||||
v-if="hasRepoLabel"
|
||||
:value="bundleResources"
|
||||
/>
|
||||
<SortableTable
|
||||
v-else
|
||||
:rows="bundleResources"
|
||||
:headers="resourceHeaders"
|
||||
:table-actions="false"
|
||||
:row-actions="false"
|
||||
key-field="tableKey"
|
||||
default-sort-by="state"
|
||||
:paged="true"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -82,7 +82,8 @@ export default {
|
|||
const allDispatches = await checkSchemasForFindAllHash({
|
||||
allBundles: {
|
||||
inStoreType: 'management',
|
||||
type: FLEET.BUNDLE
|
||||
type: FLEET.BUNDLE,
|
||||
opt: { excludeFields: ['metadata.managedFields', 'spec.resources'] },
|
||||
},
|
||||
|
||||
allBundleDeployments: {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import KeyValue from '@shell/components/form/KeyValue';
|
||||
import Select from '@shell/components/form/Select';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { HARVESTER_NAME as VIRTUAL } from '@shell/config/features';
|
||||
|
||||
export default {
|
||||
emits: ['remove'],
|
||||
|
|
@ -39,6 +40,12 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
isHarvester() {
|
||||
return this.$store.getters['currentProduct'].inStore === VIRTUAL;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
update() {},
|
||||
|
||||
|
|
@ -51,19 +58,22 @@ export default {
|
|||
|
||||
<template>
|
||||
<div>
|
||||
<KeyValue
|
||||
v-model:value="value.labels"
|
||||
:title="value.select ? t('logging.flow.matches.pods.title.include') : t('logging.flow.matches.pods.title.exclude')"
|
||||
:mode="mode"
|
||||
:initial-empty-row="true"
|
||||
:read-allowed="false"
|
||||
:title-add="true"
|
||||
protip=""
|
||||
:key-label="t('logging.flow.matches.pods.keyLabel')"
|
||||
:value-label="t('logging.flow.matches.pods.valueLabel')"
|
||||
:add-label="t('logging.flow.matches.pods.addLabel')"
|
||||
/>
|
||||
<div class="spacer" />
|
||||
<template v-if="!isHarvester">
|
||||
<KeyValue
|
||||
v-model:value="value.labels"
|
||||
:title="value.select ? t('logging.flow.matches.pods.title.include') : t('logging.flow.matches.pods.title.exclude')"
|
||||
:mode="mode"
|
||||
:initial-empty-row="true"
|
||||
:read-allowed="false"
|
||||
:title-add="true"
|
||||
protip=""
|
||||
:key-label="t('logging.flow.matches.pods.keyLabel')"
|
||||
:value-label="t('logging.flow.matches.pods.valueLabel')"
|
||||
:add-label="t('logging.flow.matches.pods.addLabel')"
|
||||
/>
|
||||
<div class="spacer" />
|
||||
</template>
|
||||
|
||||
<h3>
|
||||
{{ value.select ? t('logging.flow.matches.nodes.title.include') : t('logging.flow.matches.nodes.title.exclude') }}
|
||||
</h3>
|
||||
|
|
@ -83,48 +93,71 @@ export default {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="spacer" />
|
||||
<h3>
|
||||
{{ value.select ? t('logging.flow.matches.containerNames.title.include') : t('logging.flow.matches.containerNames.title.exclude') }}
|
||||
</h3>
|
||||
<div class="row">
|
||||
<div class="col span-12">
|
||||
<LabeledSelect
|
||||
v-model:value="value.container_names"
|
||||
:mode="mode"
|
||||
:options="[]"
|
||||
:disabled="false"
|
||||
:placeholder="t('logging.flow.matches.containerNames.placeholder')"
|
||||
:multiple="true"
|
||||
:taggable="true"
|
||||
:clearable="true"
|
||||
:searchable="true"
|
||||
:close-on-select="false"
|
||||
no-options-label-key="logging.flow.matches.containerNames.enter"
|
||||
placement="top"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isClusterFlow">
|
||||
<div v-if="!isHarvester">
|
||||
<div class="spacer" />
|
||||
<h3>
|
||||
{{ value.select ? t('logging.flow.matches.namespaces.title.include') : t('logging.flow.matches.namespaces.title.exclude') }}
|
||||
{{ value.select ? t('logging.flow.matches.containerNames.title.include') : t('logging.flow.matches.containerNames.title.exclude') }}
|
||||
</h3>
|
||||
<div class="row">
|
||||
<div class="col span-12">
|
||||
<Select
|
||||
v-model:value="value.namespaces"
|
||||
class="lg"
|
||||
:options="namespaces"
|
||||
:placeholder="t('logging.flow.matches.namespaces.placeholder')"
|
||||
<LabeledSelect
|
||||
v-model:value="value.container_names"
|
||||
:mode="mode"
|
||||
:options="[]"
|
||||
:disabled="false"
|
||||
:placeholder="t('logging.flow.matches.containerNames.placeholder')"
|
||||
:multiple="true"
|
||||
:taggable="true"
|
||||
:clearable="true"
|
||||
:searchable="true"
|
||||
:close-on-select="false"
|
||||
no-options-label-key="logging.flow.matches.containerNames.enter"
|
||||
placement="top"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isClusterFlow">
|
||||
<div class="spacer" />
|
||||
<h3>
|
||||
{{ value.select ? t('logging.flow.matches.containerNames.title.include') : t('logging.flow.matches.containerNames.title.exclude') }}
|
||||
</h3>
|
||||
<div class="row">
|
||||
<div class="col span-12">
|
||||
<Select
|
||||
v-model:value="value.namespaces"
|
||||
class="lg"
|
||||
:options="namespaces"
|
||||
:placeholder="t('logging.flow.matches.namespaces.placeholder')"
|
||||
:multiple="true"
|
||||
:taggable="true"
|
||||
:clearable="true"
|
||||
:searchable="true"
|
||||
:close-on-select="false"
|
||||
no-options-label-key="logging.flow.matches.containerNames.enter"
|
||||
placement="top"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="spacer" />
|
||||
<h3>
|
||||
{{ value.select ? t('logging.flow.matches.namespaces.title.include') : t('logging.flow.matches.namespaces.title.exclude') }}
|
||||
</h3>
|
||||
<div class="row">
|
||||
<div class="col span-12">
|
||||
<Select
|
||||
v-model="value.namespaces"
|
||||
class="lg"
|
||||
:options="namespaces"
|
||||
:placeholder="t('logging.flow.matches.namespaces.placeholder')"
|
||||
:multiple="true"
|
||||
:taggable="true"
|
||||
:clearable="true"
|
||||
:close-on-select="false"
|
||||
placement="top"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -6,20 +6,28 @@ import Loading from '@shell/components/Loading';
|
|||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||
import Tabbed from '@shell/components/Tabbed';
|
||||
import Tab from '@shell/components/Tabbed/Tab';
|
||||
import { LOGGING, NAMESPACE, NODE, SCHEMA } from '@shell/config/types';
|
||||
import {
|
||||
LOGGING, NAMESPACE, NODE, POD, SCHEMA
|
||||
} from '@shell/config/types';
|
||||
import jsyaml from 'js-yaml';
|
||||
import { createYaml } from '@shell/utils/create-yaml';
|
||||
import YamlEditor, { EDITOR_MODES } from '@shell/components/YamlEditor';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { isArray } from '@shell/utils/array';
|
||||
import { isArray, uniq } from '@shell/utils/array';
|
||||
import { matchRuleIsPopulated } from '@shell/models/logging.banzaicloud.io.flow';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { clone } from '@shell/utils/object';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import ArrayListGrouped from '@shell/components/form/ArrayListGrouped';
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { HARVESTER_NAME as VIRTUAL } from '@shell/config/features';
|
||||
import Match from './Match';
|
||||
|
||||
const FLOW_LOGGING = 'Logging';
|
||||
const FLOW_AUDIT = 'Audit';
|
||||
const FLOW_EVENT = 'Event';
|
||||
const FLOW_TYPE = [FLOW_LOGGING, FLOW_AUDIT, FLOW_EVENT];
|
||||
|
||||
function emptyMatch(include = true) {
|
||||
const rule = {
|
||||
select: !!include,
|
||||
|
|
@ -53,14 +61,17 @@ export default {
|
|||
inheritAttrs: false,
|
||||
|
||||
async fetch() {
|
||||
const hasAccessToClusterOutputs = this.$store.getters[`cluster/schemaFor`](LOGGING.CLUSTER_OUTPUT);
|
||||
const hasAccessToOutputs = this.$store.getters[`cluster/schemaFor`](LOGGING.OUTPUT);
|
||||
const currentCluster = this.$store.getters['currentCluster'];
|
||||
const inStore = currentCluster.isHarvester ? VIRTUAL : 'cluster';
|
||||
const hasAccessToClusterOutputs = this.$store.getters[`${ inStore }/schemaFor`](LOGGING.CLUSTER_OUTPUT);
|
||||
const hasAccessToOutputs = this.$store.getters[`${ inStore }/schemaFor`](LOGGING.OUTPUT);
|
||||
const hasAccessToNamespaces = this.$store.getters[`cluster/schemaFor`](NAMESPACE);
|
||||
const hasAccessToNodes = this.$store.getters[`cluster/schemaFor`](NODE);
|
||||
const hasAccessToNodes = this.$store.getters[`${ inStore }/schemaFor`](NODE);
|
||||
const hasAccessToPods = this.$store.getters[`${ inStore }/schemaFor`](POD);
|
||||
const isFlow = this.value.type === LOGGING.FLOW;
|
||||
|
||||
const getAllOrDefault = (type, hasAccess) => {
|
||||
return hasAccess ? this.$store.dispatch('cluster/findAll', { type }) : Promise.resolve([]);
|
||||
return hasAccess ? this.$store.dispatch(`${ inStore }/findAll`, { type }) : Promise.resolve([]);
|
||||
};
|
||||
|
||||
const hash = await allHash({
|
||||
|
|
@ -68,6 +79,7 @@ export default {
|
|||
allClusterOutputs: getAllOrDefault(LOGGING.CLUSTER_OUTPUT, hasAccessToClusterOutputs),
|
||||
allNamespaces: getAllOrDefault(NAMESPACE, hasAccessToNamespaces),
|
||||
allNodes: getAllOrDefault(NODE, hasAccessToNodes),
|
||||
allPods: getAllOrDefault(POD, hasAccessToPods),
|
||||
});
|
||||
|
||||
for ( const k of Object.keys(hash) ) {
|
||||
|
|
@ -76,7 +88,9 @@ export default {
|
|||
},
|
||||
|
||||
data() {
|
||||
const schemas = this.$store.getters['cluster/all'](SCHEMA);
|
||||
const currentCluster = this.$store.getters['currentCluster'];
|
||||
const inStore = currentCluster.isHarvester ? VIRTUAL : 'cluster';
|
||||
const schemas = this.$store.getters[`${ inStore }/all`](SCHEMA);
|
||||
let filtersYaml;
|
||||
|
||||
this.value.spec = this.value.spec || {};
|
||||
|
|
@ -124,7 +138,8 @@ export default {
|
|||
filtersYaml,
|
||||
initialFiltersYaml: filtersYaml,
|
||||
globalOutputRefs,
|
||||
localOutputRefs
|
||||
localOutputRefs,
|
||||
loggingType: clone(this.value.loggingType || FLOW_LOGGING)
|
||||
};
|
||||
},
|
||||
|
||||
|
|
@ -150,7 +165,17 @@ export default {
|
|||
return true;
|
||||
}
|
||||
|
||||
return output.namespace === this.value.namespace;
|
||||
const isEqualNs = output.namespace === this.value.namespace;
|
||||
|
||||
if (!this.isHarvester) {
|
||||
return isEqualNs;
|
||||
}
|
||||
|
||||
if (this.loggingType === FLOW_AUDIT) {
|
||||
return output.loggingType === FLOW_AUDIT && isEqualNs;
|
||||
}
|
||||
|
||||
return output.loggingType !== FLOW_AUDIT && isEqualNs;
|
||||
}).map((x) => {
|
||||
return { label: x.metadata.name, value: x.metadata.name };
|
||||
});
|
||||
|
|
@ -165,7 +190,17 @@ export default {
|
|||
|
||||
return this.allClusterOutputs
|
||||
.filter((clusterOutput) => {
|
||||
return clusterOutput.namespace === 'cattle-logging-system';
|
||||
const isEqualNs = clusterOutput.namespace === 'cattle-logging-system';
|
||||
|
||||
if (!this.isHarvester) {
|
||||
return isEqualNs;
|
||||
}
|
||||
|
||||
if (this.loggingType === FLOW_AUDIT) {
|
||||
return clusterOutput.loggingType === FLOW_AUDIT && isEqualNs;
|
||||
}
|
||||
|
||||
return clusterOutput.loggingType !== FLOW_AUDIT && isEqualNs;
|
||||
})
|
||||
.map((clusterOutput) => {
|
||||
return { label: clusterOutput.metadata.name, value: clusterOutput.metadata.name };
|
||||
|
|
@ -204,6 +239,25 @@ export default {
|
|||
return out;
|
||||
},
|
||||
|
||||
containerChoices() {
|
||||
const out = [];
|
||||
|
||||
for ( const pod of this.allPods ) {
|
||||
for ( const c of (pod.spec?.containers || []) ) {
|
||||
out.push(c.name);
|
||||
}
|
||||
}
|
||||
|
||||
return uniq(out).sort();
|
||||
},
|
||||
|
||||
isHarvester() {
|
||||
return this.$store.getters['currentProduct'].inStore === VIRTUAL;
|
||||
},
|
||||
|
||||
flowTypeOptions() {
|
||||
return FLOW_TYPE;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
|
@ -312,6 +366,20 @@ export default {
|
|||
if (this.value.spec.match && this.isMatchEmpty(this.value.spec.match)) {
|
||||
delete this.value.spec['match'];
|
||||
}
|
||||
|
||||
if (this.loggingType === FLOW_AUDIT) {
|
||||
this.value.spec['loggingRef'] = 'harvester-kube-audit-log-ref';
|
||||
}
|
||||
|
||||
if (this.loggingType === FLOW_EVENT) {
|
||||
const eventSelector = { select: { labels: { 'app.kubernetes.io/name': 'event-tailer' } } };
|
||||
|
||||
if (!this.value.spec.match) {
|
||||
this.value.spec['match'] = [eventSelector];
|
||||
} else {
|
||||
this.value.spec.match.push(eventSelector);
|
||||
}
|
||||
}
|
||||
},
|
||||
onYamlEditorReady(cm) {
|
||||
cm.getMode().fold = 'yamlcomments';
|
||||
|
|
@ -359,10 +427,21 @@ export default {
|
|||
:weight="3"
|
||||
>
|
||||
<Banner
|
||||
v-if="!isHarvester"
|
||||
color="info"
|
||||
class="mt-0"
|
||||
:label="t('logging.flow.matches.banner')"
|
||||
/>
|
||||
<div v-if="isHarvester">
|
||||
<LabeledSelect
|
||||
v-model:value="loggingType"
|
||||
class="mb-20"
|
||||
:options="flowTypeOptions"
|
||||
:mode="mode"
|
||||
:disabled="!isCreate"
|
||||
:label="t('generic.type')"
|
||||
/>
|
||||
</div>
|
||||
<ArrayListGrouped
|
||||
v-model:value="matches"
|
||||
:add-label="t('ingress.rules.addRule')"
|
||||
|
|
|
|||
|
|
@ -199,13 +199,13 @@ export default {
|
|||
v-if="hasMultipleProvidersSelected"
|
||||
color="info"
|
||||
>
|
||||
This output is configured with multiple providers. We currently only support a single provider per output. You can view or edit the YAML.
|
||||
{{ t('logging.output.tips.singleProvider') }}
|
||||
</Banner>
|
||||
<Banner
|
||||
v-else-if="!value.allProvidersSupported"
|
||||
color="info"
|
||||
>
|
||||
This output is configured with providers we don't support yet. You can view or edit the YAML.
|
||||
{{ t('logging.output.tips.multipleProviders') }}
|
||||
</Banner>
|
||||
<Tabbed
|
||||
v-else
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['currentCluster']),
|
||||
...mapGetters(['currentCluster', 'isStandaloneHarvester']),
|
||||
|
||||
canViewMembers() {
|
||||
return canViewProjectMembershipEditor(this.$store);
|
||||
|
|
@ -222,7 +222,7 @@ export default {
|
|||
<ResourceQuota
|
||||
:value="value"
|
||||
:mode="canEditTabElements"
|
||||
:types="isHarvester ? HARVESTER_TYPES : RANCHER_TYPES"
|
||||
:types="isStandaloneHarvester ? HARVESTER_TYPES : RANCHER_TYPES"
|
||||
@remove="removeQuota"
|
||||
/>
|
||||
</Tab>
|
||||
|
|
|
|||
|
|
@ -15,9 +15,9 @@ import MoveModal from '@shell/components/MoveModal';
|
|||
import ResourceQuota from '@shell/components/form/ResourceQuota/Namespace';
|
||||
import Loading from '@shell/components/Loading';
|
||||
import { HARVESTER_TYPES, RANCHER_TYPES } from '@shell/components/form/ResourceQuota/shared';
|
||||
import { HARVESTER_NAME as HARVESTER } from '@shell/config/features';
|
||||
import Labels from '@shell/components/form/Labels';
|
||||
import { randomStr } from '@shell/utils/string';
|
||||
import { HARVESTER_NAME as HARVESTER } from '@shell/config/features';
|
||||
|
||||
export default {
|
||||
emits: ['input'],
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ describe('component: DirectoryConfig', () => {
|
|||
expect(wrapper.vm.value.k8sDistro).toStrictEqual(k8sDistroValue);
|
||||
});
|
||||
|
||||
it('should render the component with configuration being an empty object, without errors and radio be of value DATA_DIR_RADIO_OPTIONS.DEFAULT (edit scenario)', () => {
|
||||
it('should render the component with configuration being an empty object, without errors and radio be of value DATA_DIR_RADIO_OPTIONS.CUSTOM (edit scenario)', () => {
|
||||
const newMountOptions = clone(mountOptions);
|
||||
|
||||
newMountOptions.propsData.value = {};
|
||||
|
|
@ -131,17 +131,21 @@ describe('component: DirectoryConfig', () => {
|
|||
const k8sDistroInput = wrapper.find('[data-testid="rke2-directory-config-k8sDistro-data-dir"]');
|
||||
|
||||
expect(title.exists()).toBe(true);
|
||||
expect(radioInput.exists()).toBe(true);
|
||||
expect(radioInput.isVisible()).toBe(false);
|
||||
|
||||
expect(wrapper.vm.dataConfigRadioValue).toBe(DATA_DIR_RADIO_OPTIONS.DEFAULT);
|
||||
expect(wrapper.vm.dataConfigRadioValue).toBe(DATA_DIR_RADIO_OPTIONS.CUSTOM);
|
||||
|
||||
// since we have all of the vars empty, then the inputs should not be there
|
||||
expect(systemAgentInput.exists()).toBe(false);
|
||||
expect(provisioningInput.exists()).toBe(false);
|
||||
expect(k8sDistroInput.exists()).toBe(false);
|
||||
expect(systemAgentInput.exists()).toBe(true);
|
||||
expect(provisioningInput.exists()).toBe(true);
|
||||
expect(k8sDistroInput.exists()).toBe(true);
|
||||
|
||||
expect(systemAgentInput.attributes().disabled).toBeDefined();
|
||||
expect(provisioningInput.attributes().disabled).toBeDefined();
|
||||
expect(k8sDistroInput.attributes().disabled).toBeDefined();
|
||||
});
|
||||
|
||||
it('radio input should be set to DATA_DIR_RADIO_OPTIONS.CUSTOM with all data dir values existing and different (edit scenario)', async() => {
|
||||
it('radio input should be set to DATA_DIR_RADIO_OPTIONS.CUSTOM with all data dir values existing and different (edit scenario)', () => {
|
||||
const newMountOptions = clone(mountOptions);
|
||||
const inputPath = 'some-data-dir';
|
||||
|
||||
|
|
@ -157,14 +161,20 @@ describe('component: DirectoryConfig', () => {
|
|||
|
||||
expect(wrapper.vm.dataConfigRadioValue).toBe(DATA_DIR_RADIO_OPTIONS.CUSTOM);
|
||||
|
||||
const radioInput = wrapper.find('[data-testid="rke2-directory-config-radio-input"]');
|
||||
const systemAgentInput = wrapper.find('[data-testid="rke2-directory-config-systemAgent-data-dir"]');
|
||||
const provisioningInput = wrapper.find('[data-testid="rke2-directory-config-provisioning-data-dir"]');
|
||||
const k8sDistroInput = wrapper.find('[data-testid="rke2-directory-config-k8sDistro-data-dir"]');
|
||||
|
||||
expect(radioInput.isVisible()).toBe(false);
|
||||
expect(systemAgentInput.isVisible()).toBe(true);
|
||||
expect(provisioningInput.isVisible()).toBe(true);
|
||||
expect(k8sDistroInput.isVisible()).toBe(true);
|
||||
|
||||
expect(systemAgentInput.attributes().disabled).toBeDefined();
|
||||
expect(provisioningInput.attributes().disabled).toBeDefined();
|
||||
expect(k8sDistroInput.attributes().disabled).toBeDefined();
|
||||
|
||||
expect(wrapper.vm.value.systemAgent).toStrictEqual(`${ inputPath }/${ DEFAULT_SUBDIRS.AGENT }`);
|
||||
expect(wrapper.vm.value.provisioning).toStrictEqual(`${ inputPath }/${ DEFAULT_SUBDIRS.PROVISIONING }`);
|
||||
expect(wrapper.vm.value.k8sDistro).toStrictEqual(`${ inputPath }/${ DEFAULT_SUBDIRS.K8S_DISTRO_K3S }`);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
|
||||
<script>
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import { _CREATE } from '@shell/config/query-params';
|
||||
import { _CREATE, _EDIT } from '@shell/config/query-params';
|
||||
import RadioGroup from '@components/Form/Radio/RadioGroup.vue';
|
||||
import { Banner } from '@components/Banner';
|
||||
|
||||
export const DATA_DIR_RADIO_OPTIONS = {
|
||||
DEFAULT: 'defaultDataDir',
|
||||
|
|
@ -23,7 +24,8 @@ export default {
|
|||
name: 'DirectoryConfig',
|
||||
components: {
|
||||
LabeledInput,
|
||||
RadioGroup
|
||||
RadioGroup,
|
||||
Banner
|
||||
},
|
||||
props: {
|
||||
mode: {
|
||||
|
|
@ -50,9 +52,7 @@ export default {
|
|||
}
|
||||
|
||||
if (this.mode !== _CREATE) {
|
||||
if (this.value?.systemAgent?.length || this.value?.provisioning?.length || this.value?.k8sDistro?.length) {
|
||||
dataConfigRadioValue = DATA_DIR_RADIO_OPTIONS.CUSTOM;
|
||||
}
|
||||
dataConfigRadioValue = DATA_DIR_RADIO_OPTIONS.CUSTOM;
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -85,6 +85,9 @@ export default {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
isDisabled() {
|
||||
return this.mode === _EDIT;
|
||||
},
|
||||
dataConfigRadioOptions() {
|
||||
const defaultDataDirOption = {
|
||||
value: DATA_DIR_RADIO_OPTIONS.DEFAULT,
|
||||
|
|
@ -148,10 +151,17 @@ export default {
|
|||
<template>
|
||||
<div class="row">
|
||||
<div class="col span-8">
|
||||
<h3 class="mb-20">
|
||||
<h3>
|
||||
{{ t('cluster.directoryConfig.title') }}
|
||||
</h3>
|
||||
<Banner
|
||||
class="mb-20"
|
||||
:closable="false"
|
||||
color="info"
|
||||
label-key="cluster.directoryConfig.banner"
|
||||
/>
|
||||
<RadioGroup
|
||||
v-show="!isDisabled"
|
||||
:value="dataConfigRadioValue"
|
||||
class="mb-10"
|
||||
:mode="mode"
|
||||
|
|
@ -167,6 +177,7 @@ export default {
|
|||
:mode="mode"
|
||||
:label="t('cluster.directoryConfig.common.label')"
|
||||
:tooltip="t('cluster.directoryConfig.common.tooltip')"
|
||||
:disabled="isDisabled"
|
||||
data-testid="rke2-directory-config-common-data-dir"
|
||||
/>
|
||||
<div v-if="dataConfigRadioValue === DATA_DIR_RADIO_OPTIONS.CUSTOM">
|
||||
|
|
@ -176,6 +187,7 @@ export default {
|
|||
:mode="mode"
|
||||
:label="t('cluster.directoryConfig.systemAgent.label')"
|
||||
:tooltip="t('cluster.directoryConfig.systemAgent.tooltip')"
|
||||
:disabled="isDisabled"
|
||||
data-testid="rke2-directory-config-systemAgent-data-dir"
|
||||
/>
|
||||
<LabeledInput
|
||||
|
|
@ -184,6 +196,7 @@ export default {
|
|||
:mode="mode"
|
||||
:label="t('cluster.directoryConfig.provisioning.label')"
|
||||
:tooltip="t('cluster.directoryConfig.provisioning.tooltip')"
|
||||
:disabled="isDisabled"
|
||||
data-testid="rke2-directory-config-provisioning-data-dir"
|
||||
/>
|
||||
<LabeledInput
|
||||
|
|
@ -192,6 +205,7 @@ export default {
|
|||
:mode="mode"
|
||||
:label="t('cluster.directoryConfig.k8sDistro.label')"
|
||||
:tooltip="t('cluster.directoryConfig.k8sDistro.tooltip')"
|
||||
:disabled="isDisabled"
|
||||
data-testid="rke2-directory-config-k8sDistro-data-dir"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,244 @@
|
|||
<script>
|
||||
import BrandImage from '@shell/components/BrandImage';
|
||||
import TypeDescription from '@shell/components/TypeDescription';
|
||||
import ResourceTable from '@shell/components/ResourceTable';
|
||||
import Masthead from '@shell/components/ResourceList/Masthead';
|
||||
import Loading from '@shell/components/Loading';
|
||||
import { HARVESTER_NAME as VIRTUAL } from '@shell/config/features';
|
||||
import { CAPI, HCI, MANAGEMENT } from '@shell/config/types';
|
||||
import { isHarvesterCluster } from '@shell/utils/cluster';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BrandImage,
|
||||
ResourceTable,
|
||||
Masthead,
|
||||
TypeDescription,
|
||||
Loading
|
||||
},
|
||||
|
||||
props: {
|
||||
schema: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
useQueryParamsForSimpleFiltering: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
const hash = await allHash({
|
||||
hciClusters: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER }),
|
||||
mgmtClusters: this.$store.dispatch(`${ inStore }/findAll`, { type: MANAGEMENT.CLUSTER })
|
||||
});
|
||||
|
||||
this.hciClusters = hash.hciClusters;
|
||||
this.mgmtClusters = hash.mgmtClusters;
|
||||
},
|
||||
|
||||
data() {
|
||||
const resource = CAPI.RANCHER_CLUSTER;
|
||||
|
||||
return {
|
||||
navigating: false,
|
||||
VIRTUAL,
|
||||
hciDashboard: HCI.DASHBOARD,
|
||||
resource,
|
||||
hResource: HCI.CLUSTER,
|
||||
hciClusters: [],
|
||||
mgmtClusters: []
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
realSchema() {
|
||||
return this.$store.getters['management/schemaFor'](CAPI.RANCHER_CLUSTER);
|
||||
},
|
||||
|
||||
importLocation() {
|
||||
return {
|
||||
name: 'c-cluster-product-resource-create',
|
||||
params: {
|
||||
product: this.$store.getters['currentProduct'].name,
|
||||
resource: this.schema.id,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
canCreateCluster() {
|
||||
const schema = this.$store.getters['management/schemaFor'](CAPI.RANCHER_CLUSTER);
|
||||
|
||||
return !!schema?.collectionMethods.find((x) => x.toLowerCase() === 'post');
|
||||
},
|
||||
|
||||
rows() {
|
||||
return this.hciClusters.filter((c) => {
|
||||
const cluster = this.mgmtClusters.find((cluster) => cluster?.metadata?.name === c?.status?.clusterName);
|
||||
|
||||
return isHarvesterCluster(cluster);
|
||||
});
|
||||
},
|
||||
|
||||
typeDisplay() {
|
||||
return this.t(`typeLabel."${ HCI.CLUSTER }"`, { count: this.row?.length || 0 });
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async goToCluster(row) {
|
||||
const timeout = setTimeout(() => {
|
||||
// Don't show loading indicator for quickly fetched plugins
|
||||
this.navigating = row.id;
|
||||
}, 1000);
|
||||
|
||||
try {
|
||||
await row.goToCluster();
|
||||
|
||||
clearTimeout(timeout);
|
||||
this.navigating = false;
|
||||
} catch {
|
||||
// The error handling is carried out within goToCluster, but just in case something happens before the promise chain can catch it...
|
||||
clearTimeout(timeout);
|
||||
this.navigating = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Loading v-if="$fetchState.pending" />
|
||||
<div v-else>
|
||||
<Masthead
|
||||
:schema="realSchema"
|
||||
:resource="resource"
|
||||
:is-creatable="false"
|
||||
:type-display="typeDisplay"
|
||||
>
|
||||
<template #typeDescription>
|
||||
<TypeDescription :resource="hResource" />
|
||||
</template>
|
||||
|
||||
<template
|
||||
v-if="canCreateCluster"
|
||||
slot="extraActions"
|
||||
>
|
||||
<n-link
|
||||
:to="importLocation"
|
||||
class="btn role-primary"
|
||||
>
|
||||
{{ t('cluster.importAction') }}
|
||||
</n-link>
|
||||
</template>
|
||||
</Masthead>
|
||||
|
||||
<ResourceTable
|
||||
v-if="rows && rows.length"
|
||||
:schema="schema"
|
||||
:rows="rows"
|
||||
:is-creatable="true"
|
||||
:namespaced="false"
|
||||
:use-query-params-for-simple-filtering="useQueryParamsForSimpleFiltering"
|
||||
>
|
||||
<template #col:name="{row}">
|
||||
<td>
|
||||
<span class="cluster-link">
|
||||
<a
|
||||
v-if="row.isReady"
|
||||
class="link"
|
||||
:disabled="navigating"
|
||||
@click="goToCluster(row)"
|
||||
>{{ row.nameDisplay }}</a>
|
||||
<span v-else>
|
||||
{{ row.nameDisplay }}
|
||||
</span>
|
||||
<i
|
||||
class="icon icon-spinner icon-spin ml-5"
|
||||
:class="{'navigating': navigating === row.id}"
|
||||
/>
|
||||
</span>
|
||||
</td>
|
||||
</template>
|
||||
|
||||
<template #cell:harvester="{row}">
|
||||
<n-link
|
||||
class="btn btn-sm role-primary"
|
||||
:to="row.detailLocation"
|
||||
>
|
||||
{{ t('harvesterManager.manage') }}
|
||||
</n-link>
|
||||
</template>
|
||||
</ResourceTable>
|
||||
<div v-else>
|
||||
<div class="no-clusters">
|
||||
{{ t('harvesterManager.cluster.none') }}
|
||||
</div>
|
||||
<hr class="info-section">
|
||||
<div class="logo">
|
||||
<BrandImage
|
||||
file-name="harvester.png"
|
||||
height="64"
|
||||
/>
|
||||
</div>
|
||||
<div class="tagline">
|
||||
<div>{{ t('harvesterManager.cluster.description') }}</div>
|
||||
</div>
|
||||
<div class="tagline sub-tagline">
|
||||
<div v-clean-html="t('harvesterManager.cluster.learnMore', {}, true)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cluster-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.icon {
|
||||
// Use visibility to avoid the columns re-adjusting when the icon is shown
|
||||
visibility: hidden;
|
||||
|
||||
&.navigating {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.no-clusters {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.info-section {
|
||||
margin-top: 60px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 60px 0 40px 0;
|
||||
}
|
||||
|
||||
.tagline {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 30px;
|
||||
|
||||
> div {
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
max-width: 80%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { NS_SNAPSHOT_QUOTA } from '../config/table-headers';
|
||||
import ResourceTable from '@shell/components/ResourceTable';
|
||||
|
||||
import { HCI } from '@shell/config/types';
|
||||
export default {
|
||||
name: 'ListNamespace',
|
||||
components: { ResourceTable },
|
||||
|
|
@ -27,13 +28,23 @@ export default {
|
|||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return { asddsa: true };
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters(['currentProduct']),
|
||||
hasHarvesterResourceQuotaSchema() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.RESOURCE_QUOTA);
|
||||
},
|
||||
headers() {
|
||||
const headersFromSchema = this.$store.getters['type-map/headersFor'](this.schema);
|
||||
|
||||
if (this.hasHarvesterResourceQuotaSchema) {
|
||||
headersFromSchema.splice(2, 0, NS_SNAPSHOT_QUOTA);
|
||||
}
|
||||
|
||||
return headersFromSchema;
|
||||
},
|
||||
filterRow() {
|
||||
if (this.currentProduct.hideSystemResources) {
|
||||
return this.rows.filter( (N) => {
|
||||
|
|
@ -56,6 +67,7 @@ export default {
|
|||
v-bind="$attrs"
|
||||
:rows="filterRow"
|
||||
:groupable="false"
|
||||
:headers="headers"
|
||||
:schema="schema"
|
||||
key-field="_key"
|
||||
:loading="loading"
|
||||
|
|
|
|||
|
|
@ -30,7 +30,9 @@ export default class FleetBundle extends SteveModel {
|
|||
}
|
||||
|
||||
get repoName() {
|
||||
return this.metadata.labels['fleet.cattle.io/repo-name'];
|
||||
const labels = this.metadata?.labels || {};
|
||||
|
||||
return labels['fleet.cattle.io/repo-name'];
|
||||
}
|
||||
|
||||
get targetClusters() {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
import { convert, matching, convertSelectorObj } from '@shell/utils/selector';
|
||||
import jsyaml from 'js-yaml';
|
||||
import { escapeHtml, randomStr } from '@shell/utils/string';
|
||||
import { escapeHtml } from '@shell/utils/string';
|
||||
import { FLEET } from '@shell/config/types';
|
||||
import { FLEET as FLEET_ANNOTATIONS } from '@shell/config/labels-annotations';
|
||||
import { addObject, addObjects, findBy, insertAt } from '@shell/utils/array';
|
||||
import { set } from '@shell/utils/object';
|
||||
import SteveModel from '@shell/plugins/steve/steve-class';
|
||||
import {
|
||||
STATES_ENUM, colorForState, mapStateToEnum, primaryDisplayStatusFromCount, stateDisplay, stateSort
|
||||
colorForState, mapStateToEnum, primaryDisplayStatusFromCount, stateDisplay, stateSort
|
||||
} from '@shell/plugins/dashboard-store/resource-class';
|
||||
import { NAME } from '@shell/config/product/explorer';
|
||||
import FleetUtils from '@shell/utils/fleet';
|
||||
|
||||
function quacksLikeAHash(str) {
|
||||
if (str.match(/^[a-f0-9]{40,}$/i)) {
|
||||
|
|
@ -325,35 +326,24 @@ export default class GitRepo extends SteveModel {
|
|||
}
|
||||
|
||||
get resourcesStatuses() {
|
||||
const clusters = this.targetClusters || [];
|
||||
const resources = this.status?.resources || [];
|
||||
const conditions = this.status?.conditions || [];
|
||||
const bundleDeployments = this.bundleDeployments || [];
|
||||
const clusters = (this.targetClusters || []).reduce((res, c) => {
|
||||
res[c.id] = c;
|
||||
|
||||
return res;
|
||||
}, {});
|
||||
|
||||
const out = [];
|
||||
|
||||
for (const c of clusters) {
|
||||
const clusterBundleDeploymentResources = this.bundleDeployments
|
||||
.find((bd) => bd.metadata?.labels?.[FLEET_ANNOTATIONS.CLUSTER] === c.metadata.name)
|
||||
?.status?.resources || [];
|
||||
for (const bd of bundleDeployments) {
|
||||
const clusterId = FleetUtils.clusterIdFromBundleDeploymentLabels(bd.metadata?.labels);
|
||||
const c = clusters[clusterId];
|
||||
const resources = FleetUtils.resourcesFromBundleDeploymentStatus(bd.status);
|
||||
|
||||
resources.forEach((r, i) => {
|
||||
let namespacedName = r.name;
|
||||
|
||||
if (r.namespace) {
|
||||
namespacedName = `${ r.namespace }:${ r.name }`;
|
||||
}
|
||||
|
||||
let state = r.state;
|
||||
const perEntry = r.perClusterState?.find((x) => x.clusterId === c.id);
|
||||
const tooMany = r.perClusterState?.length >= 10 || false;
|
||||
|
||||
if (perEntry) {
|
||||
state = perEntry.state;
|
||||
} else if (tooMany) {
|
||||
state = STATES_ENUM.UNKNOWN;
|
||||
} else {
|
||||
state = STATES_ENUM.READY;
|
||||
}
|
||||
resources.forEach((r) => {
|
||||
const id = FleetUtils.resourceId(r);
|
||||
const type = FleetUtils.resourceType(r);
|
||||
const state = r.state;
|
||||
|
||||
const color = colorForState(state).replace('text-', 'bg-');
|
||||
const display = stateDisplay(state);
|
||||
|
|
@ -363,33 +353,38 @@ export default class GitRepo extends SteveModel {
|
|||
params: {
|
||||
product: NAME,
|
||||
cluster: c.metadata.labels[FLEET_ANNOTATIONS.CLUSTER_NAME],
|
||||
resource: r.type,
|
||||
resource: type,
|
||||
namespace: r.namespace,
|
||||
id: r.name,
|
||||
}
|
||||
};
|
||||
|
||||
const key = `${ c.id }-${ type }-${ r.namespace }-${ r.name }`;
|
||||
|
||||
out.push({
|
||||
key: `${ r.id }-${ c.id }-${ r.type }-${ r.namespace }-${ r.name }`,
|
||||
tableKey: `${ r.id }-${ c.id }-${ r.type }-${ r.namespace }-${ r.name }-${ randomStr(8) }`,
|
||||
kind: r.kind,
|
||||
apiVersion: r.apiVersion,
|
||||
type: r.type,
|
||||
id: r.id,
|
||||
namespace: r.namespace,
|
||||
name: r.name,
|
||||
clusterId: c.id,
|
||||
clusterLabel: c.metadata.labels[FLEET_ANNOTATIONS.CLUSTER_NAME],
|
||||
clusterName: c.nameDisplay,
|
||||
state: mapStateToEnum(state),
|
||||
stateBackground: color,
|
||||
stateDisplay: display,
|
||||
stateSort: stateSort(color, display),
|
||||
namespacedName,
|
||||
key,
|
||||
tableKey: key,
|
||||
|
||||
// Needed?
|
||||
id,
|
||||
type,
|
||||
clusterId: c.id,
|
||||
|
||||
// columns, see FleetResources.vue
|
||||
state: mapStateToEnum(state),
|
||||
clusterName: c.nameDisplay,
|
||||
apiVersion: r.apiVersion,
|
||||
kind: r.kind,
|
||||
name: r.name,
|
||||
namespace: r.namespace,
|
||||
creationTimestamp: r.createdAt,
|
||||
|
||||
// other properties
|
||||
clusterLabel: c.metadata.labels[FLEET_ANNOTATIONS.CLUSTER_NAME],
|
||||
stateBackground: color,
|
||||
stateDisplay: display,
|
||||
stateSort: stateSort(color, display),
|
||||
detailLocation,
|
||||
conditions: conditions[i],
|
||||
bundleDeploymentStatus: clusterBundleDeploymentResources?.[i],
|
||||
creationTimestamp: clusterBundleDeploymentResources?.[i]?.createdAt
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -410,9 +405,7 @@ export default class GitRepo extends SteveModel {
|
|||
|
||||
get clusterResourceStatus() {
|
||||
const clusterStatuses = this.resourcesStatuses.reduce((prev, curr) => {
|
||||
const { clusterId, clusterLabel } = curr;
|
||||
|
||||
const state = curr.state;
|
||||
const { clusterId, clusterLabel, state } = curr;
|
||||
|
||||
if (!prev[clusterId]) {
|
||||
prev[clusterId] = {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
import SteveModel from '@shell/plugins/steve/steve-class';
|
||||
import { HCI } from '@shell/config/labels-annotations';
|
||||
|
||||
export default class NetworkAttachmentDef extends SteveModel {
|
||||
applyDefaults() {
|
||||
const spec = this.spec || {
|
||||
config: JSON.stringify({
|
||||
cniVersion: '0.3.1',
|
||||
name: '',
|
||||
type: 'bridge',
|
||||
bridge: '',
|
||||
promiscMode: true,
|
||||
vlan: '',
|
||||
ipam: {}
|
||||
})
|
||||
};
|
||||
|
||||
this['spec'] = spec;
|
||||
}
|
||||
|
||||
get parseConfig() {
|
||||
try {
|
||||
return JSON.parse(this.spec.config) || {};
|
||||
} catch (err) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
get isIpamStatic() {
|
||||
return this.parseConfig.ipam?.type === 'static';
|
||||
}
|
||||
|
||||
get clusterNetwork() {
|
||||
return this?.metadata?.labels?.[HCI.CLUSTER_NETWORK];
|
||||
}
|
||||
|
||||
get vlanType() {
|
||||
const labels = this.metadata?.labels || {};
|
||||
const type = labels[HCI.NETWORK_TYPE];
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
get vlanId() {
|
||||
return this.vlanType === 'UntaggedNetwork' ? 'N/A' : this.parseConfig.vlan;
|
||||
}
|
||||
|
||||
get customValidationRules() {
|
||||
const rules = [
|
||||
{
|
||||
nullable: false,
|
||||
path: 'metadata.name',
|
||||
required: true,
|
||||
minLength: 1,
|
||||
maxLength: 63,
|
||||
translationKey: 'harvester.fields.name'
|
||||
}
|
||||
];
|
||||
|
||||
return rules;
|
||||
}
|
||||
|
||||
get connectivity() {
|
||||
const annotations = this.metadata?.annotations || {};
|
||||
const route = annotations[HCI.NETWORK_ROUTE];
|
||||
let config = {};
|
||||
|
||||
if (this.vlanType === 'UntaggedNetwork') {
|
||||
return 'N/A';
|
||||
}
|
||||
|
||||
try {
|
||||
config = JSON.parse(route || '{}');
|
||||
} catch {
|
||||
return 'invalid';
|
||||
}
|
||||
|
||||
const connectivity = config.connectivity;
|
||||
|
||||
if (connectivity === 'false') {
|
||||
return 'inactive';
|
||||
} else if (connectivity === 'true') {
|
||||
return 'active';
|
||||
} else {
|
||||
return connectivity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,13 @@
|
|||
import { ALLOWED_SETTINGS } from '@shell/config/settings';
|
||||
import HybridModel from '@shell/plugins/steve/hybrid-class';
|
||||
import { isServerUrl } from '@shell/utils/validators/setting';
|
||||
import { HARVESTER_NAME as HARVESTER } from '@shell/config/features';
|
||||
import {
|
||||
_EDIT,
|
||||
_UNFLAG,
|
||||
AS,
|
||||
MODE
|
||||
} from '@shell/config/query-params';
|
||||
|
||||
export default class Setting extends HybridModel {
|
||||
get fromEnv() {
|
||||
|
|
@ -43,4 +50,22 @@ export default class Setting extends HybridModel {
|
|||
|
||||
return out;
|
||||
}
|
||||
|
||||
goToEdit(moreQuery = {}) {
|
||||
if (this.$rootGetters['currentProduct'].inStore === HARVESTER) {
|
||||
location.name = `${ HARVESTER }-c-cluster-brand`;
|
||||
location.params = { cluster: this.$rootGetters['currentCluster'].id, product: HARVESTER };
|
||||
|
||||
location.query = {
|
||||
...location.query,
|
||||
[MODE]: _EDIT,
|
||||
[AS]: _UNFLAG,
|
||||
...moreQuery
|
||||
};
|
||||
|
||||
this.currentRouter().push(location);
|
||||
} else {
|
||||
super.goToEdit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,16 +112,21 @@ export default class extends SteveModel {
|
|||
return this.patch(data, {}, true, true);
|
||||
}
|
||||
|
||||
setDefault() {
|
||||
const allStorageClasses = this.$rootGetters['cluster/all'](STORAGE_CLASS) || [];
|
||||
async setDefault() {
|
||||
const inStore = this.$rootGetters['currentProduct'].inStore;
|
||||
const allStorageClasses = this.$rootGetters[`${ inStore }/all`](STORAGE_CLASS) || [];
|
||||
|
||||
for (const storageClass of allStorageClasses) {
|
||||
await storageClass.resetDefault();
|
||||
}
|
||||
|
||||
allStorageClasses.forEach((storageClass) => storageClass.resetDefault());
|
||||
this.updateDefault(true);
|
||||
}
|
||||
|
||||
resetDefault() {
|
||||
async resetDefault() {
|
||||
if (this.isDefault) {
|
||||
this.updateDefault(false);
|
||||
await this.updateDefault(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -146,4 +151,10 @@ export default class extends SteveModel {
|
|||
|
||||
return out;
|
||||
}
|
||||
|
||||
cleanForNew() {
|
||||
this.$dispatch(`cleanForNew`, this);
|
||||
|
||||
delete this?.metadata?.annotations?.[STORAGE.DEFAULT_STORAGE_CLASS];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@rancher/shell",
|
||||
"version": "3.0.0",
|
||||
"version": "3.0.1-rc.1",
|
||||
"description": "Rancher Dashboard Shell",
|
||||
"repository": "https://github.com/rancherlabs/dashboard",
|
||||
"license": "Apache-2.0",
|
||||
|
|
@ -36,7 +36,6 @@
|
|||
"@babel/plugin-proposal-private-property-in-object": "7.14.5",
|
||||
"@babel/preset-typescript": "7.16.7",
|
||||
"@novnc/novnc": "1.2.0",
|
||||
"@nuxtjs/axios": "5.13.6",
|
||||
"@popperjs/core": "2.4.4",
|
||||
"@rancher/icons": "2.0.29",
|
||||
"@types/is-url": "1.2.30",
|
||||
|
|
@ -51,6 +50,8 @@
|
|||
"@vue/vue3-jest": "^27.0.0-alpha.1",
|
||||
"add": "2.0.6",
|
||||
"ansi_up": "5.0.0",
|
||||
"axios": "0.21.4",
|
||||
"axios-retry": "3.1.9",
|
||||
"babel-eslint": "10.1.0",
|
||||
"babel-plugin-module-resolver": "4.0.0",
|
||||
"babel-preset-vue": "2.0.2",
|
||||
|
|
@ -73,6 +74,7 @@
|
|||
"d3-selection": "1.4.1",
|
||||
"dagre-d3": "0.6.4",
|
||||
"dayjs": "1.8.29",
|
||||
"defu": "5.0.1",
|
||||
"diff2html": "3.4.24",
|
||||
"dompurify": "2.5.4",
|
||||
"element-matches": "^0.1.2",
|
||||
|
|
@ -160,7 +162,7 @@
|
|||
"semver": "7.5.4",
|
||||
"@types/lodash": "4.17.5",
|
||||
"@types/node": "~20.10.0",
|
||||
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
|
||||
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
|
||||
},
|
||||
"nyc": {
|
||||
"extension": [
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ export default {
|
|||
type: MANAGEMENT.FEATURE, id: 'multi-cluster-management', opt: { url: `/v1/${ MANAGEMENT.FEATURE }/multi-cluster-management` }
|
||||
});
|
||||
|
||||
const mcmEnabled = mcmFeature?.spec?.value || mcmFeature?.status?.default;
|
||||
const mcmEnabled = (mcmFeature?.spec?.value || mcmFeature?.status?.default) && productName !== 'Harvester';
|
||||
|
||||
let serverUrl;
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ export default {
|
|||
allBundles: {
|
||||
inStoreType: 'management',
|
||||
type: FLEET.BUNDLE,
|
||||
opt: { excludeFields: ['metadata.managedFields', 'spec.resources'] },
|
||||
},
|
||||
gitRepos: {
|
||||
inStoreType: 'management',
|
||||
|
|
|
|||
|
|
@ -141,6 +141,9 @@ export default {
|
|||
const schema = this.$store.getters[`management/schemaFor`](MANAGEMENT.SETTING);
|
||||
|
||||
return schema?.resourceMethods?.includes('PUT') ? _EDIT : _VIEW;
|
||||
},
|
||||
customLinkColor() {
|
||||
return { color: this.uiLinkColor };
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -564,7 +567,7 @@ export default {
|
|||
component-testid="link"
|
||||
/>
|
||||
<span class="col link-example">
|
||||
<a>
|
||||
<a :style="customLinkColor">
|
||||
{{ t('branding.linkColor.example') }}
|
||||
</a>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
AS,
|
||||
MODE
|
||||
} from '@shell/config/query-params';
|
||||
import { VIEW_IN_API } from '@shell/store/prefs';
|
||||
import { VIEW_IN_API, DEV } from '@shell/store/prefs';
|
||||
import { addObject, addObjects, findBy, removeAt } from '@shell/utils/array';
|
||||
import CustomValidators from '@shell/utils/custom-validators';
|
||||
import { downloadFile, generateZip } from '@shell/utils/download';
|
||||
|
|
@ -84,6 +84,7 @@ export const STATES_ENUM = {
|
|||
DISCONNECTED: 'disconnected',
|
||||
DRAINED: 'drained',
|
||||
DRAINING: 'draining',
|
||||
ENABLED: 'enabled',
|
||||
ERR_APPLIED: 'errapplied',
|
||||
ERROR: 'error',
|
||||
ERRORING: 'erroring',
|
||||
|
|
@ -232,6 +233,9 @@ export const STATES = {
|
|||
[STATES_ENUM.DRAINING]: {
|
||||
color: 'warning', icon: 'tag', label: 'Draining', compoundIcon: 'warning'
|
||||
},
|
||||
[STATES_ENUM.ENABLED]: {
|
||||
color: 'success', icon: 'dot-open', label: 'Enabled', compoundIcon: 'checkmark'
|
||||
},
|
||||
[STATES_ENUM.ERR_APPLIED]: {
|
||||
color: 'error', icon: 'error', label: 'Error Applied', compoundIcon: 'error'
|
||||
},
|
||||
|
|
@ -997,7 +1001,11 @@ export default class Resource {
|
|||
}
|
||||
|
||||
get canViewInApi() {
|
||||
return this.hasLink('self') && this.$rootGetters['prefs/get'](VIEW_IN_API);
|
||||
try {
|
||||
return this.hasLink('self') && this.$rootGetters['prefs/get'](VIEW_IN_API);
|
||||
} catch {
|
||||
return this.hasLink('self') && this.$rootGetters['prefs/get'](DEV);
|
||||
}
|
||||
}
|
||||
|
||||
get canYaml() {
|
||||
|
|
@ -1055,7 +1063,7 @@ export default class Resource {
|
|||
|
||||
async doActionGrowl(actionName, body, opt = {}) {
|
||||
try {
|
||||
await this.$dispatch('resourceAction', {
|
||||
return await this.$dispatch('resourceAction', {
|
||||
resource: this,
|
||||
actionName,
|
||||
body,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { uniq } from '@shell/utils/array';
|
|||
import {
|
||||
CONFIG_MAP, MANAGEMENT, NAMESPACE, NODE, POD
|
||||
} from '@shell/config/types';
|
||||
import { Schema } from 'plugins/steve/schema';
|
||||
import { Schema } from '@shell/plugins/steve/schema';
|
||||
|
||||
class NamespaceProjectFilters {
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -169,8 +169,9 @@ fi
|
|||
|
||||
# function to clone repos and install dependencies (including the newly published shell version)
|
||||
function clone_repo_test_extension_build() {
|
||||
REPO_NAME=$1
|
||||
PKG_NAME=$2
|
||||
REPO_ORG=$1
|
||||
REPO_NAME=$2
|
||||
PKG_NAME=$3
|
||||
|
||||
echo -e "\nSetting up $REPO_NAME repository locally\n"
|
||||
|
||||
|
|
@ -183,7 +184,7 @@ function clone_repo_test_extension_build() {
|
|||
fi
|
||||
|
||||
# cloning repo
|
||||
git clone https://github.com/rancher/$REPO_NAME.git
|
||||
git clone https://github.com/$REPO_ORG/$REPO_NAME.git
|
||||
pushd ${BASE_DIR}/$REPO_NAME
|
||||
|
||||
echo -e "\nInstalling dependencies for $REPO_NAME\n"
|
||||
|
|
@ -196,9 +197,6 @@ function clone_repo_test_extension_build() {
|
|||
sed -i.bak -e "s/\"\@rancher\/shell\": \"[0-9]*.[0-9]*.[0-9]*\",/\"\@rancher\/shell\": \"${SHELL_VERSION}\",/g" package.json
|
||||
rm package.json.bak
|
||||
|
||||
# we need to remove yarn.lock, otherwise it would install a version that we don't want
|
||||
rm yarn.lock
|
||||
|
||||
echo -e "\nInstalling newly built shell version\n"
|
||||
|
||||
# installing new version of shell
|
||||
|
|
@ -223,8 +221,9 @@ function clone_repo_test_extension_build() {
|
|||
|
||||
# Here we just add the extension that we want to include as a check (all our official extensions should be included here)
|
||||
# Don't forget to add the unit tests exception to clone_repo_test_extension_build function if a new extension has those
|
||||
# clone_repo_test_extension_build "kubewarden-ui" "kubewarden"
|
||||
# clone_repo_test_extension_build "elemental-ui" "elemental"
|
||||
# clone_repo_test_extension_build "capi-ui-extension" "capi"
|
||||
clone_repo_test_extension_build "rancher" "kubewarden-ui" "kubewarden"
|
||||
clone_repo_test_extension_build "rancher" "elemental-ui" "elemental"
|
||||
clone_repo_test_extension_build "neuvector" "manager-ext" "neuvector-ui-ext"
|
||||
# clone_repo_test_extension_build "rancher" "capi-ui-extension" "capi"
|
||||
|
||||
echo "All done"
|
||||
|
|
|
|||
|
|
@ -233,6 +233,14 @@ export const getters = {
|
|||
default:
|
||||
return { name: afterLoginRoutePref };
|
||||
}
|
||||
},
|
||||
|
||||
dev: (state, getters) => {
|
||||
try {
|
||||
return getters['get'](PLUGIN_DEVELOPER);
|
||||
} catch {
|
||||
return getters['get'](DEV);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
export interface BundleResourceKey {
|
||||
kind: string,
|
||||
apiVersion: string,
|
||||
namespace?: string,
|
||||
name: string,
|
||||
}
|
||||
|
||||
export interface BundleDeploymentResource extends BundleResourceKey {
|
||||
createdAt?: string,
|
||||
}
|
||||
|
||||
export interface BundleModifiedResource extends BundleResourceKey {
|
||||
missing?: boolean,
|
||||
delete?: boolean,
|
||||
patch: string,
|
||||
}
|
||||
|
||||
export interface BundleNonReadyResource extends BundleResourceKey {
|
||||
summary: { [state: string]: string }
|
||||
}
|
||||
|
||||
export interface BundleNonReadyBundle {
|
||||
modifiedStatus: BundleModifiedResource[],
|
||||
nonReadyStatus: BundleNonReadyResource[],
|
||||
}
|
||||
|
||||
export interface BundleDeploymentStatus {
|
||||
resources?: BundleDeploymentResource[],
|
||||
modifiedStatus?: BundleModifiedResource[],
|
||||
nonReadyStatus?: BundleNonReadyResource[],
|
||||
}
|
||||
|
||||
export interface BundleStatusSummary {
|
||||
nonReadyResources?: BundleNonReadyBundle[],
|
||||
}
|
||||
|
||||
export interface BundleStatus {
|
||||
resourceKey?: BundleResourceKey[],
|
||||
summary?: BundleStatusSummary,
|
||||
}
|
||||
|
|
@ -116,7 +116,7 @@ export const checkSchemasForFindAllHash = (types, store) => {
|
|||
const validSchema = value.schemaValidator ? value.schemaValidator(schema) : !!schema;
|
||||
|
||||
if (validSchema) {
|
||||
hash[key] = store.dispatch(`${ value.inStoreType }/findAll`, { type: value.type } );
|
||||
hash[key] = store.dispatch(`${ value.inStoreType }/findAll`, { type: value.type, opt: value.opt } );
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ export function createYaml(
|
|||
}
|
||||
|
||||
// ACTIVELY_REMOVE are fields that should be removed even if they are defined in data
|
||||
for ( const entry of ACTIVELY_REMOVE ) {
|
||||
for ( const entry of (dataOptions.activelyRemove || ACTIVELY_REMOVE) ) {
|
||||
const parts = entry.split(/\./);
|
||||
const key = parts[parts.length - 1];
|
||||
const prefix = parts.slice(0, -1).join('.');
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ export function setFavIcon(store) {
|
|||
brandImage = require('~shell/assets/brand/suse/favicon.png');
|
||||
} else if (brandSetting?.value === 'csp') {
|
||||
brandImage = require('~shell/assets/brand/csp/favicon.png');
|
||||
} else if (brandSetting?.value === 'harvester') {
|
||||
brandImage = require('~shell/assets/brand/harvester/favicon.png');
|
||||
}
|
||||
|
||||
link.href = res?.value || brandImage || defaultFavIcon;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,159 @@
|
|||
import {
|
||||
BundleDeploymentResource,
|
||||
BundleResourceKey,
|
||||
BundleDeploymentStatus,
|
||||
BundleStatus,
|
||||
} from '@shell/types/resources/fleet';
|
||||
import { STATES_ENUM } from '@shell/plugins/dashboard-store/resource-class';
|
||||
import { FLEET as FLEET_ANNOTATIONS } from '@shell/config/labels-annotations';
|
||||
|
||||
interface Resource extends BundleDeploymentResource {
|
||||
state: string,
|
||||
}
|
||||
|
||||
type Labels = {
|
||||
[key: string]: string,
|
||||
}
|
||||
|
||||
interface StatesCounter { [state: string]: number }
|
||||
|
||||
function incr(counter: StatesCounter, state: string) {
|
||||
if (!counter[state]) {
|
||||
counter[state] = 0;
|
||||
}
|
||||
counter[state]++;
|
||||
}
|
||||
|
||||
function resourceKey(r: BundleResourceKey): string {
|
||||
return `${ r.kind }/${ r.namespace }/${ r.name }`;
|
||||
}
|
||||
|
||||
class Fleet {
|
||||
resourceId(r: BundleResourceKey): string {
|
||||
return r.namespace ? `${ r.namespace }/${ r.name }` : r.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* resourceType normalizes APIVersion and Kind from a Resources into a single string
|
||||
*/
|
||||
resourceType(r: Resource): string {
|
||||
// ported from https://github.com/rancher/fleet/blob/v0.10.0/internal/cmd/controller/grutil/resourcekey.go#L116-L128
|
||||
const type = r.kind.toLowerCase();
|
||||
|
||||
if (!r.apiVersion || r.apiVersion === 'v1') {
|
||||
return type;
|
||||
}
|
||||
|
||||
return `${ r.apiVersion.split('/', 2)[0] }.${ type }`;
|
||||
}
|
||||
|
||||
/**
|
||||
* resourcesFromBundleDeploymentStatus extracts the list of resources deployed by a BundleDeployment
|
||||
*/
|
||||
resourcesFromBundleDeploymentStatus(status: BundleDeploymentStatus): Resource[] {
|
||||
// status.resources includes of resources that were deployed by Fleet *and still exist in the cluster*
|
||||
// Use a map to avoid `find` over and over again
|
||||
const resources = (status?.resources || []).reduce((res, r) => {
|
||||
res[resourceKey(r)] = Object.assign({ state: STATES_ENUM.READY }, r);
|
||||
|
||||
return res;
|
||||
}, {} as { [resourceKey: string]: Resource });
|
||||
|
||||
const modified: Resource[] = [];
|
||||
|
||||
for (const r of status?.modifiedStatus || []) {
|
||||
const state = r.missing ? STATES_ENUM.MISSING : r.delete ? STATES_ENUM.ORPHANED : STATES_ENUM.MODIFIED;
|
||||
const found: Resource = resources[resourceKey(r)];
|
||||
|
||||
// Depending on the state, the same resource can appear in both fields
|
||||
if (found) {
|
||||
found.state = state;
|
||||
} else {
|
||||
modified.push(Object.assign({ state }, r));
|
||||
}
|
||||
}
|
||||
for (const r of status?.nonReadyStatus || []) {
|
||||
const state = r.summary?.state || STATES_ENUM.UNKNOWN;
|
||||
const found: Resource = resources[resourceKey(r)];
|
||||
|
||||
if (found) {
|
||||
found.state = state;
|
||||
}
|
||||
}
|
||||
|
||||
return modified.concat(Object.values(resources));
|
||||
}
|
||||
|
||||
/**
|
||||
* resourcesFromBundleStatus extracts the list of resources deployed by a Bundle
|
||||
*/
|
||||
resourcesFromBundleStatus(status: BundleStatus): Resource[] {
|
||||
// The state of every resource is spread all over the bundle status.
|
||||
// resourceKey contains one entry per resource AND cluster (built by Fleet from all the child BundleDeployments).
|
||||
// However, those entries do not contain the cluster that they belong to, leading to duplicate entries
|
||||
|
||||
// 1. Fold resourceKey by using a unique key, initializing counters for multiple occurrences of the same resource
|
||||
const resources = (status.resourceKey || []).reduce((res, r) => {
|
||||
const k = resourceKey(r);
|
||||
|
||||
if (!res[k]) {
|
||||
res[k] = { r, count: {} };
|
||||
}
|
||||
incr(res[k].count, STATES_ENUM.READY);
|
||||
|
||||
return res;
|
||||
}, {} as { [resourceKey: string]: { r: BundleResourceKey, count: StatesCounter } });
|
||||
|
||||
// 2. Non-ready resources are counted differently and may also appear in resourceKey, depending on their state
|
||||
for (const bundle of status.summary?.nonReadyResources || []) {
|
||||
for (const r of bundle.modifiedStatus || []) {
|
||||
const k = resourceKey(r);
|
||||
|
||||
if (!resources[k]) {
|
||||
resources[k] = { r, count: {} };
|
||||
}
|
||||
|
||||
if (r.missing) {
|
||||
incr(resources[k].count, STATES_ENUM.MISSING);
|
||||
} else if (r.delete) {
|
||||
resources[k].count[STATES_ENUM.READY]--;
|
||||
incr(resources[k].count, STATES_ENUM.ORPHANED);
|
||||
} else {
|
||||
resources[k].count[STATES_ENUM.READY]--;
|
||||
incr(resources[k].count, STATES_ENUM.MODIFIED);
|
||||
}
|
||||
}
|
||||
for (const r of bundle.nonReadyStatus || []) {
|
||||
const k = resourceKey(r);
|
||||
const state = r.summary?.state || STATES_ENUM.UNKNOWN;
|
||||
|
||||
resources[k].count[STATES_ENUM.READY]--;
|
||||
incr(resources[k].count, state);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Unfold back to an array of resources for display
|
||||
return Object.values(resources).reduce((res, e) => {
|
||||
const { r, count } = e;
|
||||
|
||||
for (const state in count) {
|
||||
for (let x = 0; x < count[state]; x++) {
|
||||
res.push(Object.assign({ state }, r));
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}, [] as Resource[]);
|
||||
}
|
||||
|
||||
clusterIdFromBundleDeploymentLabels(labels?: Labels): string {
|
||||
const clusterNamespace = labels?.[FLEET_ANNOTATIONS.CLUSTER_NAMESPACE];
|
||||
const clusterName = labels?.[FLEET_ANNOTATIONS.CLUSTER];
|
||||
|
||||
return `${ clusterNamespace }/${ clusterName }`;
|
||||
}
|
||||
}
|
||||
|
||||
const instance = new Fleet();
|
||||
|
||||
export default instance;
|
||||
|
|
@ -29,7 +29,7 @@ class GarbageCollect {
|
|||
* To avoid JSON.parse on the `ui-performance` setting keep a local cache
|
||||
*/
|
||||
private getUiPerfGarbageCollection = (rootState: any) => {
|
||||
const uiPerfSetting = rootState.management.types[MANAGEMENT.SETTING]?.list.find((s: any) => s.id === SETTING.UI_PERFORMANCE);
|
||||
const uiPerfSetting = rootState.management.types[MANAGEMENT.SETTING]?.list?.find((s: any) => s.id === SETTING.UI_PERFORMANCE);
|
||||
|
||||
if (!uiPerfSetting || !uiPerfSetting.value) {
|
||||
// Could be in the process of logging out
|
||||
|
|
|
|||
|
|
@ -131,11 +131,11 @@ const instrumentCode = () => {
|
|||
};
|
||||
|
||||
const getLoaders = (SHELL_ABS) => [
|
||||
// Ensure there is a fallback for browsers that don't support web workers
|
||||
// no fallback for pre-2013 browsers https://caniuse.com/webworkers
|
||||
{
|
||||
test: /web-worker.[a-z-]+.js/i,
|
||||
loader: 'worker-loader',
|
||||
options: { inline: 'fallback' },
|
||||
options: { inline: 'no-fallback' },
|
||||
},
|
||||
// Handler for csv files (e.g. ec2 instance data)
|
||||
{
|
||||
|
|
|
|||
703
shell/yarn.lock
703
shell/yarn.lock
File diff suppressed because it is too large
Load Diff
|
|
@ -9421,9 +9421,9 @@ http-proxy-agent@^4.0.1:
|
|||
debug "4"
|
||||
|
||||
http-proxy-middleware@^2.0.3:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f"
|
||||
integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==
|
||||
version "2.0.7"
|
||||
resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz#915f236d92ae98ef48278a95dedf17e991936ec6"
|
||||
integrity sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==
|
||||
dependencies:
|
||||
"@types/http-proxy" "^1.17.8"
|
||||
http-proxy "^1.18.1"
|
||||
|
|
|
|||
Loading…
Reference in New Issue