@rancher/shell 3.0.12-rc.1 → 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/translations/en-us.yaml +19 -17
- package/assets/translations/zh-hans.yaml +4 -8
- 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/SelectIconGrid.vue +5 -0
- package/components/__tests__/CountBox.test.ts +72 -0
- package/components/__tests__/DetailText.test.ts +113 -0
- package/components/fleet/FleetClusterTargets/index.vue +18 -1
- package/components/form/InputWithSelect.vue +18 -10
- package/components/form/KeyValue.vue +17 -1
- package/components/form/LabeledSelect.vue +82 -24
- 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/labeled-select-utils/useLabeledSelectPagination.ts +138 -0
- package/components/nav/Group.vue +7 -6
- package/components/nav/Header.vue +24 -3
- 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/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/query-params.js +1 -0
- package/config/router/routes.js +0 -8
- package/core/__tests__/plugin-products.test.ts +616 -25
- package/core/plugin-products-base.ts +31 -14
- package/core/plugin-products-helpers.ts +5 -4
- package/core/plugin-types.ts +18 -3
- package/core/types.ts +3 -1
- package/detail/__tests__/management.cattle.io.fleetworkspace.test.ts +128 -0
- package/detail/management.cattle.io.fleetworkspace.vue +49 -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/index.vue +70 -99
- 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/machine-config/amazonec2.vue +1 -0
- package/mixins/chart.js +40 -9
- 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/package.json +13 -12
- 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/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 +15 -10
- package/pages/c/_cluster/uiplugins/index.vue +23 -25
- 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/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/auth.js +0 -3
- package/store/catalog.js +60 -8
- package/types/shell/index.d.ts +26 -22
- 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/object.js +33 -2
- package/utils/time.ts +5 -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,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
|
}
|
|
@@ -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
|
},
|
|
@@ -581,7 +599,10 @@ export default {
|
|
|
581
599
|
class="top"
|
|
582
600
|
>
|
|
583
601
|
<NamespaceFilter v-if="clusterReady && currentProduct && (currentProduct.showNamespaceFilter || isExplorer)" />
|
|
584
|
-
<WorkspaceSwitcher
|
|
602
|
+
<WorkspaceSwitcher
|
|
603
|
+
v-else-if="clusterReady && showWorkspaceSwitcher"
|
|
604
|
+
:disabled="disableWorkspaceSwitcher"
|
|
605
|
+
/>
|
|
585
606
|
</div>
|
|
586
607
|
<div
|
|
587
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
|
+
});
|
package/components/nav/Type.vue
CHANGED
|
@@ -68,8 +68,9 @@ export default {
|
|
|
68
68
|
},
|
|
69
69
|
|
|
70
70
|
isActive() {
|
|
71
|
-
|
|
72
|
-
const
|
|
71
|
+
// Use .path instead of .fullPath to ignore query parameters and hashes when comparing routes
|
|
72
|
+
const typePath = this.$router.resolve(this.typeRoute)?.path.toLowerCase();
|
|
73
|
+
const pagePath = this.$route.path?.toLowerCase();
|
|
73
74
|
const routeMetaNav = this.$route.meta?.nav;
|
|
74
75
|
|
|
75
76
|
// If the route explicitly declares the nav path that should be highlighted, then use that
|
|
@@ -80,14 +81,14 @@ export default {
|
|
|
80
81
|
.replace(':cluster', cluster)
|
|
81
82
|
.replace(':product', product);
|
|
82
83
|
|
|
83
|
-
if (navPath ===
|
|
84
|
+
if (navPath === typePath) {
|
|
84
85
|
return true;
|
|
85
86
|
}
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
if ( !this.type.exact) {
|
|
89
|
-
const typeSplit =
|
|
90
|
-
const pageSplit =
|
|
90
|
+
const typeSplit = typePath.split('/');
|
|
91
|
+
const pageSplit = pagePath.split('/');
|
|
91
92
|
|
|
92
93
|
for (let index = 0; index < typeSplit.length; ++index) {
|
|
93
94
|
if ( index >= pageSplit.length || typeSplit[index] !== pageSplit[index] ) {
|
|
@@ -98,7 +99,7 @@ export default {
|
|
|
98
99
|
return true;
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
return
|
|
102
|
+
return typePath === pagePath;
|
|
102
103
|
},
|
|
103
104
|
|
|
104
105
|
typeRoute() {
|
|
@@ -131,7 +132,7 @@ export default {
|
|
|
131
132
|
<router-link
|
|
132
133
|
v-if="type.route"
|
|
133
134
|
:key="type.name"
|
|
134
|
-
v-slot="{ href, navigate,isExactActive }"
|
|
135
|
+
v-slot="{ href, navigate, isExactActive }"
|
|
135
136
|
custom
|
|
136
137
|
:to="typeRoute"
|
|
137
138
|
>
|
|
@@ -36,7 +36,7 @@ const props = defineProps({
|
|
|
36
36
|
|
|
37
37
|
const { loadComponent } = useComponentsMount();
|
|
38
38
|
|
|
39
|
-
const { isPanelEnabled } = usePanelsHandler(
|
|
39
|
+
const { isPanelEnabled } = usePanelsHandler(props);
|
|
40
40
|
const { tabs } = useTabsHandler();
|
|
41
41
|
</script>
|
|
42
42
|
|
|
@@ -66,6 +66,7 @@ const { tabs } = useTabsHandler();
|
|
|
66
66
|
:active="true"
|
|
67
67
|
:height="tab.containerHeight"
|
|
68
68
|
:width="tab.containerWidth"
|
|
69
|
+
:layout="layout"
|
|
69
70
|
v-bind="tab.attrs"
|
|
70
71
|
/>
|
|
71
72
|
</keep-alive>
|
|
@@ -8,6 +8,13 @@ export default {
|
|
|
8
8
|
name: 'WorkspaceSwitcher',
|
|
9
9
|
components: { Select },
|
|
10
10
|
|
|
11
|
+
props: {
|
|
12
|
+
disabled: {
|
|
13
|
+
type: Boolean,
|
|
14
|
+
default: false,
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
|
|
11
18
|
computed: {
|
|
12
19
|
...mapState(['allWorkspaces', 'workspace', 'allNamespaces', 'defaultNamespace', 'getActiveNamespaces']),
|
|
13
20
|
|
|
@@ -94,6 +101,7 @@ export default {
|
|
|
94
101
|
label="label"
|
|
95
102
|
:options="options"
|
|
96
103
|
:clearable="false"
|
|
104
|
+
:disabled="disabled"
|
|
97
105
|
:reduce="(opt) => opt.value"
|
|
98
106
|
/>
|
|
99
107
|
<!--button v-shortkey.once="['w']" class="hide" @shortkey="focus()" /-->
|
|
@@ -187,4 +195,9 @@ export default {
|
|
|
187
195
|
.filter :deep() .unlabeled-select INPUT[type='search'] {
|
|
188
196
|
padding: 7px;
|
|
189
197
|
}
|
|
198
|
+
|
|
199
|
+
.filter :deep() .unlabeled-select.disabled {
|
|
200
|
+
opacity: 0.5;
|
|
201
|
+
cursor: not-allowed;
|
|
202
|
+
}
|
|
190
203
|
</style>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { shallowMount } from '@vue/test-utils';
|
|
2
|
+
import Group from '@shell/components/nav/Group.vue';
|
|
3
|
+
|
|
4
|
+
describe('component: Group', () => {
|
|
5
|
+
it('isOverview ignores query parameters and hash strings when checking active state', () => {
|
|
6
|
+
const group = {
|
|
7
|
+
name: 'test',
|
|
8
|
+
children: [
|
|
9
|
+
{
|
|
10
|
+
route: { name: 'overview-route' },
|
|
11
|
+
overview: true
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const wrapper = shallowMount(Group as any, {
|
|
17
|
+
props: {
|
|
18
|
+
group, canCollapse: true, idPrefix: ''
|
|
19
|
+
},
|
|
20
|
+
global: {
|
|
21
|
+
mocks: {
|
|
22
|
+
$route: { path: '/test/route', fullPath: '/test/route?query=val#hash' },
|
|
23
|
+
$router: {
|
|
24
|
+
resolve: jest.fn().mockReturnValue({ path: '/test/route', fullPath: '/test/route' }),
|
|
25
|
+
getRoutes: jest.fn().mockReturnValue([])
|
|
26
|
+
},
|
|
27
|
+
t: (key: string) => key
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
expect((wrapper.vm as any).isOverview).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('hasActiveRoute ignores query parameters when checking item paths', () => {
|
|
36
|
+
const group = {
|
|
37
|
+
name: 'test',
|
|
38
|
+
children: [
|
|
39
|
+
{ route: { name: 'child-route', params: {} } }
|
|
40
|
+
]
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const wrapper = shallowMount(Group as any, {
|
|
44
|
+
props: {
|
|
45
|
+
group, canCollapse: true, idPrefix: ''
|
|
46
|
+
},
|
|
47
|
+
global: {
|
|
48
|
+
mocks: {
|
|
49
|
+
$route: {
|
|
50
|
+
params: {},
|
|
51
|
+
hash: '#hash',
|
|
52
|
+
path: '/child/route',
|
|
53
|
+
fullPath: '/child/route?query=val#hash',
|
|
54
|
+
matched: []
|
|
55
|
+
},
|
|
56
|
+
$router: {
|
|
57
|
+
resolve: jest.fn().mockReturnValue({ path: '/child/route', fullPath: '/child/route' }),
|
|
58
|
+
getRoutes: jest.fn().mockReturnValue([])
|
|
59
|
+
},
|
|
60
|
+
t: (key: string) => key
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect((wrapper.vm as any).hasActiveRoute()).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
});
|