Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion shell/assets/translations/en-us.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3246,7 +3246,7 @@ fleet:
none: None
local: Local
resources:
label: 'Resource Handling'
label: 'Resources'
keepResources: Always keep resources
keepResourcesTooltip: When enabled, resources will be kept when deleting a HelmOp or Bundle - only Helm release secrets will be deleted.
correctDrift: Enable self-healing
Expand Down Expand Up @@ -3300,6 +3300,10 @@ fleet:
=0 { Adding namespaces here will create a GitRepoRestriction. }
other { Only the Git Repo Restriction's <code>allowedTargetNamespaces</code> is managed here. You can make additional changes to the Git Repo Restriction}
}"
configMaps:
label: Config Maps
secrets:
label: Secrets
footer:
docs: Docs
download: Download CLI
Expand Down
117 changes: 117 additions & 0 deletions shell/components/fleet/FleetConfigMapSelector.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { _EDIT } from '@shell/config/query-params';
import { CONFIG_MAP } from '@shell/config/types';
import { PaginationParamFilter } from '@shell/types/store/pagination.types';
import ResourceLabeledSelect from '@shell/components/form/ResourceLabeledSelect.vue';

interface ConfigMap {
id?: string;
name: string;
namespace: string;
}

const props = defineProps({
value: {
type: Object,
required: true,
},
namespace: {
type: String,
required: true,
},
inStore: {
type: String,
default: 'management',
},
mode: {
type: String,
default: _EDIT
},
label: {
type: String,
default: '',
},
});

const emit = defineEmits(['update:value']);

const configMaps = ref<ConfigMap[]>([]);

const allConfigMapsSettings = {
updateResources: (configMapsList: ConfigMap[]) => {
const allConfigMapsInNamespace = configMapsList.filter((configMap) => configMap.namespace === props.namespace);
const mappedConfigMaps = mapConfigMaps(allConfigMapsInNamespace.sort((a, b) => a.name.localeCompare(b.name)));

configMaps.value = allConfigMapsInNamespace;

return mappedConfigMaps;
}
};

const paginateConfigMapsSetting = {
requestSettings: paginatePageOptions,
updateResources: (configMapsList: ConfigMap[]) => {
const mappedConfigMaps = mapConfigMaps(configMapsList);

configMaps.value = configMapsList;

return mappedConfigMaps;
}
};

function mapConfigMaps(configMapsList: ConfigMap[]) {
return configMapsList.reduce<{ label: string; value: string }[]>((res, c) => {
if (c.id) {
res.push({ label: c.name, value: c.name });
} else {
res.push(c as any);
}

return res;
}, []);
}

function paginatePageOptions(opts: any) {
const { opts: { filter } } = opts;

const filters = !!filter ? [PaginationParamFilter.createSingleField({
field: 'metadata.name', value: filter, exact: false, equals: true
})] : [];

filters.push(
PaginationParamFilter.createSingleField({ field: 'metadata.namespace', value: props.namespace }),
);

return {
...opts,
filters,
groupByNamespace: false,
classify: true,
sort: [{ asc: true, field: 'metadata.name' }],
};
}

function update(value: any) {
emit('update:value', value);
}
</script>

<template>
<ResourceLabeledSelect
:key="namespace"
:value="value"
:label="label || t('fleet.configMaps.label')"
:mode="mode"
:resource-type="CONFIG_MAP"
:loading="$fetchState.pending"
:in-store="inStore"
:paginated-resource-settings="paginateConfigMapsSetting"
:all-resources-settings="allConfigMapsSettings"
:multiple="true"
@update:value="update"
/>
</template>

<style lang="scss" scoped>
</style>
127 changes: 127 additions & 0 deletions shell/components/fleet/FleetSecretSelector.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<script lang="ts" setup>
import { ref, computed, defineProps, defineEmits } from 'vue';
import { _EDIT } from '@shell/config/query-params';
import { TYPES } from '@shell/models/secret';
import { SECRET } from '@shell/config/types';
import { PaginationParamFilter } from '@shell/types/store/pagination.types';
import ResourceLabeledSelect from '@shell/components/form/ResourceLabeledSelect.vue';

interface Secret {
id?: string;
name: string;
namespace: string;
_type: string;
}

const props = defineProps({
value: {
type: Object,
required: true,
},
namespace: {
type: String,
required: true,
},
inStore: {
type: String,
default: 'management',
},
mode: {
type: String,
default: _EDIT
},
label: {
type: String,
default: '',
},
});

const emit = defineEmits(['update:value']);

const types = computed<string[]>(() => Object.values(TYPES));

const secrets = ref<Secret[]>([]);

const allSecretsSettings = {
updateResources: (secretsList: Secret[]) => {
const allSecretsInNamespace = secretsList.filter((secret) => types.value.includes(secret._type) && secret.namespace === props.namespace);
const mappedSecrets = mapSecrets(allSecretsInNamespace.sort((a, b) => a.name.localeCompare(b.name)));

secrets.value = allSecretsInNamespace;

return mappedSecrets;
}
};

const paginateSecretsSetting = {
requestSettings: paginatePageOptions,
updateResources: (secretsList: Secret[]) => {
const mappedSecrets = mapSecrets(secretsList);

secrets.value = secretsList;

return mappedSecrets;
}
};

function mapSecrets(secretsList: Secret[]) {
return secretsList.reduce<{ label: string; value: string }[]>((res, s) => {
if (s.id) {
res.push({ label: s.name, value: s.name });
} else {
res.push(s as any);
}

return res;
}, []);
}

function update(value: any) {
emit('update:value', value);
}

function paginatePageOptions(opts: any) {
const { opts: { filter } } = opts;

const filters = !!filter ? [PaginationParamFilter.createSingleField({
field: 'metadata.name', value: filter, exact: false, equals: true
})] : [];

filters.push(
PaginationParamFilter.createSingleField({ field: 'metadata.namespace', value: props.namespace }),
PaginationParamFilter.createMultipleFields(types.value.map((t) => ({
field: 'metadata.fields.1',
equals: true,
exact: true,
value: t
})))
);

return {
...opts,
filters,
groupByNamespace: false,
classify: true,
sort: [{ asc: true, field: 'metadata.name' }],
};
}
</script>

<template>
<ResourceLabeledSelect
:key="namespace"
:value="value"
:label="label || t('fleet.secrets.label')"
:mode="mode"
:resource-type="SECRET"
:loading="$fetchState.pending"
:in-store="inStore"
:paginated-resource-settings="paginateSecretsSetting"
:all-resources-settings="allSecretsSettings"
:multiple="true"
@update:value="update"
/>
</template>

<style lang="scss" scoped>
</style>
125 changes: 125 additions & 0 deletions shell/components/fleet/__tests__/FleetConfigMapSelector.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { shallowMount } from '@vue/test-utils';
import { _EDIT } from '@shell/config/query-params';
import FleetConfigMapSelector from '@shell/components/fleet/FleetConfigMapSelector.vue';
import ResourceLabeledSelect from '@shell/components/form/ResourceLabeledSelect.vue';

describe('fleetConfigMapSelector.vue', () => {
const defaultProps = {
value: {},
namespace: 'fleet-default',
inStore: 'management',
mode: _EDIT,
label: 'Config Map',
};

const global = {
stubs: { ResourceLabeledSelect },
mocks: {
$fetchState: { pending: false },
$store: { getters: { 'management/all': () => [] } },
},
};

it('should emit update:value when update is called', async() => {
const wrapper = shallowMount(FleetConfigMapSelector, {
props: defaultProps,
global,
});

const vm = wrapper.vm as any;

await vm.update('cm1');

expect(wrapper.emitted('update:value')).toBeTruthy();
expect(wrapper.emitted('update:value')?.[0]).toStrictEqual(['cm1']);
});

it('should correctly map configMaps', () => {
const wrapper = shallowMount(FleetConfigMapSelector, {
props: defaultProps,
global
});

const configMapsList = [
{
id: '1', name: 'cm1', namespace: 'fleet-default'
},
{ name: 'cm2', namespace: 'fleet-default' }
];

const vm = wrapper.vm as any;
const result = vm.mapConfigMaps(configMapsList);

expect(result).toStrictEqual([
{ label: 'cm1', value: 'cm1' },
{ name: 'cm2', namespace: 'fleet-default' }
]);
});

it('should return correct filter options from paginatePageOptions', () => {
const wrapper = shallowMount(FleetConfigMapSelector, {
props: defaultProps,
global
});

const opts = { opts: { filter: 'test' } };

const vm = wrapper.vm as any;
const result = vm.paginatePageOptions(opts);

expect(result.filters).toHaveLength(2);
expect(result.groupByNamespace).toStrictEqual(false);
expect(result.classify).toStrictEqual(true);
expect(result.sort).toStrictEqual([{ asc: true, field: 'metadata.name' }]);
});

it('should correctly filter and map configMaps in allConfigMapsSettings.updateResources', () => {
const wrapper = shallowMount(FleetConfigMapSelector, {
props: defaultProps,
global
});

const configMapsList = [
{
id: '1', name: 'cm1', namespace: 'fleet-default'
},
{
id: '2', name: 'cm2', namespace: 'other'
}
];

const vm = wrapper.vm as any;

const result = vm.allConfigMapsSettings.updateResources(configMapsList);

expect(result).toStrictEqual([{ label: 'cm1', value: 'cm1' }]);
expect(vm.configMaps).toStrictEqual([{
id: '1', name: 'cm1', namespace: 'fleet-default'
}]);
});

it('should correctly map configMaps in paginateConfigMapsSetting.updateResources', () => {
const wrapper = shallowMount(FleetConfigMapSelector, {
props: defaultProps,
global
});

const configMapsList = [
{
id: '1', name: 'cm1', namespace: 'fleet-default'
},
{
id: '2', name: 'cm2', namespace: 'fleet-default'
}
];

const vm = wrapper.vm as any;
const result = vm.paginateConfigMapsSetting.updateResources(configMapsList);

expect(result).toStrictEqual([
{ label: 'cm1', value: 'cm1' },
{ label: 'cm2', value: 'cm2' }
]);
expect(vm.configMaps).toStrictEqual(configMapsList);
});
});
Loading
Loading