Merge branch 'master' of github.com:rancher/dashboard into 11289-undefined-chart-version

This commit is contained in:
Mo Mesgin 2024-11-15 08:58:48 -08:00
commit 1603dd2551
64 changed files with 1742 additions and 381 deletions

View File

@ -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()

View File

@ -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);

View File

@ -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

View File

@ -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;
}

View File

@ -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);

View File

@ -117,6 +117,7 @@ export default {
display: flex;
justify-content: flex-end;
margin-top: 20px;
z-index: 40;
.btn {
margin-left: 20px;

View File

@ -94,7 +94,7 @@ export default {
},
labels() {
if (!this.showFilteredSystemLabels) {
if (this.showAllLabels || !this.showFilteredSystemLabels) {
return this.value?.labels || {};
}

View File

@ -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')"
>

View File

@ -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.

View File

@ -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 }}

View File

@ -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 />

View File

@ -2,7 +2,7 @@
export default {
props: {
to: {
type: String,
type: [String, Object],
required: true
},

View File

@ -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>

View File

@ -168,7 +168,8 @@ export default {
},
bundle: {
inStoreType: 'management',
type: FLEET.BUNDLE
type: FLEET.BUNDLE,
opt: { excludeFields: ['metadata.managedFields', 'spec.resources'] },
},
bundleDeployment: {

View File

@ -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"

View File

@ -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>

View File

@ -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>

View File

@ -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() {

View File

@ -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}"

View File

@ -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>

View File

@ -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>

View File

@ -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' ) {

View File

@ -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]);

View File

@ -38,7 +38,7 @@ export default {
},
completed() {
return Number.parseFloat(this.value) === 100 && !this.failed;
return Number.parseFloat(this.value) === 100;
},
},
};

View File

@ -38,5 +38,9 @@ export default {
</script>
<template>
<span>{{ formattedValue }}</span>
<span v-if="value">{{ formattedValue }}</span>
<span
v-else
class="text-muted"
>&mdash;</span>
</template>

View File

@ -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);
},
},
};

View File

@ -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

View File

@ -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;

View File

@ -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',
};

View File

@ -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',

View File

@ -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',
};

View File

@ -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>

View File

@ -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: {

View File

@ -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>

View File

@ -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')"

View File

@ -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

View File

@ -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>

View File

@ -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'],

View File

@ -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 }`);

View File

@ -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>

View File

@ -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>

View File

@ -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"

View File

@ -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() {

View File

@ -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] = {

View File

@ -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;
}
}
}

View File

@ -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();
}
}
}

View File

@ -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];
}
}

View File

@ -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": [

View File

@ -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;

View File

@ -40,6 +40,7 @@ export default {
allBundles: {
inStoreType: 'management',
type: FLEET.BUNDLE,
opt: { excludeFields: ['metadata.managedFields', 'spec.resources'] },
},
gitRepos: {
inStoreType: 'management',

View File

@ -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>

View File

@ -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,

View File

@ -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 {
/**

View File

@ -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"

View File

@ -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);
}
}
};

40
shell/types/resources/fleet.d.ts vendored Normal file
View File

@ -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,
}

View File

@ -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 } );
}
}

View File

@ -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('.');

View File

@ -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;

159
shell/utils/fleet.ts Normal file
View File

@ -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;

View File

@ -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

View File

@ -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)
{

File diff suppressed because it is too large Load Diff

View File

@ -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"