@rancher/shell 3.0.11 → 3.0.12-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/images/providers/entraid-black.svg +4 -0
- package/assets/images/providers/entraid.svg +9 -0
- package/assets/images/vendor/entraid.svg +9 -0
- package/assets/styles/app.scss +0 -1
- package/assets/styles/base/_mixins.scss +31 -0
- package/assets/styles/base/_variables.scss +2 -0
- package/assets/styles/themes/_modern.scss +6 -5
- package/assets/translations/en-us.yaml +24 -21
- package/assets/translations/zh-hans.yaml +4 -11
- package/chart/__tests__/S3.test.ts +10 -3
- package/components/CountBox.vue +20 -0
- package/components/CreateDriver.vue +0 -12
- package/components/DetailText.vue +12 -3
- package/components/EmptyProductPage.vue +76 -0
- package/components/Resource/Detail/CopyToClipboard.vue +1 -2
- package/components/Resource/Detail/Metadata/KeyValueRow.vue +9 -3
- package/components/Resource/Detail/TitleBar/__tests__/__snapshots__/index.test.ts.snap +31 -0
- package/components/Resource/Detail/TitleBar/__tests__/index.test.ts +45 -1
- package/components/Resource/Detail/TitleBar/index.vue +1 -1
- package/components/Resource/Detail/ViewOptions/__tests__/__snapshots__/index.test.ts.snap +9 -0
- package/components/Resource/Detail/ViewOptions/__tests__/index.test.ts +62 -0
- package/components/Resource/Detail/ViewOptions/index.vue +2 -1
- package/components/ResourceList/Masthead.vue +25 -2
- package/components/SelectIconGrid.vue +5 -0
- package/components/SideNav.vue +13 -0
- package/components/__tests__/CountBox.test.ts +72 -0
- package/components/__tests__/DetailText.test.ts +113 -0
- package/components/__tests__/PromptModal.test.ts +2 -0
- package/components/fleet/FleetClusterTargets/index.vue +18 -1
- package/components/fleet/FleetClusters.vue +1 -0
- package/components/fleet/__tests__/FleetClusters.test.ts +71 -0
- package/components/form/InputWithSelect.vue +18 -10
- package/components/form/KeyValue.vue +17 -1
- package/components/form/LabeledSelect.vue +82 -24
- package/components/form/NodeScheduling.vue +17 -3
- package/components/form/PrivateRegistry.vue +69 -0
- package/components/form/Select.vue +73 -56
- package/components/form/ServiceNameSelect.vue +13 -11
- package/components/form/__tests__/KeyValue.test.ts +66 -0
- package/components/form/__tests__/NodeScheduling.test.ts +9 -0
- package/components/form/__tests__/PrivateRegistry.test.ts +133 -0
- package/components/form/labeled-select-utils/useLabeledSelectPagination.ts +138 -0
- package/components/formatter/WorkloadHealthScale.vue +3 -1
- package/components/nav/Group.vue +33 -9
- package/components/nav/Header.vue +56 -10
- package/components/nav/NotificationCenter/Notification.vue +4 -1
- package/components/nav/NotificationCenter/NotificationHeader.vue +20 -8
- package/components/nav/NotificationCenter/__tests__/NotificationHeader.test.ts +80 -0
- package/components/nav/TopLevelMenu.vue +15 -1
- package/components/nav/Type.vue +8 -7
- package/components/nav/WindowManager/index.vue +2 -1
- package/components/nav/WorkspaceSwitcher.vue +13 -0
- package/components/nav/__tests__/Group.test.ts +67 -0
- package/components/nav/__tests__/Header.test.ts +235 -0
- package/components/nav/__tests__/Type.test.ts +20 -3
- package/components/templates/default.vue +34 -4
- package/components/templates/home.vue +12 -25
- package/components/templates/plain.vue +13 -26
- package/composables/useLabeledFormElement.ts +10 -2
- package/composables/useLabeledSelect.ts +60 -0
- package/composables/useUserRetentionValidation.ts +1 -49
- package/config/cookies.js +0 -1
- package/config/labels-annotations.js +1 -0
- package/config/pagination-table-headers.js +8 -1
- package/config/product/apps.js +2 -1
- package/config/product/auth.js +1 -0
- package/config/product/backup.js +1 -0
- package/config/product/compliance.js +1 -1
- package/config/product/explorer.js +25 -6
- package/config/product/fleet.js +1 -0
- package/config/product/gatekeeper.js +1 -0
- package/config/product/istio.js +1 -0
- package/config/product/logging.js +1 -0
- package/config/product/longhorn.js +2 -1
- package/config/product/manager.js +1 -0
- package/config/product/monitoring.js +1 -0
- package/config/product/navlinks.js +1 -0
- package/config/product/neuvector.js +2 -1
- package/config/product/settings.js +1 -0
- package/config/product/uiplugins.js +1 -0
- package/config/query-params.js +1 -0
- package/config/router/routes.js +0 -8
- package/core/__tests__/plugin-products-helpers.test.ts +454 -0
- package/core/__tests__/plugin-products.test.ts +3810 -0
- package/core/extension-manager-impl.js +30 -1
- package/core/plugin-products-base.ts +392 -0
- package/core/plugin-products-extending.ts +44 -0
- package/core/plugin-products-helpers.ts +263 -0
- package/core/plugin-products-top-level.ts +66 -0
- package/core/plugin-products-type-guards.ts +33 -0
- package/core/plugin-products.ts +50 -0
- package/core/plugin-types.ts +237 -0
- package/core/plugin.ts +45 -10
- package/core/productDebugger.js +48 -0
- package/core/types.ts +97 -11
- package/detail/__tests__/__snapshots__/fleet.cattle.io.bundle.test.ts.snap +52 -0
- package/detail/__tests__/fleet.cattle.io.bundle.test.ts +171 -0
- package/detail/__tests__/management.cattle.io.fleetworkspace.test.ts +128 -0
- package/detail/fleet.cattle.io.bundle.vue +21 -34
- package/detail/management.cattle.io.fleetworkspace.vue +49 -0
- package/dialog/ExtensionCatalogInstallDialog.vue +1 -1
- package/dialog/InstallExtensionDialog.vue +6 -27
- package/dialog/UninstallExistingExtensionDialog.vue +141 -0
- package/dialog/UninstallExtensionDialog.vue +4 -26
- package/dialog/__tests__/UninstallExistingExtensionDialog.test.ts +114 -0
- package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +1 -0
- package/edit/__tests__/fleet.cattle.io.helmop.test.ts +9 -0
- package/edit/__tests__/kontainerDriver.test.ts +0 -13
- package/edit/__tests__/nodeDriver.test.ts +5 -11
- package/edit/__tests__/resources.cattle.io.restore.test.ts +9 -0
- package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/General.test.ts.snap +6 -0
- package/edit/auth/__tests__/oidc.test.ts +54 -0
- package/edit/auth/azuread.vue +1 -1
- package/edit/auth/oidc.vue +8 -0
- package/edit/kontainerDriver.vue +1 -2
- package/edit/nodeDriver.vue +0 -2
- package/edit/provisioning.cattle.io.cluster/AgentEnv.vue +1 -0
- package/edit/provisioning.cattle.io.cluster/__tests__/AgentEnv.test.ts +25 -0
- package/edit/provisioning.cattle.io.cluster/__tests__/Ingress.test.ts +176 -0
- package/edit/provisioning.cattle.io.cluster/index.vue +70 -99
- package/edit/provisioning.cattle.io.cluster/rke2.vue +4 -1
- package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +6 -0
- package/edit/provisioning.cattle.io.cluster/tabs/Ingress.vue +7 -2
- package/initialize/App.vue +29 -2
- package/initialize/install-plugins.js +0 -2
- package/list/__tests__/management.cattle.io.feature.test.ts +105 -0
- package/list/catalog.cattle.io.app.vue +25 -5
- package/list/management.cattle.io.feature.vue +1 -1
- package/list/management.cattle.io.fleetworkspace.vue +8 -0
- package/list/provisioning.cattle.io.cluster.vue +0 -1
- package/list/workload.vue +11 -4
- package/machine-config/amazonec2.vue +1 -0
- package/mixins/chart.js +40 -9
- package/mixins/resource-fetch.js +12 -3
- package/models/__tests__/catalog.cattle.io.app.test.ts +15 -1
- package/models/__tests__/catalog.cattle.io.clusterrepo.test.ts +84 -0
- package/models/__tests__/chart.test.ts +99 -6
- package/models/__tests__/management.cattle.io.feature.test.ts +131 -0
- package/models/__tests__/monitoring.coreos.com.alertmanagerconfig.test.ts +98 -0
- package/models/catalog.cattle.io.app.js +21 -17
- package/models/catalog.cattle.io.clusterrepo.js +39 -11
- package/models/chart.js +33 -19
- package/models/fleet-application.js +1 -1
- package/models/fleet.cattle.io.bundle.js +1 -1
- package/models/kontainerdriver.js +11 -0
- package/models/management.cattle.io.authconfig.js +5 -1
- package/models/management.cattle.io.cluster.js +0 -53
- package/models/management.cattle.io.feature.js +3 -3
- package/models/management.cattle.io.kontainerdriver.js +1 -26
- package/models/monitoring.coreos.com.alertmanagerconfig.js +31 -17
- package/models/nodedriver.js +7 -0
- package/models/pod.js +18 -0
- package/models/workload.js +20 -2
- package/package.json +13 -13
- package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +0 -1
- package/pages/c/_cluster/apps/charts/__tests__/chart.test.ts +189 -0
- package/pages/c/_cluster/apps/charts/__tests__/index.test.ts +55 -0
- package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +53 -0
- package/pages/c/_cluster/apps/charts/chart.vue +217 -33
- package/pages/c/_cluster/apps/charts/index.vue +2 -2
- package/pages/c/_cluster/apps/charts/install.vue +8 -3
- package/pages/c/_cluster/auth/user.retention/index.vue +55 -22
- package/pages/c/_cluster/manager/drivers/kontainerDriver/index.vue +5 -7
- package/pages/c/_cluster/settings/brand.vue +4 -4
- package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +39 -2
- package/pages/c/_cluster/uiplugins/__tests__/PluginInfoPanel.test.ts +61 -0
- package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +246 -23
- package/pages/c/_cluster/uiplugins/index.vue +166 -62
- package/plugins/dashboard-store/__tests__/resource-class.test.ts +1 -0
- package/plugins/dashboard-store/actions.js +3 -2
- package/plugins/dashboard-store/resource-class.js +62 -6
- package/plugins/plugin.js +16 -0
- package/plugins/steve/steve-pagination-utils.ts +7 -0
- package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +205 -1
- package/rancher-components/Form/LabeledInput/LabeledInput.vue +82 -4
- package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +1 -1
- package/scripts/test-plugins-build.sh +5 -2
- package/scripts/typegen.sh +13 -1
- package/server/server-middleware.js +2 -2
- package/static/humans.txt +1 -0
- package/static/robots.txt +34 -0
- package/static/welcome-cow.svg +18 -0
- package/store/__tests__/catalog.test.ts +161 -11
- package/store/__tests__/type-map.test.ts +84 -24
- package/store/auth.js +0 -3
- package/store/catalog.js +60 -8
- package/store/type-map.js +42 -3
- package/tsconfig.paths.json +1 -0
- package/types/resources/pod.ts +18 -0
- package/types/shell/index.d.ts +8539 -2938
- package/types/store/dashboard-store.types.ts +5 -0
- package/types/store/pagination.types.ts +6 -0
- package/utils/__tests__/git.test.ts +270 -0
- package/utils/__tests__/inactivity.test.ts +316 -0
- package/utils/__tests__/object.test.ts +77 -0
- package/utils/__tests__/time.test.ts +14 -1
- package/utils/__tests__/url.test.ts +246 -0
- package/utils/axios.js +1 -4
- package/utils/dynamic-importer.js +3 -2
- package/utils/object.js +33 -2
- package/utils/pagination-utils.ts +1 -1
- package/utils/time.ts +5 -0
- package/utils/uiplugins.ts +12 -16
- package/utils/validators/__tests__/private-registry.test.ts +76 -0
- package/utils/validators/private-registry.ts +28 -0
- package/vue.config.js +0 -9
- package/assets/images/providers/azuread-black.svg +0 -22
- package/assets/images/providers/azuread.svg +0 -25
- package/assets/images/vendor/azuread.svg +0 -18
- package/assets/styles/fonts/_dots.scss +0 -18
- package/components/EmberPage.vue +0 -622
- package/components/EmberPageView.vue +0 -39
- package/components/form/labeled-select-utils/labeled-select-pagination.ts +0 -116
- package/mixins/labeled-form-element.ts +0 -225
- package/pages/c/_cluster/explorer/tools/pages/_page.vue +0 -28
- package/pages/c/_cluster/manager/pages/_page.vue +0 -22
- package/pages/c/_cluster/mcapps/pages/_page.vue +0 -22
- package/plugins/ember-cookie.js +0 -17
- package/utils/ember-page.js +0 -30
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { shallowMount } from '@vue/test-utils';
|
|
2
|
+
import PrivateRegistry from '@shell/components/form/PrivateRegistry.vue';
|
|
3
|
+
import { Checkbox } from '@components/Form/Checkbox';
|
|
4
|
+
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
|
|
5
|
+
|
|
6
|
+
const defaultMocks = {
|
|
7
|
+
$store: {
|
|
8
|
+
getters: {
|
|
9
|
+
'i18n/t': (text: string) => text,
|
|
10
|
+
t: (text: string) => text,
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const mountPrivateRegistry = (props = {}) => {
|
|
16
|
+
return shallowMount(PrivateRegistry, {
|
|
17
|
+
props: {
|
|
18
|
+
mode: 'edit',
|
|
19
|
+
...props
|
|
20
|
+
},
|
|
21
|
+
global: { mocks: defaultMocks }
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
describe('privateRegistry', () => {
|
|
26
|
+
it('should render the info banner', () => {
|
|
27
|
+
const wrapper = mountPrivateRegistry();
|
|
28
|
+
const banner = wrapper.find('[color="info"]');
|
|
29
|
+
|
|
30
|
+
expect(banner.exists()).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should render the enable checkbox', () => {
|
|
34
|
+
const wrapper = mountPrivateRegistry();
|
|
35
|
+
const checkbox = wrapper.findComponent(Checkbox);
|
|
36
|
+
|
|
37
|
+
expect(checkbox.exists()).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should not show the URL input when no value is provided', () => {
|
|
41
|
+
const wrapper = mountPrivateRegistry();
|
|
42
|
+
|
|
43
|
+
expect(wrapper.findComponent(LabeledInput).exists()).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should show the URL input when a value is provided', () => {
|
|
47
|
+
const wrapper = mountPrivateRegistry({ value: 'registry.example.com' });
|
|
48
|
+
|
|
49
|
+
expect(wrapper.findComponent(LabeledInput).exists()).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should show the URL input when checkbox is checked', async() => {
|
|
53
|
+
const wrapper = mountPrivateRegistry();
|
|
54
|
+
|
|
55
|
+
expect(wrapper.findComponent(LabeledInput).exists()).toBe(false);
|
|
56
|
+
|
|
57
|
+
const checkbox = wrapper.findComponent(Checkbox);
|
|
58
|
+
|
|
59
|
+
await checkbox.vm.$emit('update:value', true);
|
|
60
|
+
await wrapper.vm.$nextTick();
|
|
61
|
+
|
|
62
|
+
expect(wrapper.findComponent(LabeledInput).exists()).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should emit update:value with null when checkbox is unchecked', async() => {
|
|
66
|
+
const wrapper = mountPrivateRegistry({ value: 'registry.example.com' });
|
|
67
|
+
|
|
68
|
+
const checkbox = wrapper.findComponent(Checkbox);
|
|
69
|
+
|
|
70
|
+
await checkbox.vm.$emit('update:value', false);
|
|
71
|
+
await wrapper.vm.$nextTick();
|
|
72
|
+
|
|
73
|
+
expect(wrapper.emitted('update:value')).toHaveLength(1);
|
|
74
|
+
expect(wrapper.emitted('update:value')![0]).toStrictEqual([null]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should emit update:value when the URL input changes', async() => {
|
|
78
|
+
const wrapper = mountPrivateRegistry({ value: 'registry.example.com' });
|
|
79
|
+
const input = wrapper.findComponent(LabeledInput);
|
|
80
|
+
|
|
81
|
+
await input.vm.$emit('update:value', 'new-registry.example.com');
|
|
82
|
+
|
|
83
|
+
expect(wrapper.emitted('update:value')).toHaveLength(1);
|
|
84
|
+
expect(wrapper.emitted('update:value')![0]).toStrictEqual(['new-registry.example.com']);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should auto-enable the checkbox when value changes from null to a string', async() => {
|
|
88
|
+
const wrapper = mountPrivateRegistry();
|
|
89
|
+
|
|
90
|
+
expect(wrapper.findComponent(LabeledInput).exists()).toBe(false);
|
|
91
|
+
|
|
92
|
+
await wrapper.setProps({ value: 'registry.example.com' });
|
|
93
|
+
|
|
94
|
+
expect(wrapper.findComponent(LabeledInput).exists()).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should pass rules to the URL input', () => {
|
|
98
|
+
const mockRule = jest.fn();
|
|
99
|
+
const wrapper = mountPrivateRegistry({
|
|
100
|
+
value: 'registry.example.com',
|
|
101
|
+
rules: [mockRule]
|
|
102
|
+
});
|
|
103
|
+
const input = wrapper.findComponent(LabeledInput);
|
|
104
|
+
|
|
105
|
+
expect(input.attributes('rules')).toBeDefined();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should apply custom data-testid to checkbox when provided', () => {
|
|
109
|
+
const wrapper = mountPrivateRegistry({ checkboxTestId: 'my-checkbox' });
|
|
110
|
+
const checkbox = wrapper.findComponent(Checkbox);
|
|
111
|
+
|
|
112
|
+
expect(checkbox.attributes('data-testid')).toBe('my-checkbox');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should apply custom data-testid to input when provided', () => {
|
|
116
|
+
const wrapper = mountPrivateRegistry({
|
|
117
|
+
value: 'registry.example.com',
|
|
118
|
+
inputTestId: 'my-input'
|
|
119
|
+
});
|
|
120
|
+
const input = wrapper.findComponent(LabeledInput);
|
|
121
|
+
|
|
122
|
+
expect(input.attributes('data-testid')).toBe('my-input');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should not set data-testid when not provided', () => {
|
|
126
|
+
const wrapper = mountPrivateRegistry({ value: 'registry.example.com' });
|
|
127
|
+
const checkbox = wrapper.findComponent(Checkbox);
|
|
128
|
+
const input = wrapper.findComponent(LabeledInput);
|
|
129
|
+
|
|
130
|
+
expect(checkbox.attributes('data-testid')).toBeUndefined();
|
|
131
|
+
expect(input.attributes('data-testid')).toBeUndefined();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* composable to provide pagination support to LabeledSelect
|
|
3
|
+
*/
|
|
4
|
+
import {
|
|
5
|
+
ref, computed, onMounted, ComputedRef, Ref, PropType
|
|
6
|
+
} from 'vue';
|
|
7
|
+
import { useStore } from 'vuex';
|
|
8
|
+
import { debounce } from 'lodash';
|
|
9
|
+
import { LabelSelectPaginateFn, LABEL_SELECT_NOT_OPTION_KINDS, LABEL_SELECT_KINDS } from '@shell/types/components/labeledSelect';
|
|
10
|
+
|
|
11
|
+
interface LabeledSelectPaginationProps {
|
|
12
|
+
paginate?: LabelSelectPaginateFn | null;
|
|
13
|
+
inStore?: string;
|
|
14
|
+
resourceType?: string | null;
|
|
15
|
+
options?: Array<any>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface UseLabeledSelectPagination {
|
|
19
|
+
canPaginate: ComputedRef<boolean>;
|
|
20
|
+
canLoadMore: ComputedRef<boolean>;
|
|
21
|
+
optionCounts: ComputedRef<string>;
|
|
22
|
+
_options: ComputedRef<any[]>;
|
|
23
|
+
pages: Ref<number>;
|
|
24
|
+
totalResults: Ref<number>;
|
|
25
|
+
paginating: Ref<boolean>;
|
|
26
|
+
loadMore: () => void;
|
|
27
|
+
setPaginationFilter: (filter: string) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const labeledSelectPaginationProps = {
|
|
31
|
+
paginate: {
|
|
32
|
+
default: null,
|
|
33
|
+
type: Function as PropType<LabelSelectPaginateFn>,
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
inStore: {
|
|
37
|
+
type: String,
|
|
38
|
+
default: 'cluster',
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resource to show
|
|
43
|
+
*/
|
|
44
|
+
resourceType: {
|
|
45
|
+
type: String,
|
|
46
|
+
default: null,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const useLabeledSelectPagination = (props: LabeledSelectPaginationProps): UseLabeledSelectPagination => {
|
|
51
|
+
const store = useStore();
|
|
52
|
+
|
|
53
|
+
// Internal
|
|
54
|
+
const currentPage = ref(1);
|
|
55
|
+
const search = ref('');
|
|
56
|
+
const pageSize = ref(10);
|
|
57
|
+
const pages = ref(0);
|
|
58
|
+
|
|
59
|
+
// External
|
|
60
|
+
const page = ref<any[]>([]);
|
|
61
|
+
const totalResults = ref(0);
|
|
62
|
+
const paginating = ref(false);
|
|
63
|
+
|
|
64
|
+
const canPaginate = computed(() => {
|
|
65
|
+
return !!props.paginate && !!props.resourceType && store.getters[`${ props.inStore }/paginationEnabled`](props.resourceType);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const _options = computed(() => canPaginate.value ? page.value : (props.options || []));
|
|
69
|
+
|
|
70
|
+
const canLoadMore = computed(() => pages.value > currentPage.value);
|
|
71
|
+
|
|
72
|
+
const optionsInPage = computed(() => {
|
|
73
|
+
// Number of genuine options (not groups, dividers, etc)
|
|
74
|
+
return canPaginate.value ? _options.value.filter((o: any) => {
|
|
75
|
+
return o.kind !== LABEL_SELECT_KINDS.NONE && !LABEL_SELECT_NOT_OPTION_KINDS.includes(o.kind);
|
|
76
|
+
}).length : 0;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const optionCounts = computed(() => {
|
|
80
|
+
if (!canPaginate.value || optionsInPage.value === totalResults.value) {
|
|
81
|
+
return '';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return store.getters['i18n/t']('labelSelect.pagination.counts', {
|
|
85
|
+
count: optionsInPage.value,
|
|
86
|
+
totalCount: totalResults.value
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const requestPagination = async(resetPage = false) => {
|
|
91
|
+
paginating.value = true;
|
|
92
|
+
|
|
93
|
+
const { page: p, pages: pg, total } = await (props.paginate as LabelSelectPaginateFn)({
|
|
94
|
+
resetPage,
|
|
95
|
+
pageContent: page.value || [],
|
|
96
|
+
page: currentPage.value,
|
|
97
|
+
filter: search.value,
|
|
98
|
+
pageSize: pageSize.value,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
page.value = p;
|
|
102
|
+
pages.value = pg || 0;
|
|
103
|
+
totalResults.value = total || 0;
|
|
104
|
+
paginating.value = false;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const debouncedRequestPagination = debounce(requestPagination, 700);
|
|
108
|
+
|
|
109
|
+
const setPaginationFilter = (filter: string) => {
|
|
110
|
+
paginating.value = true; // Do this before debounce
|
|
111
|
+
currentPage.value = 1;
|
|
112
|
+
search.value = filter;
|
|
113
|
+
debouncedRequestPagination(true);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const loadMore = () => {
|
|
117
|
+
currentPage.value++;
|
|
118
|
+
requestPagination();
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
onMounted(async() => {
|
|
122
|
+
if (canPaginate.value) {
|
|
123
|
+
await requestPagination();
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
canPaginate,
|
|
129
|
+
canLoadMore,
|
|
130
|
+
optionCounts,
|
|
131
|
+
_options,
|
|
132
|
+
pages,
|
|
133
|
+
totalResults,
|
|
134
|
+
paginating,
|
|
135
|
+
loadMore,
|
|
136
|
+
setPaginationFilter,
|
|
137
|
+
};
|
|
138
|
+
};
|
package/components/nav/Group.vue
CHANGED
|
@@ -82,7 +82,8 @@ export default {
|
|
|
82
82
|
const validRoute = filterLocationValidParams(this.$router, overviewRoute || {});
|
|
83
83
|
const route = this.$router.resolve(validRoute);
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
// Use .path instead of .fullPath to ignore query parameters and hashes when comparing routes
|
|
86
|
+
return this.$route.path === route?.path;
|
|
86
87
|
}
|
|
87
88
|
}
|
|
88
89
|
|
|
@@ -204,14 +205,14 @@ export default {
|
|
|
204
205
|
} else if (item.route) {
|
|
205
206
|
const navLevels = ['cluster', 'product', 'resource'];
|
|
206
207
|
const matchesNavLevel = navLevels.filter((param) => !this.$route.params[param] || this.$route.params[param] !== item.route.params[param]).length === 0;
|
|
207
|
-
const withoutHash = this.$route.hash ? this.$route.fullPath.slice(0, this.$route.fullPath.indexOf(this.$route.hash)) : this.$route.fullPath;
|
|
208
|
-
const withoutQuery = withoutHash.split('?')[0];
|
|
209
208
|
const validItemRoute = filterLocationValidParams(this.$router, item.route);
|
|
210
|
-
const itemFullPath = this.$router.resolve(validItemRoute).fullPath;
|
|
211
209
|
|
|
212
|
-
|
|
210
|
+
// Use .path instead of .fullPath to ignore query parameters and hashes when comparing routes
|
|
211
|
+
const itemPath = this.$router.resolve(validItemRoute).path;
|
|
212
|
+
|
|
213
|
+
if (matchesNavLevel || itemPath === this.$route.path) {
|
|
213
214
|
return true;
|
|
214
|
-
} else if (parentPath &&
|
|
215
|
+
} else if (parentPath && itemPath === parentPath) {
|
|
215
216
|
return true;
|
|
216
217
|
}
|
|
217
218
|
}
|
|
@@ -269,8 +270,9 @@ export default {
|
|
|
269
270
|
@keyup.space="groupSelected()"
|
|
270
271
|
>
|
|
271
272
|
<slot name="header">
|
|
273
|
+
<!-- Group overview with link -->
|
|
272
274
|
<router-link
|
|
273
|
-
v-if="hasOverview"
|
|
275
|
+
v-if="hasOverview && hasChildren"
|
|
274
276
|
:to="headerRoute"
|
|
275
277
|
:exact="group.children[0].exact"
|
|
276
278
|
:tabindex="-1"
|
|
@@ -279,15 +281,32 @@ export default {
|
|
|
279
281
|
<span v-clean-html="group.labelDisplay || group.label" />
|
|
280
282
|
</h6>
|
|
281
283
|
</router-link>
|
|
284
|
+
<!-- Non-linked group header -->
|
|
282
285
|
<h6
|
|
283
|
-
v-else
|
|
286
|
+
v-else-if="hasChildren"
|
|
284
287
|
>
|
|
285
288
|
<span v-clean-html="group.labelDisplay || group.label" />
|
|
286
289
|
</h6>
|
|
290
|
+
<!-- Simple child (nav item) -->
|
|
291
|
+
<ul
|
|
292
|
+
v-else
|
|
293
|
+
class="list-unstyled body root-depth"
|
|
294
|
+
v-bind="$attrs"
|
|
295
|
+
>
|
|
296
|
+
<Type
|
|
297
|
+
|
|
298
|
+
:key="id+'_' + group.name + '_type'"
|
|
299
|
+
:is-root="depth == 0 && !showHeader"
|
|
300
|
+
:type="group"
|
|
301
|
+
:depth="depth"
|
|
302
|
+
:highlight-route="highlightRoute"
|
|
303
|
+
@selected="selectType($event)"
|
|
304
|
+
/>
|
|
305
|
+
</ul>
|
|
287
306
|
</slot>
|
|
288
307
|
</div>
|
|
289
308
|
<i
|
|
290
|
-
v-if="!onlyHasOverview && canCollapse"
|
|
309
|
+
v-if="!onlyHasOverview && canCollapse && hasChildren"
|
|
291
310
|
class="icon toggle toggle-accordion"
|
|
292
311
|
:class="{'icon-chevron-right': !isExpanded, 'icon-chevron-down': isExpanded}"
|
|
293
312
|
role="button"
|
|
@@ -377,6 +396,7 @@ export default {
|
|
|
377
396
|
display: block;
|
|
378
397
|
box-sizing:border-box;
|
|
379
398
|
height: 100%;
|
|
399
|
+
|
|
380
400
|
&:hover{
|
|
381
401
|
text-decoration: none;
|
|
382
402
|
}
|
|
@@ -484,6 +504,10 @@ export default {
|
|
|
484
504
|
}
|
|
485
505
|
}
|
|
486
506
|
}
|
|
507
|
+
|
|
508
|
+
.root-depth :deep() > .child.nav-type a {
|
|
509
|
+
padding-left: 14px;
|
|
510
|
+
}
|
|
487
511
|
}
|
|
488
512
|
|
|
489
513
|
&.depth-1 {
|
|
@@ -189,12 +189,30 @@ export default {
|
|
|
189
189
|
(this.currentProduct && this.currentProduct.showWorkspaceSwitcher);
|
|
190
190
|
// Don't show if the header is in 'simple' mode
|
|
191
191
|
const notSimple = !this.simple;
|
|
192
|
-
// One of these must be enabled, otherwise
|
|
193
|
-
const validFilterSettings = this.currentProduct?.showNamespaceFilter || this.
|
|
192
|
+
// One of these must be enabled, otherwise there's no component to show
|
|
193
|
+
const validFilterSettings = this.currentProduct?.showNamespaceFilter || this.showWorkspaceSwitcher;
|
|
194
194
|
|
|
195
195
|
return validClusterOrProduct && notSimple && validFilterSettings;
|
|
196
196
|
},
|
|
197
197
|
|
|
198
|
+
/**
|
|
199
|
+
* The workspace switcher should be disabled on detail, edit and create pages.
|
|
200
|
+
* Only list pages should allow changing the workspace.
|
|
201
|
+
*/
|
|
202
|
+
disableWorkspaceSwitcher() {
|
|
203
|
+
// Disable on detail/edit pages (route has an id param)
|
|
204
|
+
if (this.$route?.params?.id) {
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Disable on create pages (route names end with '-create')
|
|
209
|
+
if (this.$route?.name?.endsWith('-create')) {
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return false;
|
|
214
|
+
},
|
|
215
|
+
|
|
198
216
|
featureRancherDesktop() {
|
|
199
217
|
return this.$config.rancherEnv === 'desktop';
|
|
200
218
|
},
|
|
@@ -203,12 +221,6 @@ export default {
|
|
|
203
221
|
return !!this.currentCluster?.actions?.apply;
|
|
204
222
|
},
|
|
205
223
|
|
|
206
|
-
prod() {
|
|
207
|
-
const name = this.rootProduct.name;
|
|
208
|
-
|
|
209
|
-
return this.$store.getters['i18n/withFallback'](`product."${ name }"`, null, ucFirst(name));
|
|
210
|
-
},
|
|
211
|
-
|
|
212
224
|
showSearch() {
|
|
213
225
|
return this.rootProduct?.inStore === 'cluster';
|
|
214
226
|
},
|
|
@@ -239,6 +251,30 @@ export default {
|
|
|
239
251
|
isHarvester() {
|
|
240
252
|
return this.$store.getters['currentProduct'].inStore === HARVESTER;
|
|
241
253
|
},
|
|
254
|
+
|
|
255
|
+
productLabel() {
|
|
256
|
+
const name = this.rootProduct.name;
|
|
257
|
+
|
|
258
|
+
// single products do their own thing, which is the previous default behavior as per next line
|
|
259
|
+
if (this.isSingleProduct) {
|
|
260
|
+
return this.$store.getters['i18n/withFallback'](`product."${ name }"`, null, ucFirst(name));
|
|
261
|
+
} else {
|
|
262
|
+
if (this.rootProduct?.label) {
|
|
263
|
+
return this.rootProduct.label;
|
|
264
|
+
}
|
|
265
|
+
if (this.rootProduct?.labelKey) {
|
|
266
|
+
return this.$store.getters['i18n/t'](this.rootProduct.labelKey);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return this.$store.getters['i18n/withFallback'](`product."${ name }"`, null, ucFirst(name));
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
// Determine if we are on a route that shows the logo instead of the product label
|
|
274
|
+
// This is to enforce the logo display on certain routes like home, about, prefs, account, etc
|
|
275
|
+
isLogoRoute() {
|
|
276
|
+
return !this.$route.name.includes('c-cluster');
|
|
277
|
+
}
|
|
242
278
|
},
|
|
243
279
|
|
|
244
280
|
watch: {
|
|
@@ -518,7 +554,7 @@ export default {
|
|
|
518
554
|
:alt="t('branding.logos.label')"
|
|
519
555
|
>
|
|
520
556
|
<div class="product-name">
|
|
521
|
-
{{
|
|
557
|
+
{{ productLabel }}
|
|
522
558
|
</div>
|
|
523
559
|
</div>
|
|
524
560
|
</div>
|
|
@@ -534,6 +570,13 @@ export default {
|
|
|
534
570
|
{{ t(isSingleProduct.productNameKey) }}
|
|
535
571
|
</div>
|
|
536
572
|
|
|
573
|
+
<div
|
|
574
|
+
v-else-if="productLabel && !isLogoRoute"
|
|
575
|
+
class="product-name"
|
|
576
|
+
>
|
|
577
|
+
{{ productLabel }}
|
|
578
|
+
</div>
|
|
579
|
+
|
|
537
580
|
<div
|
|
538
581
|
v-else
|
|
539
582
|
class="side-menu-logo"
|
|
@@ -556,7 +599,10 @@ export default {
|
|
|
556
599
|
class="top"
|
|
557
600
|
>
|
|
558
601
|
<NamespaceFilter v-if="clusterReady && currentProduct && (currentProduct.showNamespaceFilter || isExplorer)" />
|
|
559
|
-
<WorkspaceSwitcher
|
|
602
|
+
<WorkspaceSwitcher
|
|
603
|
+
v-else-if="clusterReady && showWorkspaceSwitcher"
|
|
604
|
+
:disabled="disableWorkspaceSwitcher"
|
|
605
|
+
/>
|
|
560
606
|
</div>
|
|
561
607
|
<div
|
|
562
608
|
v-if="currentCluster && !simple"
|
|
@@ -246,7 +246,10 @@ const findNewIndex = (shouldAdvance: boolean, activeIndex: number, itemsArr: Ele
|
|
|
246
246
|
:class="clz"
|
|
247
247
|
/>
|
|
248
248
|
</div>
|
|
249
|
-
<div
|
|
249
|
+
<div
|
|
250
|
+
v-clean-tooltip="item.title"
|
|
251
|
+
class="item-title"
|
|
252
|
+
>
|
|
250
253
|
{{ item.title }}
|
|
251
254
|
</div>
|
|
252
255
|
<button
|
|
@@ -2,17 +2,19 @@
|
|
|
2
2
|
import { useStore } from 'vuex';
|
|
3
3
|
import { computed, inject, ref } from 'vue';
|
|
4
4
|
import { DropdownContext, defaultContext } from '@components/RcDropdown/types';
|
|
5
|
+
import RcButton from '@components/RcButton/RcButton.vue';
|
|
6
|
+
import { RcButtonType } from '@components/RcButton/types';
|
|
5
7
|
|
|
6
8
|
const { dropdownItems } = inject<DropdownContext>('dropdownContext') || defaultContext;
|
|
7
9
|
const store = useStore();
|
|
8
10
|
const unreadCount = computed<number>(() => store.getters['notifications/unreadCount']);
|
|
9
|
-
const markAllReadButton = ref<
|
|
11
|
+
const markAllReadButton = ref<RcButtonType | null>(null);
|
|
10
12
|
|
|
11
13
|
const markAllRead = (keyboard: boolean) => {
|
|
12
14
|
store.dispatch('notifications/markAllRead');
|
|
13
15
|
|
|
14
|
-
// If
|
|
15
|
-
if (keyboard
|
|
16
|
+
// If activated via keyboard, move focus to the next dropdown item
|
|
17
|
+
if (keyboard) {
|
|
16
18
|
moveFocus(true);
|
|
17
19
|
}
|
|
18
20
|
};
|
|
@@ -65,18 +67,19 @@ const gotFocus = (e: Event) => {
|
|
|
65
67
|
{{ t('notificationCenter.title') }}
|
|
66
68
|
</div>
|
|
67
69
|
<div v-if="unreadCount !== 0">
|
|
68
|
-
<
|
|
70
|
+
<RcButton
|
|
69
71
|
ref="markAllReadButton"
|
|
70
|
-
|
|
72
|
+
variant="ghost"
|
|
73
|
+
size="small"
|
|
71
74
|
tabindex="-1"
|
|
72
|
-
|
|
75
|
+
class="mark-all-read"
|
|
73
76
|
data-testid="notifications-center-markall-read"
|
|
74
77
|
@keydown.up.down.stop.prevent="handleKeydown"
|
|
75
78
|
@keydown.enter.space.stop="markAllRead(true)"
|
|
76
79
|
@click="markAllRead(false)"
|
|
77
80
|
>
|
|
78
81
|
{{ t('notificationCenter.markAllRead') }}
|
|
79
|
-
</
|
|
82
|
+
</RcButton>
|
|
80
83
|
</div>
|
|
81
84
|
</div>
|
|
82
85
|
<div class="notification-border" />
|
|
@@ -104,8 +107,17 @@ const gotFocus = (e: Event) => {
|
|
|
104
107
|
flex: 1;
|
|
105
108
|
}
|
|
106
109
|
|
|
107
|
-
|
|
110
|
+
.mark-all-read {
|
|
111
|
+
padding: 0;
|
|
112
|
+
min-height: auto;
|
|
113
|
+
font-size: inherit;
|
|
114
|
+
line-height: inherit;
|
|
108
115
|
color: var(--link);
|
|
116
|
+
|
|
117
|
+
&:hover {
|
|
118
|
+
color: var(--body-text);
|
|
119
|
+
text-decoration: underline;
|
|
120
|
+
}
|
|
109
121
|
}
|
|
110
122
|
}
|
|
111
123
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { ref } from 'vue';
|
|
2
|
+
import { mount, shallowMount } from '@vue/test-utils';
|
|
3
|
+
import NotificationHeader from '@shell/components/nav/NotificationCenter/NotificationHeader.vue';
|
|
4
|
+
import { defaultContext } from '@components/RcDropdown/types';
|
|
5
|
+
|
|
6
|
+
const buildStore = (unreadCount = 1) => {
|
|
7
|
+
const dispatch = jest.fn();
|
|
8
|
+
const store = {
|
|
9
|
+
dispatch,
|
|
10
|
+
getters: { 'notifications/unreadCount': unreadCount },
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
return { store, dispatch };
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const buildGlobal = (store: any) => ({
|
|
17
|
+
provide: {
|
|
18
|
+
store,
|
|
19
|
+
dropdownContext: { ...defaultContext, dropdownItems: ref<HTMLElement[]>([]) },
|
|
20
|
+
},
|
|
21
|
+
mocks: { $store: store },
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
jest.mock('vuex', () => ({ useStore: () => (globalThis as any).__testStore }));
|
|
25
|
+
|
|
26
|
+
describe('component: NotificationHeader', () => {
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
(globalThis as any).__testStore = undefined;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('renders the mark all read action when there are unread notifications', () => {
|
|
32
|
+
const { store } = buildStore(3);
|
|
33
|
+
|
|
34
|
+
(globalThis as any).__testStore = store;
|
|
35
|
+
|
|
36
|
+
const wrapper = shallowMount(NotificationHeader, { global: buildGlobal(store) });
|
|
37
|
+
|
|
38
|
+
expect(wrapper.find('[data-testid="notifications-center-markall-read"]').exists()).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('hides the mark all read action when there are no unread notifications', () => {
|
|
42
|
+
const { store } = buildStore(0);
|
|
43
|
+
|
|
44
|
+
(globalThis as any).__testStore = store;
|
|
45
|
+
|
|
46
|
+
const wrapper = shallowMount(NotificationHeader, { global: buildGlobal(store) });
|
|
47
|
+
|
|
48
|
+
expect(wrapper.find('[data-testid="notifications-center-markall-read"]').exists()).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('dispatches notifications/markAllRead when clicked', async() => {
|
|
52
|
+
const { store, dispatch } = buildStore(2);
|
|
53
|
+
|
|
54
|
+
(globalThis as any).__testStore = store;
|
|
55
|
+
|
|
56
|
+
const wrapper = mount(NotificationHeader, { global: buildGlobal(store) });
|
|
57
|
+
|
|
58
|
+
await wrapper.find('[data-testid="notifications-center-markall-read"]').trigger('click');
|
|
59
|
+
|
|
60
|
+
expect(dispatch).toHaveBeenCalledWith('notifications/markAllRead');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Regression test for https://github.com/rancher/dashboard/issues/16923
|
|
64
|
+
// "Mark all as read" was originally an <a href="#"> which, on click, navigated
|
|
65
|
+
// to "#" and stripped any existing URL hash fragment (e.g. #pod). Rendering it
|
|
66
|
+
// as a <button> (via RcButton) removes the default navigation behavior entirely,
|
|
67
|
+
// so the URL hash is preserved and extensions scoped via LocationConfig.hash
|
|
68
|
+
// continue to match after activation.
|
|
69
|
+
it('renders mark all read as a <button> so activating it cannot strip the URL hash', () => {
|
|
70
|
+
const { store } = buildStore(2);
|
|
71
|
+
|
|
72
|
+
(globalThis as any).__testStore = store;
|
|
73
|
+
|
|
74
|
+
const wrapper = mount(NotificationHeader, { global: buildGlobal(store) });
|
|
75
|
+
const markAll = wrapper.find('[data-testid="notifications-center-markall-read"]');
|
|
76
|
+
|
|
77
|
+
expect(markAll.element.tagName).toBe('BUTTON');
|
|
78
|
+
expect(markAll.attributes('href')).toBeUndefined();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -186,8 +186,22 @@ export default {
|
|
|
186
186
|
to.params.product = p.name;
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
let label;
|
|
190
|
+
|
|
191
|
+
// Allow product to specify its label (old DSL product() did not have "label" or "labelKey")
|
|
192
|
+
// new extensions product registration supports both "label" and "labelKey" (with "labelKey" taking precedence if both are provided)
|
|
193
|
+
if (p.labelKey) {
|
|
194
|
+
label = this.$store.getters['i18n/t'](p.labelKey);
|
|
195
|
+
} else if (p.label) {
|
|
196
|
+
label = p.label;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (!label) {
|
|
200
|
+
label = this.$store.getters['i18n/withFallback'](`product.${ p.name }`, null, ucFirst(p.name));
|
|
201
|
+
}
|
|
202
|
+
|
|
189
203
|
return {
|
|
190
|
-
label
|
|
204
|
+
label,
|
|
191
205
|
icon: `icon-${ p.icon || 'copy' }`,
|
|
192
206
|
svg: p.svg,
|
|
193
207
|
value: p.name,
|