mirror of https://github.com/rancher/dashboard.git
Wire in pagination to LabeledSelect and use for pods, secrets and configmaps (#10786)
* Changes for new design - New visuals - Pagination controls --> load more - finished testing of label select with pagination off # Conflicts: # shell/edit/provisioning.cattle.io.cluster/__tests__/Basics.tests.ts * Changes following review * Update Node list to support server-side pagination - Setup pagination headers for the node type - Define a pattern for fetching custom list secondary resources - Major improvements to the way pagination settings are defined and created - Lots of docs improvements - Handle calling fetch again once fetch is in progress (nuxt caches running request) - Validate filter fields (not all are supported by the vai cache - General pagination fixes * Lint / test / fixes * Improvements to configmap e2e test & Improve pagination disabled * Beef up validation * Fix missing name column in non-server-side paginated node list * Fix PR automation actions - fix syntax - catch scenario where a pr has no fixed issue > There's duplication between files, see https://github.com/rancher/dashboard/pull/10534 * CI bump * Fixes post merge * Wire in 2.9.0 settings for server-side pagination - Everything is gated on `on-disk-steve-cache` feature flag - There's a backend in progress item to resolve a `revision` issue, until then disable watching a resource given it - Global Settings - Performance - Added new setting to enable server side pagination - this is incompatible with two other performance settings * Integrate pagination with configmaps in cis clusterscanbenchmark edit form Also - improved labeled select pagination - gate label select pagination functinality on steve cache being enabled * TODOs and TEST * Paginate Secret selection for logging providers - Allow `None` option in Paginationed LabelSelect - Optionally classify pagination response * WIP * fixes arfter merge * Don't suggest container names, not practical - previously all pods were fetched... and we scrapped all container names from them - this is a scaling nightmare, user now must just enter the name/s to match * Avoid findAll secrets in SimpleSecretSelector * tidying up * Move LabeledSelect/index.vue back to LabeledSelect.vue to not break extensions * changes after self review... 1 * changes after self review... 2 * changes after self review... 3 * Link new paginated label select with pagination setting * Work around failing kubewarden unit tests in check-plugins gate * Fix backup.spec e2e test * Create a convienence wrapper called ResourceLabelSelector that hides most of the complexity * fix unit test
This commit is contained in:
parent
67dd70e5b2
commit
542ebd4f40
|
|
@ -41,7 +41,7 @@ describe('Charts', { tags: ['@charts', '@adminUser'] }, () => {
|
|||
installPage.nextPage();
|
||||
cy.wait('@storageClasses', { timeout: 10000 }).its('response.statusCode').should('eq', 200);
|
||||
cy.wait('@persistentVolumes', { timeout: 10000 }).its('response.statusCode').should('eq', 200);
|
||||
cy.wait('@secrets', { timeout: 10000 }).its('response.statusCode').should('eq', 200);
|
||||
|
||||
installPage.waitForPage('repo-type=cluster&repo=rancher-charts&chart=rancher-backup');
|
||||
|
||||
// Select the 'Use an existing storage class' option
|
||||
|
|
|
|||
|
|
@ -112,7 +112,8 @@
|
|||
--dropdown-hover-bg : var(--link);
|
||||
--dropdown-disabled-bg : #{$disabled};
|
||||
--dropdown-disabled-text : #{$disabled};
|
||||
--dropdown-highlight-text : var(--link);
|
||||
--dropdown-group-text : #{$disabled};
|
||||
--dropdown-highlight-text : var(--link);
|
||||
|
||||
--input-text : #{$lightest};
|
||||
--input-label : #{$lighter};
|
||||
|
|
|
|||
|
|
@ -376,6 +376,7 @@ BODY, .theme-light {
|
|||
--dropdown-hover-bg : var(--link);
|
||||
--dropdown-disabled-text : var(--muted);
|
||||
--dropdown-disabled-bg : #{$disabled};
|
||||
--dropdown-group-text : #{$darker};
|
||||
--dropdown-highlight-text : var(--link);
|
||||
|
||||
--card-badge-text : #ffffff;
|
||||
|
|
|
|||
|
|
@ -94,6 +94,11 @@
|
|||
hr {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.vs__option-kind-group {
|
||||
color: var(--dropdown-group-text);
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
&.vs__dropdown-option--selected {
|
||||
|
|
|
|||
|
|
@ -2874,6 +2874,13 @@ labels:
|
|||
annotations:
|
||||
title: Annotations
|
||||
|
||||
labelSelect:
|
||||
noOptions:
|
||||
empty: No options
|
||||
noMatch: No matching options
|
||||
pagination:
|
||||
counts: '{count} / {totalCount} items'
|
||||
more: Load More...
|
||||
landing:
|
||||
clusters:
|
||||
title: Clusters
|
||||
|
|
@ -2940,6 +2947,7 @@ logging:
|
|||
include: Limit to specific container names
|
||||
exclude: Exclude specific container names
|
||||
placeholder: "Default: Any container"
|
||||
enter: Enter container name to select
|
||||
namespaces:
|
||||
title:
|
||||
include: Limit to specific namespaces
|
||||
|
|
@ -7341,7 +7349,7 @@ performance:
|
|||
checkboxLabel: Enable Server-side Pagination
|
||||
applicable: "This applies to the following resource types"
|
||||
incompatibleDescription: "Server-side Pagination is incomaptible with Manual Refresh and Incremental Loading. Enabling this will disable them."
|
||||
featureFlag: The <a href="{ffUrl}">Feature Flag</a> `on-disk-steve-cache` must be enabled to use this feature
|
||||
featureFlag: The <a href="{ffUrl}">Feature Flag</a> `on-disk-steve-cache` must be enabled to use this feature
|
||||
resources:
|
||||
generic: most resources in the cluster's 'More Resources' section
|
||||
all: All Resources
|
||||
|
|
|
|||
|
|
@ -9,8 +9,14 @@ const localVue = createLocalVue();
|
|||
localVue.use(Vuex);
|
||||
|
||||
describe('rancher-backup: S3', () => {
|
||||
const mockStore = { getters: { 'i18n/t': (text: string) => text, t: (text: string) => text } };
|
||||
const wrapper = mount(S3, { mocks: { $store: mockStore } });
|
||||
const mockStore = {
|
||||
getters: {
|
||||
'i18n/t': (text: string) => text,
|
||||
t: (text: string) => text,
|
||||
'cluster/all': () => [],
|
||||
}
|
||||
};
|
||||
const wrapper = mount(S3, { mocks: { $store: mockStore, $fetchState: { pending: false } } });
|
||||
|
||||
it('should emit invalid when form is not filled', () => {
|
||||
expect(wrapper.emitted('valid')).toHaveLength(1);
|
||||
|
|
|
|||
|
|
@ -2,15 +2,16 @@
|
|||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import { Checkbox } from '@components/Form/Checkbox';
|
||||
import FileSelector from '@shell/components/form/FileSelector';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import ResourceLabeledSelect from '@shell/components/form/ResourceLabeledSelect';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { SECRET } from '@shell/config/types';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LabeledInput,
|
||||
Checkbox,
|
||||
FileSelector,
|
||||
LabeledSelect,
|
||||
ResourceLabeledSelect,
|
||||
},
|
||||
|
||||
props: {
|
||||
|
|
@ -25,10 +26,10 @@ export default {
|
|||
default: 'create'
|
||||
},
|
||||
|
||||
secrets: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return { SECRET };
|
||||
},
|
||||
|
||||
mounted() {
|
||||
|
|
@ -44,6 +45,7 @@ export default {
|
|||
this.$emit('valid', this.valid);
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
credentialSecret: {
|
||||
get() {
|
||||
|
|
@ -75,7 +77,7 @@ export default {
|
|||
// eslint-disable-next-line no-console
|
||||
console.warn(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
|
|
@ -92,13 +94,13 @@ export default {
|
|||
<div>
|
||||
<div class="row mb-10">
|
||||
<div class="col span-6">
|
||||
<LabeledSelect
|
||||
<ResourceLabeledSelect
|
||||
v-model="credentialSecret"
|
||||
:get-option-label="opt=>opt.metadata.name || ''"
|
||||
option-key="id"
|
||||
:mode="mode"
|
||||
:options="secrets"
|
||||
:label="t('backupRestoreOperator.s3.credentialSecretName')"
|
||||
:resource-type="SECRET"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { LabeledInput } from '@components/Form/LabeledInput';
|
|||
import { Banner } from '@components/Banner';
|
||||
import { get } from '@shell/utils/object';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { STORAGE_CLASS, SECRET, PV } from '@shell/config/types';
|
||||
import { STORAGE_CLASS, PV } from '@shell/config/types';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { STORAGE } from '@shell/config/labels-annotations';
|
||||
|
||||
|
|
@ -41,10 +41,8 @@ export default {
|
|||
const hash = await allHash({
|
||||
storageClasses: this.$store.dispatch('cluster/findAll', { type: STORAGE_CLASS }),
|
||||
persistentVolumes: this.$store.dispatch('cluster/findAll', { type: PV }),
|
||||
secrets: this.$store.dispatch('cluster/findAll', { type: SECRET }),
|
||||
});
|
||||
|
||||
this.secrets = hash.secrets;
|
||||
this.storageClasses = hash.storageClasses;
|
||||
this.persistentVolumes = hash.persistentVolumes;
|
||||
|
||||
|
|
@ -89,6 +87,12 @@ export default {
|
|||
|
||||
watch: {
|
||||
storageSource(neu) {
|
||||
if (!this.value.persistence) {
|
||||
this.value.persistence = {};
|
||||
}
|
||||
if (!this.value.s3) {
|
||||
this.value.s3 = {};
|
||||
}
|
||||
switch (neu) {
|
||||
case 'pickSC':
|
||||
this.value.persistence.enabled = true;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { _VIEW, _CREATE } from '@shell/config/query-params';
|
|||
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
|
||||
import Checkbox from '@components/Form/Checkbox/Checkbox.vue';
|
||||
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
|
||||
import { PSADimension, PSAMode } from '@shell/types/pod-security-admission';
|
||||
import { PSADimension, PSAMode } from '@shell/types/resources/pod-security-admission';
|
||||
import {
|
||||
PSADefaultLevel,
|
||||
PSADefaultVersion, PSADimensions, PSALevels, PSAModes
|
||||
|
|
|
|||
|
|
@ -5,12 +5,16 @@ import { get } from '@shell/utils/object';
|
|||
import { LabeledTooltip } from '@components/LabeledTooltip';
|
||||
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)
|
||||
|
||||
export default {
|
||||
name: 'LabeledSelect',
|
||||
|
||||
components: { LabeledTooltip },
|
||||
mixins: [CompactInput, LabeledFormElement, VueSelectOverrides],
|
||||
mixins: [CompactInput, LabeledFormElement, VueSelectOverrides, LabeledSelectPagination],
|
||||
|
||||
props: {
|
||||
appendToBody: {
|
||||
|
|
@ -66,7 +70,7 @@ export default {
|
|||
selectable: {
|
||||
default: (opt) => {
|
||||
if ( opt ) {
|
||||
if ( opt.disabled || opt.kind === 'group' || opt.kind === 'divider' || opt.loading ) {
|
||||
if ( opt.disabled || LABEL_SELECT_NOT_OPTION_KINDS.includes(opt.kind) || opt.loading ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -88,13 +92,17 @@ export default {
|
|||
type: [String, Object, Number, Array, Boolean]
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
required: true
|
||||
type: Array,
|
||||
default: () => ([])
|
||||
},
|
||||
closeOnSelect: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
noOptionsLabelKey: {
|
||||
type: String,
|
||||
default: 'labelSelect.noOptions.empty'
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
|
|
@ -108,6 +116,16 @@ export default {
|
|||
hasLabel() {
|
||||
return this.isCompact ? false : !!this.label || !!this.labelKey || !!this.$slots.label;
|
||||
},
|
||||
|
||||
hasGroupIcon() {
|
||||
// Required for option.icon. Note that we only apply if paginating as well (there might be 2 x performance issues with 2k entries. one to iterate through this list, the other with conditional class per entry in dom)
|
||||
return this.canPaginate ? !!this._options.find((o) => o.kind === 'group' && !!o.icon) : false;
|
||||
},
|
||||
|
||||
_options() {
|
||||
// If we're paginated show the page as provided by `paginate`. See label-select-pagination mixin
|
||||
return this.canPaginate ? this.page : this.options;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
|
@ -198,8 +216,12 @@ export default {
|
|||
},
|
||||
|
||||
onSearch(newSearchString) {
|
||||
if (newSearchString) {
|
||||
this.dropdownShouldOpen(this.$refs['select-input'], true);
|
||||
if (this.canPaginate) {
|
||||
this.setPaginationFilter(newSearchString);
|
||||
} else {
|
||||
if (newSearchString) {
|
||||
this.dropdownShouldOpen(this.$refs['select-input'], true);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -255,16 +277,17 @@ export default {
|
|||
class="inline"
|
||||
:append-to-body="appendToBody"
|
||||
:calculate-position="positionDropdown"
|
||||
:class="{ 'no-label': !(label || '').length }"
|
||||
:class="{ 'no-label': !(label || '').length}"
|
||||
:clearable="clearable"
|
||||
:disabled="isView || disabled || loading"
|
||||
:get-option-key="getOptionKey"
|
||||
:get-option-label="(opt) => getOptionLabel(opt)"
|
||||
:label="optionLabel"
|
||||
:options="options"
|
||||
:options="_options"
|
||||
:map-keydown="mappedKeys"
|
||||
:placeholder="placeholder"
|
||||
:reduce="(x) => reduce(x)"
|
||||
:filterable="isFilterable"
|
||||
:searchable="isSearchable"
|
||||
:selectable="selectable"
|
||||
:value="value != null && !loading ? value : ''"
|
||||
|
|
@ -280,6 +303,11 @@ export default {
|
|||
<template #option="option">
|
||||
<template v-if="option.kind === 'group'">
|
||||
<div class="vs__option-kind-group">
|
||||
<i
|
||||
v-if="option.icon"
|
||||
class="icon"
|
||||
:class="{ [option.icon]: true}"
|
||||
/>
|
||||
<b>{{ getOptionLabel(option) }}</b>
|
||||
<div v-if="option.badge">
|
||||
{{ option.badge }}
|
||||
|
|
@ -296,6 +324,8 @@ export default {
|
|||
</template>
|
||||
<div
|
||||
v-else
|
||||
class="vs__option-kind"
|
||||
:class="{ 'has-icon' : hasGroupIcon}"
|
||||
@mousedown="(e) => onClickOption(option, e)"
|
||||
>
|
||||
{{ getOptionLabel(option) }}
|
||||
|
|
@ -316,6 +346,45 @@ export default {
|
|||
v-bind="scope"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div
|
||||
v-if="canPaginate && totalResults"
|
||||
slot="list-footer"
|
||||
class="pagination-slot"
|
||||
>
|
||||
<div class="load-more">
|
||||
<i
|
||||
v-if="paginating"
|
||||
class="icon icon-spinner icon-spin"
|
||||
/>
|
||||
<div v-else>
|
||||
<a
|
||||
v-if="canLoadMore"
|
||||
@click="loadMore"
|
||||
> {{ t('labelSelect.pagination.more') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="count">
|
||||
{{ optionCounts }}
|
||||
</div>
|
||||
</div>
|
||||
<template #no-options="{ search }">
|
||||
<div class="no-options-slot">
|
||||
<div
|
||||
v-if="paginating"
|
||||
class="paginating"
|
||||
>
|
||||
<i class="icon icon-spinner icon-spin" />
|
||||
</div>
|
||||
<template v-else-if="search">
|
||||
{{ t('labelSelect.noOptions.noMatch') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ t(noOptionsLabelKey) }}
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</v-select>
|
||||
<i
|
||||
v-if="loading"
|
||||
|
|
@ -493,21 +562,73 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
// Styling for option group badge
|
||||
.vs__dropdown-menu .vs__dropdown-option .vs__option-kind-group {
|
||||
display: flex;
|
||||
> b {
|
||||
flex: 1;
|
||||
$icon-size: 18px;
|
||||
|
||||
// This represents the drop down area. Note - it might be attached to body and NOT the parent label select div
|
||||
.vs__dropdown-menu {
|
||||
|
||||
// Styling for individual options
|
||||
.vs__dropdown-option .vs__option-kind {
|
||||
&-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
i { // icon
|
||||
width: $icon-size;
|
||||
}
|
||||
|
||||
> b { // group label
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
> div { // badge
|
||||
background-color: var(--primary);
|
||||
border-radius: 4px;
|
||||
color: var(--primary-text);
|
||||
font-size: 12px;
|
||||
height: 18px;
|
||||
line-height: 18px;
|
||||
margin-top: 1px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&.has-icon {
|
||||
padding-left: $icon-size;
|
||||
}
|
||||
}
|
||||
> div {
|
||||
background-color: var(--primary);
|
||||
border-radius: 4px;
|
||||
color: var(--primary-text);
|
||||
font-size: 12px;
|
||||
height: 18px;
|
||||
line-height: 18px;
|
||||
margin-top: 1px;
|
||||
padding: 0 10px;
|
||||
|
||||
&.has-icon .vs__option-kind div{
|
||||
padding-left: $icon-size;
|
||||
}
|
||||
|
||||
.pagination-slot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
margin-top: 5px;
|
||||
|
||||
.load-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 19px;
|
||||
|
||||
a {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.count {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.no-options-slot .paginating {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,188 @@
|
|||
<script lang="ts">
|
||||
import { PropType, defineComponent } from 'vue';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
|
||||
import { PaginationParamFilter } from '@shell/types/store/pagination.types';
|
||||
import { labelSelectPaginationFunction, LabelSelectPaginationFunctionOptions } from '@shell/components/form/labeled-select-utils/labeled-select.utils';
|
||||
import paginationUtils from '@shell/utils/pagination-utils';
|
||||
import { LabelSelectPaginateFn, LabelSelectPaginateFnOptions, LabelSelectPaginateFnResponse } from '~/shell/types/components/labeledSelect';
|
||||
|
||||
type PaginateTypeOverridesFn = (opts: LabelSelectPaginationFunctionOptions) => LabelSelectPaginationFunctionOptions;
|
||||
|
||||
interface SharedSettings {
|
||||
/**
|
||||
* Provide specific LabelSelect options for this mode (paginated / not paginated)
|
||||
*/
|
||||
labelSelectOptions?: { [key: string]: any },
|
||||
/**
|
||||
* Map the resources shown in LabelSelect
|
||||
*/
|
||||
mapResult?: (resources: any[]) => any[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings to use when the LabelSelect is paginating
|
||||
*/
|
||||
export interface ResourceLabeledSelectPaginateSettings extends SharedSettings {
|
||||
/**
|
||||
* Override the convience function which fetches a page of results
|
||||
*/
|
||||
overrideRequest?: LabelSelectPaginateFn,
|
||||
/**
|
||||
* Override the default settings used in the convience function to fetch a page of results
|
||||
*/
|
||||
requestSettings?: PaginateTypeOverridesFn,
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings to use when the LabelSelect is fetching all resources (not paginating)
|
||||
*/
|
||||
export type ResourceLabeledSelectSettings = SharedSettings
|
||||
|
||||
/**
|
||||
* Force a specific mode
|
||||
*/
|
||||
export enum RESOURCE_LABEL_SELECT_MODE {
|
||||
/**
|
||||
* Fetch all resources
|
||||
*/
|
||||
ALL_RESOURCES = 'ALL', // eslint-disable-line no-unused-vars
|
||||
/**
|
||||
* Determine if all resources are fetched given system settings
|
||||
*/
|
||||
DYNAMIC = 'DYNAMIC', // eslint-disable-line no-unused-vars
|
||||
}
|
||||
|
||||
/**
|
||||
* Convience wrapper around the LabelSelect component to support pagination
|
||||
*
|
||||
* Handles
|
||||
*
|
||||
* 1) Conditionally enabling the pagination feature given system settings
|
||||
* 2) Helper function to fetch the pagination result
|
||||
*
|
||||
* A number of ways can be provided to override the convienences (see props)
|
||||
*/
|
||||
export default defineComponent({
|
||||
name: 'ResourceLabeledSelect',
|
||||
|
||||
components: { LabeledSelect },
|
||||
|
||||
props: {
|
||||
/**
|
||||
* Resource to show
|
||||
*/
|
||||
resourceType: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
|
||||
inStore: {
|
||||
type: String,
|
||||
default: 'cluster',
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if pagination is used via settings (DYNAMIC) or hardcode off
|
||||
*/
|
||||
paginateMode: {
|
||||
type: String as PropType<RESOURCE_LABEL_SELECT_MODE>,
|
||||
default: RESOURCE_LABEL_SELECT_MODE.DYNAMIC,
|
||||
},
|
||||
|
||||
/**
|
||||
* Specific settings to use when we're showing all results
|
||||
*/
|
||||
allResourcesSettings: {
|
||||
type: Object as PropType<ResourceLabeledSelectSettings>,
|
||||
default: null,
|
||||
},
|
||||
|
||||
/**
|
||||
* Specific settings to use when we're showing paginated results
|
||||
*/
|
||||
paginatedResourceSettings: {
|
||||
type: Object as PropType<ResourceLabeledSelectPaginateSettings>,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return { paginate: false };
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
switch (this.paginateMode) {
|
||||
case RESOURCE_LABEL_SELECT_MODE.ALL_RESOURCES:
|
||||
this.paginate = false;
|
||||
break;
|
||||
case RESOURCE_LABEL_SELECT_MODE.DYNAMIC:
|
||||
this.paginate = paginationUtils.isEnabled({ rootGetters: this.$store.getters }, { store: this.inStore, resource: { id: this.resourceType } });
|
||||
break;
|
||||
}
|
||||
|
||||
if (!this.paginate) {
|
||||
await this.$store.dispatch(`${ this.inStore }/findAll`, { type: this.resourceType });
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
labelSelectAttributes() {
|
||||
return this.paginate ? {
|
||||
...this.$attrs,
|
||||
...this.paginatedResourceSettings?.labelSelectOptions || {}
|
||||
} : {
|
||||
...this.$attrs,
|
||||
...this.allResourcesSettings?.labelSelectOptions || {}
|
||||
};
|
||||
},
|
||||
|
||||
allOfType() {
|
||||
if (this.$fetchState.pending || this.paginate) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const all = this.$store.getters[`${ this.inStore }/all`](this.resourceType);
|
||||
|
||||
return this.allResourcesSettings?.mapResult ? this.allResourcesSettings.mapResult(all) : all;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Typeof LabelSelectPaginateFn
|
||||
*/
|
||||
async paginateType(opts: LabelSelectPaginateFnOptions): Promise<LabelSelectPaginateFnResponse> {
|
||||
if (this.paginatedResourceSettings?.overrideRequest) {
|
||||
return await this.paginatedResourceSettings.overrideRequest(opts);
|
||||
}
|
||||
|
||||
const { filter } = opts;
|
||||
const filters = !!filter ? [PaginationParamFilter.createSingleField({ field: 'metadata.name', value: filter })] : [];
|
||||
const defaultOptions: LabelSelectPaginationFunctionOptions = {
|
||||
opts,
|
||||
filters,
|
||||
type: this.resourceType,
|
||||
ctx: { getters: this.$store.getters, dispatch: this.$store.dispatch },
|
||||
sort: [{ asc: true, field: 'metadata.name' }],
|
||||
};
|
||||
const options = this.paginatedResourceSettings?.requestSettings ? this.paginatedResourceSettings.requestSettings(defaultOptions) : defaultOptions;
|
||||
const res = await labelSelectPaginationFunction(options);
|
||||
|
||||
return this.paginatedResourceSettings?.mapResult ? {
|
||||
...res,
|
||||
page: this.paginatedResourceSettings.mapResult(res.page)
|
||||
} : res;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LabeledSelect
|
||||
v-bind="labelSelectAttributes"
|
||||
:loading="$fetchState.pending"
|
||||
:options="allOfType"
|
||||
:paginate="paginateType"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</template>
|
||||
|
|
@ -1,14 +1,16 @@
|
|||
<script>
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import ResourceLabeledSelect from '@shell/components/form/ResourceLabeledSelect';
|
||||
import { SECRET } from '@shell/config/types';
|
||||
import { _EDIT, _VIEW } from '@shell/config/query-params';
|
||||
import { SECRET_TYPES as TYPES } from '@shell/config/secret';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import { PaginationParamFilter } from '@shell/types/store/pagination.types';
|
||||
import { LABEL_SELECT_KINDS } from '@shell/types/components/labeledSelect';
|
||||
|
||||
const NONE = '__[[NONE]]__';
|
||||
|
||||
export default {
|
||||
components: { LabeledSelect },
|
||||
components: { LabeledSelect, ResourceLabeledSelect },
|
||||
|
||||
props: {
|
||||
value: {
|
||||
|
|
@ -62,6 +64,33 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
secrets: null,
|
||||
SECRET,
|
||||
allSecretsSettings: {
|
||||
mapResult: (secrets) => {
|
||||
const allSecretsInNamespace = secrets.filter((secret) => this.types.includes(secret._type) && secret.namespace === this.namespace);
|
||||
const mappedSecrets = this.mapSecrets(allSecretsInNamespace.sort((a, b) => a.name.localeCompare(b.name)));
|
||||
|
||||
this.secrets = allSecretsInNamespace; // We need the key from the selected secret
|
||||
|
||||
return mappedSecrets;
|
||||
}
|
||||
},
|
||||
paginateSecretsSetting: {
|
||||
requestSettings: this.paginatePageOptions,
|
||||
mapResult: (secrets) => {
|
||||
const mappedSecrets = this.mapSecrets(secrets);
|
||||
|
||||
this.secrets = secrets; // We need the key from the selected secret. When paginating we won't touch the store, so just pass back here
|
||||
|
||||
return mappedSecrets;
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
name: {
|
||||
get() {
|
||||
|
|
@ -89,36 +118,77 @@ export default {
|
|||
this.$emit('input', { [this.mountKey]: { secretKeyRef: { [this.nameKey]: this.name, [this.keyKey]: key } } });
|
||||
}
|
||||
},
|
||||
secrets() {
|
||||
const allSecrets = this.$store.getters[`${ this.inStore }/all`](SECRET);
|
||||
|
||||
return allSecrets
|
||||
.filter((secret) => this.types.includes(secret._type) && secret.namespace === this.namespace);
|
||||
},
|
||||
secretNames() {
|
||||
const mappedSecrets = this.secrets.map((secret) => ({
|
||||
label: secret.name,
|
||||
value: secret.name
|
||||
})).sort();
|
||||
|
||||
return [{ label: 'None', value: NONE }, ...sortBy(mappedSecrets, 'label')];
|
||||
},
|
||||
keys() {
|
||||
const secret = this.secrets.find((secret) => secret.name === this.name) || {};
|
||||
const secret = (this.secrets || []).find((secret) => secret.name === this.name) || {};
|
||||
|
||||
return Object.keys(secret.data || {}).map((key) => ({
|
||||
label: key,
|
||||
value: key
|
||||
}));
|
||||
},
|
||||
|
||||
isView() {
|
||||
return this.mode === _VIEW;
|
||||
},
|
||||
|
||||
isKeyDisabled() {
|
||||
return !this.isView && (!this.name || this.name === NONE || this.disabled);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Provide a set of options for the LabelSelect ([none, ...{label, value}])
|
||||
*/
|
||||
mapSecrets(secrets) {
|
||||
const mappedSecrets = secrets
|
||||
.reduce((res, s) => {
|
||||
if (s.kind === LABEL_SELECT_KINDS.NONE) {
|
||||
return res;
|
||||
}
|
||||
|
||||
if (s.id) {
|
||||
res.push({ label: s.name, value: s.name });
|
||||
} else {
|
||||
res.push(s);
|
||||
}
|
||||
|
||||
return res;
|
||||
}, []);
|
||||
|
||||
return [
|
||||
{
|
||||
label: 'None', value: NONE, kind: LABEL_SELECT_KINDS.NONE
|
||||
},
|
||||
...mappedSecrets
|
||||
];
|
||||
},
|
||||
|
||||
/**
|
||||
* @param [LabelSelectPaginationFunctionOptions] opts
|
||||
* @returns LabelSelectPaginationFunctionOptions
|
||||
*/
|
||||
paginatePageOptions(opts) {
|
||||
const { opts: { filter } } = opts;
|
||||
|
||||
const filters = !!filter ? [PaginationParamFilter.createSingleField({ field: 'metadata.name', value: filter })] : [];
|
||||
|
||||
filters.push(
|
||||
PaginationParamFilter.createSingleField({ field: 'metadata.namespace', value: this.namespace }),
|
||||
PaginationParamFilter.createSingleField({ field: 'metadata.fields.1', value: this.types.join(',') })
|
||||
);
|
||||
|
||||
return {
|
||||
...opts,
|
||||
filters,
|
||||
groupByNamespace: false,
|
||||
classify: true,
|
||||
sort: [{ asc: true, field: 'metadata.name' }],
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -128,12 +198,17 @@ export default {
|
|||
:class="{'show-key-selector': showKeySelector}"
|
||||
>
|
||||
<div class="input-container">
|
||||
<LabeledSelect
|
||||
<!-- key by namespace to ensure label select current page is recreated on ns change -->
|
||||
<ResourceLabeledSelect
|
||||
:key="namespace"
|
||||
v-model="name"
|
||||
:disabled="!isView && disabled"
|
||||
:options="secretNames"
|
||||
:label="secretNameLabel"
|
||||
:mode="mode"
|
||||
:resource-type="SECRET"
|
||||
:in-store="inStore"
|
||||
:paginated-resource-settings="paginateSecretsSetting"
|
||||
:all-resources-settings="allSecretsSettings"
|
||||
/>
|
||||
<LabeledSelect
|
||||
v-if="showKeySelector"
|
||||
|
|
|
|||
|
|
@ -13,28 +13,22 @@
|
|||
|
||||
name: example-secret-name
|
||||
key: example-secret-key
|
||||
|
||||
FIXME: The solution to above would have been to have a configurable path to set/get name and key from.
|
||||
This would have avoided a lot of copy and paste
|
||||
*/
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import ResourceLabeledSelect from '@shell/components/form/ResourceLabeledSelect';
|
||||
import { SECRET } from '@shell/config/types';
|
||||
import { _EDIT, _VIEW } from '@shell/config/query-params';
|
||||
import { TYPES } from '@shell/models/secret';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import { LABEL_SELECT_KINDS } from '@shell/types/components/labeledSelect';
|
||||
import { PaginationParamFilter } from '@shell/types/store/pagination.types';
|
||||
|
||||
const NONE = '__[[NONE]]__';
|
||||
|
||||
export default {
|
||||
components: { LabeledSelect },
|
||||
|
||||
async fetch() {
|
||||
// Make sure secrets are in the store so that the secret
|
||||
// selectors in the receiver config forms will have secrets
|
||||
// to choose from.
|
||||
const allSecrets = await this.$store.dispatch('cluster/findAll', { type: SECRET });
|
||||
|
||||
const allSecretsInNamespace = allSecrets.filter((secret) => this.types.includes(secret._type) && secret.namespace === this.namespace);
|
||||
|
||||
this.secrets = allSecretsInNamespace;
|
||||
},
|
||||
components: { LabeledSelect, ResourceLabeledSelect },
|
||||
|
||||
props: {
|
||||
test: { type: String, default: '' },
|
||||
|
|
@ -74,24 +68,37 @@ export default {
|
|||
|
||||
data(props) {
|
||||
return {
|
||||
secrets: [],
|
||||
name: props.initialName,
|
||||
key: props.initialKey,
|
||||
none: NONE
|
||||
secrets: [],
|
||||
name: props.initialName,
|
||||
key: props.initialKey,
|
||||
none: NONE,
|
||||
SECRET,
|
||||
allSecretsSettings: {
|
||||
mapResult: (secrets) => {
|
||||
const allSecretsInNamespace = secrets.filter((secret) => this.types.includes(secret._type) && secret.namespace === this.namespace);
|
||||
const mappedSecrets = this.mapSecrets(allSecretsInNamespace.sort((a, b) => a.name.localeCompare(b.name)));
|
||||
|
||||
this.secrets = allSecretsInNamespace; // We need the key from the selected secret
|
||||
|
||||
return mappedSecrets;
|
||||
}
|
||||
},
|
||||
paginateSecretsSetting: {
|
||||
requestSettings: this.paginatePageOptions,
|
||||
mapResult: (secrets) => {
|
||||
const mappedSecrets = this.mapSecrets(secrets);
|
||||
|
||||
this.secrets = secrets; // We need the key from the selected secret. When paginating we won't touch the store, so just pass back here
|
||||
|
||||
return mappedSecrets;
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
secretNames() {
|
||||
const mappedSecrets = this.secrets.map((secret) => ({
|
||||
label: secret.name,
|
||||
value: secret.name
|
||||
})).sort();
|
||||
|
||||
return [{ label: 'None', value: NONE }, ...sortBy(mappedSecrets, 'label')];
|
||||
},
|
||||
keys() {
|
||||
const secret = this.secrets.find((secret) => secret.name === this.name) || {};
|
||||
const secret = (this.secrets || []).find((secret) => secret.name === this.name) || {};
|
||||
|
||||
return Object.keys(secret.data || {}).map((key) => ({
|
||||
label: key,
|
||||
|
|
@ -107,6 +114,56 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Provide a set of options for the LabelSelect ([none, ...{label, value}])
|
||||
*/
|
||||
mapSecrets(secrets) {
|
||||
const mappedSecrets = secrets
|
||||
.reduce((res, s) => {
|
||||
if (s.kind === LABEL_SELECT_KINDS.NONE) {
|
||||
return res;
|
||||
}
|
||||
|
||||
if (s.id) {
|
||||
res.push({ label: s.name, value: s.name });
|
||||
} else {
|
||||
res.push(s);
|
||||
}
|
||||
|
||||
return res;
|
||||
}, []);
|
||||
|
||||
return [
|
||||
{
|
||||
label: 'None', value: NONE, kind: LABEL_SELECT_KINDS.NONE
|
||||
},
|
||||
...mappedSecrets
|
||||
];
|
||||
},
|
||||
|
||||
/**
|
||||
* @param [LabelSelectPaginationFunctionOptions] opts
|
||||
* @returns LabelSelectPaginationFunctionOptions
|
||||
*/
|
||||
paginatePageOptions(opts) {
|
||||
const { opts: { filter } } = opts;
|
||||
|
||||
const filters = !!filter ? [PaginationParamFilter.createSingleField({ field: 'metadata.name', value: filter })] : [];
|
||||
|
||||
filters.push(
|
||||
PaginationParamFilter.createSingleField({ field: 'metadata.namespace', value: this.namespace }),
|
||||
PaginationParamFilter.createSingleField({ field: 'metadata.fields.1', value: this.types.join(',') })
|
||||
);
|
||||
|
||||
return {
|
||||
...opts,
|
||||
filters,
|
||||
groupByNamespace: false,
|
||||
classify: true,
|
||||
sort: [{ asc: true, field: 'metadata.name' }],
|
||||
};
|
||||
},
|
||||
|
||||
updateSecretName(e) {
|
||||
if (e.value === this.none) {
|
||||
// The key should appear blank if the secret name is cleared
|
||||
|
|
@ -128,13 +185,16 @@ export default {
|
|||
<template>
|
||||
<div class="secret-selector show-key-selector">
|
||||
<div class="input-container">
|
||||
<LabeledSelect
|
||||
<ResourceLabeledSelect
|
||||
v-model="name"
|
||||
class="col span-6"
|
||||
:disabled="!isView && disabled"
|
||||
:options="secretNames"
|
||||
:loading="$fetchState.pending"
|
||||
:label="secretNameLabel"
|
||||
:mode="mode"
|
||||
:resource-type="SECRET"
|
||||
:paginated-resource-settings="paginateSecretsSetting"
|
||||
:all-resources-settings="allSecretsSettings"
|
||||
@selecting="updateSecretName"
|
||||
/>
|
||||
<LabeledSelect
|
||||
|
|
|
|||
|
|
@ -0,0 +1,139 @@
|
|||
import { debounce } from 'lodash';
|
||||
import { PropType, defineComponent } from 'vue';
|
||||
import { ComputedOptions, MethodOptions } from 'vue/types/v3-component-options';
|
||||
import { LabelSelectPaginateFn, LABEL_SELECT_NOT_OPTION_KINDS, LABEL_SELECT_KINDS } from '@shell/types/components/labeledSelect';
|
||||
import paginationUtils from '@shell/utils/pagination-utils';
|
||||
|
||||
interface Props {
|
||||
paginate?: LabelSelectPaginateFn
|
||||
}
|
||||
|
||||
interface Data {
|
||||
currentPage: number,
|
||||
search: string,
|
||||
pageSize: number,
|
||||
|
||||
page: any[],
|
||||
pages: number,
|
||||
totalResults: number,
|
||||
|
||||
paginating: boolean,
|
||||
|
||||
debouncedRequestPagination: Function
|
||||
}
|
||||
|
||||
interface Computed extends ComputedOptions {
|
||||
canPaginate: () => boolean,
|
||||
|
||||
canLoadMore: () => boolean,
|
||||
|
||||
optionsInPage: () => number,
|
||||
|
||||
optionCounts: () => string,
|
||||
}
|
||||
|
||||
interface Methods extends MethodOptions {
|
||||
loadMore: () => void
|
||||
setPaginationFilter: (filter: string) => void
|
||||
requestPagination: () => Promise<any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 'mixin' to provide pagination support to LabeledSelect
|
||||
*/
|
||||
export default defineComponent<Props, any, Data, Computed, Methods>({
|
||||
props: {
|
||||
paginate: {
|
||||
default: null,
|
||||
type: Function as PropType<LabelSelectPaginateFn>,
|
||||
}
|
||||
},
|
||||
|
||||
data(): Data {
|
||||
return {
|
||||
// Internal
|
||||
currentPage: 1,
|
||||
search: '',
|
||||
pageSize: 10,
|
||||
pages: 0,
|
||||
|
||||
debouncedRequestPagination: debounce(this.requestPagination, 700),
|
||||
|
||||
// External
|
||||
page: [],
|
||||
totalResults: 0,
|
||||
paginating: false,
|
||||
};
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
if (this.canPaginate) {
|
||||
await this.requestPagination();
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
canPaginate() {
|
||||
return !!this.paginate && paginationUtils.isEnabled({ rootGetters: this.$store.getters }, { store: 'cluster' });
|
||||
},
|
||||
|
||||
canLoadMore() {
|
||||
return this.pages > this.currentPage;
|
||||
},
|
||||
|
||||
optionsInPage() {
|
||||
// Number of genuine options (not groups, dividers, etc)
|
||||
return this.canPaginate ? this._options.filter((o: any) => {
|
||||
return o.kind !== LABEL_SELECT_KINDS.NONE && !LABEL_SELECT_NOT_OPTION_KINDS.includes(o.kind);
|
||||
}).length : 0;
|
||||
},
|
||||
|
||||
optionCounts() {
|
||||
if (!this.canPaginate || this.optionsInPage === this.totalResults) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return this.$store.getters['i18n/t']('labelSelect.pagination.counts', {
|
||||
count: this.optionsInPage,
|
||||
totalCount: this.totalResults
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
loadMore() {
|
||||
this.currentPage++;
|
||||
this.requestPagination();
|
||||
},
|
||||
|
||||
setPaginationFilter(filter: string) {
|
||||
this.paginating = true; // Do this before debounce
|
||||
this.currentPage = 1;
|
||||
this.search = filter;
|
||||
this.debouncedRequestPagination(true);
|
||||
},
|
||||
|
||||
async requestPagination(resetPage = false) {
|
||||
this.paginating = true;
|
||||
const paginate: LabelSelectPaginateFn = this.paginate as LabelSelectPaginateFn; // Checking is done via prop
|
||||
|
||||
const {
|
||||
page,
|
||||
pages,
|
||||
total
|
||||
} = await paginate({
|
||||
resetPage,
|
||||
pageContent: this.page || [],
|
||||
page: this.currentPage,
|
||||
filter: this.search,
|
||||
pageSize: this.pageSize,
|
||||
});
|
||||
|
||||
this.page = page;
|
||||
this.pages = pages || 0;
|
||||
this.totalResults = total || 0;
|
||||
|
||||
this.paginating = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
import { LabelSelectPaginateFnOptions, LabelSelectPaginateFnResponse } from '@shell/types/components/labeledSelect';
|
||||
import { PaginationArgs, PaginationParam, PaginationSort } from '@shell/types/store/pagination.types';
|
||||
|
||||
export interface LabelSelectPaginationFunctionOptions<T = any> {
|
||||
opts: LabelSelectPaginateFnOptions<T>,
|
||||
/**
|
||||
* Resource type
|
||||
*/
|
||||
type: string,
|
||||
/**
|
||||
* Store things
|
||||
*/
|
||||
ctx: { getters: any, dispatch: any}
|
||||
/**
|
||||
* Filters to apply. This mostly covers the text a user has entered, but could be other things like namespace
|
||||
*/
|
||||
filters?: PaginationParam[],
|
||||
/**
|
||||
* How to sort the response
|
||||
*/
|
||||
sort?: PaginationSort[],
|
||||
/**
|
||||
* Vuex store name
|
||||
*/
|
||||
store?: string,
|
||||
/**
|
||||
* True if the options returned should be grouped by namespace
|
||||
*/
|
||||
groupByNamespace?: boolean,
|
||||
|
||||
/**
|
||||
* Convert the results from JSON object to Rancher model class instance
|
||||
*/
|
||||
classify?: boolean,
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a helper function to cover common functionality that could happen when a LabelSelect requests a new page
|
||||
*/
|
||||
export async function labelSelectPaginationFunction<T>({
|
||||
opts,
|
||||
type,
|
||||
ctx,
|
||||
filters = [],
|
||||
sort = [{ asc: true, field: 'metadata.namespace' }, { asc: true, field: 'metadata.name' }],
|
||||
store = 'cluster',
|
||||
groupByNamespace = true,
|
||||
classify = false,
|
||||
}: LabelSelectPaginationFunctionOptions<T>): Promise<LabelSelectPaginateFnResponse<T>> {
|
||||
const {
|
||||
pageContent, page, pageSize, resetPage
|
||||
} = opts;
|
||||
|
||||
try {
|
||||
// Construct params for request
|
||||
|
||||
const pagination = new PaginationArgs({
|
||||
page,
|
||||
pageSize,
|
||||
sort,
|
||||
filters
|
||||
});
|
||||
const url = ctx.getters[`${ store }/urlFor`](type, null, { pagination });
|
||||
// Make request (note we're not bothering to persist anything to the store, response is transient)
|
||||
const res = await ctx.dispatch(`${ store }/request`, { url });
|
||||
let data = res.data;
|
||||
|
||||
if (classify) {
|
||||
data = await ctx.dispatch('cluster/createMany', data);
|
||||
}
|
||||
|
||||
const options = resetPage ? data : pageContent.concat(data);
|
||||
|
||||
// Create the new option collection by...
|
||||
let resPage: any[];
|
||||
|
||||
if (groupByNamespace) {
|
||||
// ... grouping by namespace
|
||||
const namespaced: { [ns: string]: T[]} = {};
|
||||
|
||||
options.forEach((option: any) => {
|
||||
const ns = option.metadata.namespace;
|
||||
|
||||
if (option.kind === 'group') { // this could contain a previous option set which contains groups
|
||||
return;
|
||||
}
|
||||
if (!namespaced[ns]) {
|
||||
namespaced[ns] = [];
|
||||
}
|
||||
namespaced[ns].push(option);
|
||||
});
|
||||
|
||||
resPage = [];
|
||||
|
||||
// ... then sort groups by name and combined into a single array
|
||||
Object.keys(namespaced).sort().forEach((ns) => {
|
||||
resPage.push({
|
||||
kind: 'group',
|
||||
icon: 'icon-namespace',
|
||||
id: ns,
|
||||
metadata: { name: ns },
|
||||
disabled: true,
|
||||
});
|
||||
resPage = resPage.concat(namespaced[ns]);
|
||||
});
|
||||
} else {
|
||||
resPage = options;
|
||||
}
|
||||
|
||||
return {
|
||||
page: resPage,
|
||||
pages: res.pages,
|
||||
total: res.count
|
||||
};
|
||||
} catch (err) {
|
||||
console.error(err); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
return {
|
||||
page: [], pages: 0, total: 0
|
||||
};
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { PSADimension, PSALevel, PSAMode } from '@shell/types/pod-security-admission';
|
||||
import { PSADimension, PSALevel, PSAMode } from '@shell/types/resources/pod-security-admission';
|
||||
|
||||
/**
|
||||
* All the PSA labels are created with this prefix, so we can use this to identify them
|
||||
|
|
|
|||
|
|
@ -2,15 +2,18 @@
|
|||
import createEditView from '@shell/mixins/create-edit-view';
|
||||
import CruResource from '@shell/components/CruResource';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import ResourceLabeledSelect from '@shell/components/form/ResourceLabeledSelect';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { CONFIG_MAP } from '@shell/config/types';
|
||||
import { PaginationParamFilter } from '@shell/types/store/pagination.types';
|
||||
|
||||
const providers = ['aks', 'docker', 'eks', 'gke', 'k3s', 'minikube', 'rke-windows', 'rke', 'rke2'];
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CruResource, LabeledSelect, LabeledInput, NameNsDescription
|
||||
CruResource, LabeledSelect, ResourceLabeledSelect, LabeledInput, NameNsDescription
|
||||
},
|
||||
|
||||
mixins: [createEditView],
|
||||
|
|
@ -29,16 +32,19 @@ export default {
|
|||
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
this.configMaps = await this.$store.dispatch('cluster/findAll', { type: CONFIG_MAP });
|
||||
},
|
||||
|
||||
data() {
|
||||
if (!this.value.spec) {
|
||||
this.$set(this.value, 'spec', {});
|
||||
}
|
||||
|
||||
return { configMaps: [], providers };
|
||||
return {
|
||||
CONFIG_MAP,
|
||||
providers,
|
||||
configMapPaginateSettings: {
|
||||
labelSelectOptions: { 'get-option-label': (opt) => opt?.metadata?.name || opt.id || opt },
|
||||
requestSettings: this.pageRequestSettings,
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
|
@ -55,7 +61,31 @@ export default {
|
|||
this.$set(this.value.spec, 'customBenchmarkConfigMapNamespace', namespace);
|
||||
}
|
||||
},
|
||||
...mapGetters({ t: 'i18n/t' })
|
||||
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* @param [LabelSelectPaginationFunctionOptions] opts
|
||||
* @returns LabelSelectPaginationFunctionOptions
|
||||
*/
|
||||
pageRequestSettings(opts) {
|
||||
const { opts: { filter } } = opts;
|
||||
|
||||
return {
|
||||
...opts,
|
||||
classify: true,
|
||||
filters: !!filter ? [PaginationParamFilter.createMultipleFields([
|
||||
{
|
||||
field: 'metadata.name', value: filter, equals: true
|
||||
},
|
||||
{
|
||||
field: 'metadata.namespace', value: filter, equals: true
|
||||
},
|
||||
])] : []
|
||||
};
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
@ -89,13 +119,15 @@ export default {
|
|||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabeledSelect
|
||||
<ResourceLabeledSelect
|
||||
v-model="customConfigMap"
|
||||
:clearable="true"
|
||||
option-key="id"
|
||||
option-label="id"
|
||||
:options="configMaps"
|
||||
:mode="mode"
|
||||
:label="t('cis.customConfigMap')"
|
||||
:resource-type="CONFIG_MAP"
|
||||
:paginated-resource-settings="configMapPaginateSettings"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
<script>
|
||||
import KeyValue from '@shell/components/form/KeyValue';
|
||||
import Select from '@shell/components/form/Select';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
|
||||
export default {
|
||||
components: { KeyValue, Select },
|
||||
components: {
|
||||
KeyValue, Select, LabeledSelect
|
||||
},
|
||||
|
||||
props: {
|
||||
mode: {
|
||||
|
|
@ -23,11 +26,6 @@ export default {
|
|||
default: () => [],
|
||||
},
|
||||
|
||||
containers: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
|
||||
namespaces: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
|
|
@ -89,15 +87,18 @@ export default {
|
|||
</h3>
|
||||
<div class="row">
|
||||
<div class="col span-12">
|
||||
<Select
|
||||
<LabeledSelect
|
||||
v-model="value.container_names"
|
||||
class="lg"
|
||||
:options="containers"
|
||||
: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>
|
||||
|
|
|
|||
|
|
@ -6,14 +6,12 @@ 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, POD, SCHEMA
|
||||
} from '@shell/config/types';
|
||||
import { LOGGING, NAMESPACE, NODE, 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, uniq } from '@shell/utils/array';
|
||||
import { isArray } from '@shell/utils/array';
|
||||
import { matchRuleIsPopulated } from '@shell/models/logging.banzaicloud.io.flow';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { clone, set } from '@shell/utils/object';
|
||||
|
|
@ -55,7 +53,6 @@ export default {
|
|||
const hasAccessToOutputs = this.$store.getters[`cluster/schemaFor`](LOGGING.OUTPUT);
|
||||
const hasAccessToNamespaces = this.$store.getters[`cluster/schemaFor`](NAMESPACE);
|
||||
const hasAccessToNodes = this.$store.getters[`cluster/schemaFor`](NODE);
|
||||
const hasAccessToPods = this.$store.getters[`cluster/schemaFor`](POD);
|
||||
const isFlow = this.value.type === LOGGING.FLOW;
|
||||
|
||||
const getAllOrDefault = (type, hasAccess) => {
|
||||
|
|
@ -67,7 +64,6 @@ 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) ) {
|
||||
|
|
@ -204,17 +200,6 @@ 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();
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
|
@ -386,7 +371,6 @@ export default {
|
|||
:mode="mode"
|
||||
:namespaces="namespaceChoices"
|
||||
:nodes="nodeChoices"
|
||||
:containers="containerChoices"
|
||||
:is-cluster-flow="value.type === LOGGING.CLUSTER_FLOW"
|
||||
@remove="e=>removeMatch(props.row.i)"
|
||||
@input="e=>updateMatch(e,props.row.i)"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import { SECRET, LOGGING, SCHEMA } from '@shell/config/types';
|
||||
import { LOGGING, SCHEMA } from '@shell/config/types';
|
||||
import Tabbed from '@shell/components/Tabbed';
|
||||
import Tab from '@shell/components/Tabbed/Tab';
|
||||
import CruResource from '@shell/components/CruResource';
|
||||
|
|
@ -24,10 +24,6 @@ export default {
|
|||
|
||||
mixins: [CreateEditView],
|
||||
|
||||
async fetch() {
|
||||
await this.$store.dispatch('cluster/findAll', { type: SECRET });
|
||||
},
|
||||
|
||||
data() {
|
||||
const schemas = this.$store.getters['cluster/all'](SCHEMA);
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import CruResource from '@shell/components/CruResource.vue';
|
|||
import PodSecurityAdmission from '@shell/components/PodSecurityAdmission.vue';
|
||||
import Loading from '@shell/components/Loading.vue';
|
||||
import NameNsDescription from '@shell/components/form/NameNsDescription.vue';
|
||||
import { PSA, PSAConfig, PSADefaults, PSAExemptions } from '@shell/types/pod-security-admission';
|
||||
import { PSA, PSAConfig, PSADefaults, PSAExemptions } from '@shell/types/resources/pod-security-admission';
|
||||
import { PSADimensions } from '@shell/config/pod-security-admission';
|
||||
import { MANAGEMENT } from '@shell/config/types';
|
||||
|
||||
|
|
|
|||
|
|
@ -77,6 +77,11 @@ export default Vue.extend({
|
|||
type: Boolean
|
||||
},
|
||||
|
||||
filterable: {
|
||||
default: true,
|
||||
type: Boolean
|
||||
},
|
||||
|
||||
rules: {
|
||||
default: () => [],
|
||||
type: Array,
|
||||
|
|
@ -116,7 +121,11 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
isSearchable(): boolean {
|
||||
const { searchable } = this;
|
||||
const { searchable, canPaginate } = this as any; // This will be resolved when we migrate from mixin
|
||||
|
||||
if (canPaginate) {
|
||||
return true;
|
||||
}
|
||||
const options = ( this.options || [] );
|
||||
|
||||
if (searchable || options.length >= 10) {
|
||||
|
|
@ -125,6 +134,17 @@ export default Vue.extend({
|
|||
|
||||
return false;
|
||||
},
|
||||
|
||||
isFilterable(): boolean {
|
||||
const { filterable, canPaginate } = this as any; // This will be resolved when we migrate from mixin
|
||||
|
||||
if (canPaginate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return filterable;
|
||||
},
|
||||
|
||||
validationMessage(): string | undefined {
|
||||
// we want to grab the required rule passed in if we can but if it's not there then we can just grab it from the formRulesGenerator
|
||||
const requiredRule = this.rules.find((rule: any) => rule?.name === 'required') as Function;
|
||||
|
|
|
|||
|
|
@ -387,10 +387,9 @@ export default {
|
|||
/>
|
||||
<Banner
|
||||
v-if="!steveCacheEnabled"
|
||||
v-clean-html="t(`performance.serverPagination.featureFlag`, { ffUrl }, true)"
|
||||
color="warning"
|
||||
>
|
||||
<span v-clean-html="t(`performance.serverPagination.featureFlag`, { ffUrl }, true)" />
|
||||
</Banner>
|
||||
/>
|
||||
<Checkbox
|
||||
v-model="value.serverPagination.enabled"
|
||||
:mode="mode"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
export const LABEL_SELECT_KINDS = {
|
||||
GROUP: 'group',
|
||||
DIVIDER: 'divider',
|
||||
NONE: 'none',
|
||||
};
|
||||
|
||||
export const LABEL_SELECT_NOT_OPTION_KINDS = [
|
||||
LABEL_SELECT_KINDS.GROUP,
|
||||
LABEL_SELECT_KINDS.DIVIDER,
|
||||
];
|
||||
|
||||
/**
|
||||
* Options used When LabelSelect requests a new page
|
||||
*/
|
||||
export interface LabelSelectPaginateFnOptions<T = any> {
|
||||
/**
|
||||
* Current page
|
||||
*/
|
||||
pageContent: T[],
|
||||
/**
|
||||
* page number to fetch
|
||||
*/
|
||||
page: number,
|
||||
/**
|
||||
* number of items in the page to fetch
|
||||
*/
|
||||
pageSize: number,
|
||||
/**
|
||||
* filter pagination filter. this is just a text string associated with user entered text
|
||||
*/
|
||||
filter: string,
|
||||
/**
|
||||
* true if the result should only contain the fetched page, false if the result should be added to the pageContent
|
||||
*/
|
||||
resetPage: boolean,
|
||||
}
|
||||
|
||||
/**
|
||||
* Response that LabelSelect needs when it's requested a new page
|
||||
*/
|
||||
export interface LabelSelectPaginateFnResponse<T = any> {
|
||||
page: T[],
|
||||
pages: number,
|
||||
total: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Function called when LabelSelect needs a new page
|
||||
*/
|
||||
export type LabelSelectPaginateFn<T = any> = (opts: LabelSelectPaginateFnOptions<T>) => Promise<LabelSelectPaginateFnResponse<T>>
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { PSA } from '@shell/types/pod-security-admission';
|
||||
import { PSA } from '@shell/types/resources/pod-security-admission';
|
||||
import { getPSATooltipsDescription } from '@shell/utils/pod-security-admission';
|
||||
|
||||
describe('fX: getPSATooltipsDescription', () => {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,18 @@ import { sameArrayObjects } from '@shell/utils/array';
|
|||
import { isEqual } from '@shell/utils/object';
|
||||
import { STEVE_CACHE } from '@shell/store/features';
|
||||
import { getPerformanceSetting } from '@shell/utils/settings';
|
||||
import { DEFAULT_PERF_SETTING } from '@shell/config/settings';
|
||||
|
||||
/**
|
||||
* Given the vai cache changes haven't merged, work around the settings that are blocked by it
|
||||
*
|
||||
* Once cache is merged (pre 2.9.0) this will be removed
|
||||
*/
|
||||
const TEMP_VAI_CACHE_MERGED = false;
|
||||
/**
|
||||
* Given above, just a dev thing
|
||||
*/
|
||||
const TEMP_PERF_ENABLED = false;
|
||||
|
||||
/**
|
||||
* Helper functions for server side pagination
|
||||
|
|
@ -32,6 +44,11 @@ class PaginationUtils {
|
|||
return perf.serverPagination;
|
||||
}
|
||||
|
||||
isSteveCacheEnabled({ rootGetters }: any): boolean {
|
||||
// We always get Feature flags as part of start up (see `dispatch('features/loadServer')` in loadManagement)
|
||||
return TEMP_VAI_CACHE_MERGED || rootGetters['features/get']?.(STEVE_CACHE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is pagination enabled at a global level or for a specific resource
|
||||
*/
|
||||
|
|
@ -41,13 +58,16 @@ class PaginationUtils {
|
|||
id: string,
|
||||
}
|
||||
}) {
|
||||
const settings = this.getSettings({ rootGetters });
|
||||
|
||||
// Cache must be enabled to support pagination api
|
||||
if (!rootGetters['features/get']?.(STEVE_CACHE)) {
|
||||
if (!this.isSteveCacheEnabled({ rootGetters })) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const settings = TEMP_PERF_ENABLED ? {
|
||||
...DEFAULT_PERF_SETTING.serverPagination,
|
||||
enabled: true,
|
||||
} : this.getSettings({ rootGetters });
|
||||
|
||||
// No setting, not enabled
|
||||
if (!settings?.enabled) {
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { reduce, filter, keys } from 'lodash';
|
||||
import { PSALabelPrefix, PSALabelsNamespaces } from '@shell/config/pod-security-admission';
|
||||
import { camelToTitle } from '@shell/utils/string';
|
||||
import { PSA } from '@shell/types/pod-security-admission';
|
||||
import { PSA } from '@shell/types/resources/pod-security-admission';
|
||||
|
||||
/**
|
||||
* Return PSA labels present in the resource
|
||||
|
|
|
|||
Loading…
Reference in New Issue