@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { mount, RouterLinkStub } from '@vue/test-utils';
|
|
2
|
-
import TitleBar from '@shell/components/Resource/Detail/TitleBar/index.vue';
|
|
2
|
+
import TitleBar, { AdditionalActionButton } from '@shell/components/Resource/Detail/TitleBar/index.vue';
|
|
3
3
|
import ActionMenu from '@shell/components/ActionMenuShell.vue';
|
|
4
4
|
import { createStore } from 'vuex';
|
|
5
5
|
import { defineComponent, h } from 'vue';
|
|
@@ -240,5 +240,49 @@ describe('component: TitleBar/index', () => {
|
|
|
240
240
|
expect(wrapper.find('.slot-button').exists()).toBeTruthy();
|
|
241
241
|
expect(wrapper.find('.slot-button').text()).toBe('Slot Button');
|
|
242
242
|
});
|
|
243
|
+
|
|
244
|
+
it('should render the actions container correctly when additional-actions slot contains nested buttons', async() => {
|
|
245
|
+
const wrapper = mount(TitleBar, {
|
|
246
|
+
props: { resourceTypeLabel, resourceName },
|
|
247
|
+
slots: { 'additional-actions': '<div class="btn-group"><button class="nested-btn">A</button><button class="nested-btn">B</button></div>' },
|
|
248
|
+
global: { stubs: { 'router-link': RouterLinkStub }, provide: { store } }
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const actions = wrapper.find('.actions');
|
|
252
|
+
|
|
253
|
+
expect(actions.find('.btn-group').exists()).toBeTruthy();
|
|
254
|
+
expect(actions.findAll('.btn-group .nested-btn')).toHaveLength(2);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should match the full component snapshot', () => {
|
|
259
|
+
const additionalActions: AdditionalActionButton[] = [
|
|
260
|
+
{
|
|
261
|
+
label: 'Deploy', variant: 'primary', onClick: jest.fn()
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
label: 'Rollback', variant: 'secondary', size: 'large', onClick: jest.fn()
|
|
265
|
+
},
|
|
266
|
+
];
|
|
267
|
+
|
|
268
|
+
const wrapper = mount(TitleBar, {
|
|
269
|
+
props: {
|
|
270
|
+
resource: {},
|
|
271
|
+
resourceTypeLabel,
|
|
272
|
+
resourceName,
|
|
273
|
+
resourceTo,
|
|
274
|
+
description: 'A test description',
|
|
275
|
+
badge: { color: 'bg-success', label: 'Active' },
|
|
276
|
+
additionalActions,
|
|
277
|
+
actionMenuResource: { resource: 'test-menu' },
|
|
278
|
+
onShowConfiguration() {},
|
|
279
|
+
},
|
|
280
|
+
global: {
|
|
281
|
+
stubs: { 'router-link': RouterLinkStub },
|
|
282
|
+
provide: { store }
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
243
287
|
});
|
|
244
288
|
});
|
|
@@ -178,7 +178,7 @@ const showAdditionalActionButtons = computed(() => isArray(additionalActions));
|
|
|
178
178
|
align-items: center;
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
.show-configuration, &:deep() .actions button {
|
|
181
|
+
.show-configuration, &:deep() .actions > button {
|
|
182
182
|
margin-left: 16px;
|
|
183
183
|
}
|
|
184
184
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`component: ViewOptions/index should match the snapshot 1`] = `
|
|
4
|
+
<div class="btn-group"><button data-testid="button-group-child-0" type="button" class="btn bg-primary" role="button" aria-label="%resourceDetail.masthead.detail%" aria-pressed="true">
|
|
5
|
+
<!--v-if--><span k="resourceDetail.masthead.detail"></span>
|
|
6
|
+
</button><button data-testid="button-group-child-1" type="button" class="btn bg-disabled" role="button" aria-label="%resourceDetail.masthead.graph%" aria-pressed="false">
|
|
7
|
+
<!--v-if--><span k="resourceDetail.masthead.graph"></span>
|
|
8
|
+
</button></div>
|
|
9
|
+
`;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils';
|
|
2
|
+
import ViewOptions from '@shell/components/Resource/Detail/ViewOptions/index.vue';
|
|
3
|
+
import ButtonGroup from '@shell/components/ButtonGroup.vue';
|
|
4
|
+
import { _CONFIG, _GRAPH } from '@shell/config/query-params';
|
|
5
|
+
|
|
6
|
+
const mockPush = jest.fn();
|
|
7
|
+
const mockQuery = { view: _CONFIG };
|
|
8
|
+
|
|
9
|
+
jest.mock('vue-router', () => ({
|
|
10
|
+
useRouter: () => ({ push: mockPush }),
|
|
11
|
+
useRoute: () => ({ query: mockQuery }),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
describe('component: ViewOptions/index', () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
jest.clearAllMocks();
|
|
17
|
+
mockQuery.view = _CONFIG;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const createWrapper = () => {
|
|
21
|
+
return mount(ViewOptions);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
it('should render a ButtonGroup component', () => {
|
|
25
|
+
const wrapper = createWrapper();
|
|
26
|
+
|
|
27
|
+
expect(wrapper.findComponent(ButtonGroup).exists()).toBeTruthy();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should provide two view options: detail and graph', () => {
|
|
31
|
+
const wrapper = createWrapper();
|
|
32
|
+
const buttonGroup = wrapper.findComponent(ButtonGroup);
|
|
33
|
+
const options = buttonGroup.props('options');
|
|
34
|
+
|
|
35
|
+
expect(options).toHaveLength(2);
|
|
36
|
+
expect(options[0].value).toStrictEqual(_CONFIG);
|
|
37
|
+
expect(options[1].value).toStrictEqual(_GRAPH);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should set the initial view from the route query', () => {
|
|
41
|
+
const wrapper = createWrapper();
|
|
42
|
+
const buttonGroup = wrapper.findComponent(ButtonGroup);
|
|
43
|
+
|
|
44
|
+
expect(buttonGroup.props('value')).toStrictEqual(_CONFIG);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should push to router when view changes', async() => {
|
|
48
|
+
const wrapper = createWrapper();
|
|
49
|
+
const buttons = wrapper.findAll('.btn-group button');
|
|
50
|
+
const graphButton = buttons[1];
|
|
51
|
+
|
|
52
|
+
await graphButton.trigger('click');
|
|
53
|
+
|
|
54
|
+
expect(mockPush).toHaveBeenCalledWith({ query: { view: _GRAPH } });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should match the snapshot', () => {
|
|
58
|
+
const wrapper = createWrapper();
|
|
59
|
+
|
|
60
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -14,7 +14,8 @@ const view = ref(currentView.value);
|
|
|
14
14
|
const viewOptions = computed(() => {
|
|
15
15
|
return [
|
|
16
16
|
{
|
|
17
|
-
labelKey: 'resourceDetail.masthead.
|
|
17
|
+
labelKey: 'resourceDetail.masthead.detail',
|
|
18
|
+
// _CONFIG is the default when there is no query on the router
|
|
18
19
|
value: _CONFIG,
|
|
19
20
|
},
|
|
20
21
|
{
|
|
@@ -85,7 +85,29 @@ export default {
|
|
|
85
85
|
data() {
|
|
86
86
|
const params = { ...this.$route.params };
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
// Determine if the current product has a topLevelProduct defined, and if so,
|
|
89
|
+
// use that for the formRoute instead of the current route's product.
|
|
90
|
+
// This allows resources from extensions (new product registration) to use the correct route for creation,
|
|
91
|
+
// which may be different from the route of the resource list.
|
|
92
|
+
let currPluginName = '';
|
|
93
|
+
let formRoute;
|
|
94
|
+
let overrideCreateLocationByExtension = false;
|
|
95
|
+
const plugins = this.$extension.getPlugins();
|
|
96
|
+
|
|
97
|
+
Object.keys(plugins).forEach((key) => {
|
|
98
|
+
if (plugins[key].productNames.includes(this.$store.getters['productId'])) {
|
|
99
|
+
currPluginName = key;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (currPluginName && plugins[currPluginName]?.topLevelProduct) {
|
|
104
|
+
// override create route for extension resource lists
|
|
105
|
+
formRoute = { name: `${ this.$route.name }-create`, params: { ...params, product: this.$store.getters['productId'] } };
|
|
106
|
+
overrideCreateLocationByExtension = true;
|
|
107
|
+
} else {
|
|
108
|
+
// this was the original logic before the topLevelProduct override was added
|
|
109
|
+
formRoute = { name: `${ this.$route.name }-create`, params };
|
|
110
|
+
}
|
|
89
111
|
|
|
90
112
|
const hasEditComponent = this.$store.getters['type-map/hasCustomEdit'](this.resource);
|
|
91
113
|
|
|
@@ -96,6 +118,7 @@ export default {
|
|
|
96
118
|
};
|
|
97
119
|
|
|
98
120
|
return {
|
|
121
|
+
overrideCreateLocationByExtension,
|
|
99
122
|
formRoute,
|
|
100
123
|
yamlRoute,
|
|
101
124
|
hasEditComponent,
|
|
@@ -149,7 +172,7 @@ export default {
|
|
|
149
172
|
},
|
|
150
173
|
|
|
151
174
|
_createLocation() {
|
|
152
|
-
return this.createLocation || this.formRoute;
|
|
175
|
+
return this.overrideCreateLocationByExtension ? this.formRoute : this.createLocation || this.formRoute;
|
|
153
176
|
},
|
|
154
177
|
|
|
155
178
|
_yamlCreateLocation() {
|
|
@@ -38,6 +38,10 @@ export default {
|
|
|
38
38
|
type: String,
|
|
39
39
|
default: 'disabled',
|
|
40
40
|
},
|
|
41
|
+
tooltipField: {
|
|
42
|
+
type: String,
|
|
43
|
+
default: 'tooltip',
|
|
44
|
+
},
|
|
41
45
|
|
|
42
46
|
asLink: {
|
|
43
47
|
type: Boolean,
|
|
@@ -104,6 +108,7 @@ export default {
|
|
|
104
108
|
:is="asLink ? 'a' : 'div'"
|
|
105
109
|
v-for="(r, idx) in rows"
|
|
106
110
|
:key="get(r, keyField)"
|
|
111
|
+
v-clean-tooltip="get(r, tooltipField) || null"
|
|
107
112
|
:role="asLink ? 'link' : null"
|
|
108
113
|
:aria-disabled="asLink && get(r, disabledField) === true ? true : null"
|
|
109
114
|
:aria-label="get(r, nameField)"
|
package/components/SideNav.vue
CHANGED
|
@@ -229,6 +229,19 @@ export default {
|
|
|
229
229
|
|
|
230
230
|
this.getExplorerGroups(out);
|
|
231
231
|
|
|
232
|
+
// If there's a root group, pull its children up to the top level
|
|
233
|
+
// so that we can order them alongside group items in the nav
|
|
234
|
+
const rootGroupIndex = out.findIndex((g) => g.name.toLowerCase() === 'root');
|
|
235
|
+
const rootGroup = out[rootGroupIndex];
|
|
236
|
+
|
|
237
|
+
if (rootGroup && rootGroup.children?.length) {
|
|
238
|
+
out.splice(rootGroupIndex, 1);
|
|
239
|
+
|
|
240
|
+
rootGroup.children.forEach((child) => {
|
|
241
|
+
addObject(out, { ...child, children: [] });
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
232
245
|
replaceWith(this.groups, ...sortBy(out, ['weight:desc', 'label']));
|
|
233
246
|
|
|
234
247
|
this.gettingGroups = false;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { shallowMount } from '@vue/test-utils';
|
|
2
|
+
import CountBox from '@shell/components/CountBox.vue';
|
|
3
|
+
|
|
4
|
+
describe('component: CountBox', () => {
|
|
5
|
+
const defaultProps = {
|
|
6
|
+
name: 'Test',
|
|
7
|
+
count: 5,
|
|
8
|
+
primaryColorVar: '--sizzle-1',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
describe('when clickable is false', () => {
|
|
12
|
+
it('should render as a div', () => {
|
|
13
|
+
const wrapper = shallowMount(CountBox, { props: defaultProps });
|
|
14
|
+
|
|
15
|
+
expect(wrapper.element.tagName).toBe('DIV');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should not have the clickable class', () => {
|
|
19
|
+
const wrapper = shallowMount(CountBox, { props: defaultProps });
|
|
20
|
+
|
|
21
|
+
expect(wrapper.classes()).not.toContain('clickable');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should not emit click event when clicked', async() => {
|
|
25
|
+
const wrapper = shallowMount(CountBox, { props: defaultProps });
|
|
26
|
+
|
|
27
|
+
await wrapper.trigger('click');
|
|
28
|
+
|
|
29
|
+
expect(wrapper.emitted('click')).toBeUndefined();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('when clickable is true', () => {
|
|
34
|
+
it('should have the clickable class', () => {
|
|
35
|
+
const wrapper = shallowMount(CountBox, {
|
|
36
|
+
props: {
|
|
37
|
+
...defaultProps,
|
|
38
|
+
clickable: true,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(wrapper.classes()).toContain('clickable');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should emit click event when clicked', async() => {
|
|
46
|
+
const wrapper = shallowMount(CountBox, {
|
|
47
|
+
props: {
|
|
48
|
+
...defaultProps,
|
|
49
|
+
clickable: true,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
await wrapper.trigger('click');
|
|
54
|
+
|
|
55
|
+
expect(wrapper.emitted('click')).toHaveLength(1);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('display', () => {
|
|
60
|
+
it('should display the count', () => {
|
|
61
|
+
const wrapper = shallowMount(CountBox, { props: defaultProps });
|
|
62
|
+
|
|
63
|
+
expect(wrapper.find('h1').text()).toBe('5');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should display the name', () => {
|
|
67
|
+
const wrapper = shallowMount(CountBox, { props: defaultProps });
|
|
68
|
+
|
|
69
|
+
expect(wrapper.find('label').text()).toBe('Test');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils';
|
|
2
|
+
|
|
3
|
+
import DetailText from '@shell/components/DetailText.vue';
|
|
4
|
+
|
|
5
|
+
jest.mock('@shell/utils/clipboard', () => ({ copyTextToClipboard: jest.fn() }));
|
|
6
|
+
|
|
7
|
+
describe('component: DetailText', () => {
|
|
8
|
+
const defaultMocks = {
|
|
9
|
+
$store: {
|
|
10
|
+
getters: {
|
|
11
|
+
'i18n/t': jest.fn((key: string) => `%${ key }%`),
|
|
12
|
+
'prefs/get': jest.fn(() => true),
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
describe('concealment', () => {
|
|
18
|
+
it('should not render the actual secret value in the content area when concealed', () => {
|
|
19
|
+
const secretValue = 'super-secret-password-xyz';
|
|
20
|
+
const wrapper = mount(DetailText, {
|
|
21
|
+
props: {
|
|
22
|
+
value: secretValue,
|
|
23
|
+
conceal: true,
|
|
24
|
+
label: 'Password',
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
global: {
|
|
28
|
+
mocks: defaultMocks,
|
|
29
|
+
directives: {
|
|
30
|
+
'clean-html': () => {},
|
|
31
|
+
'clean-tooltip': () => {},
|
|
32
|
+
t: () => {},
|
|
33
|
+
},
|
|
34
|
+
stubs: {
|
|
35
|
+
CopyToClipboard: true,
|
|
36
|
+
CodeMirror: true,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const concealedSpan = wrapper.find('[data-testid="detail-top_html"]');
|
|
42
|
+
|
|
43
|
+
expect(concealedSpan.exists()).toBe(true);
|
|
44
|
+
expect(concealedSpan.classes()).toContain('conceal');
|
|
45
|
+
expect(concealedSpan.text()).not.toContain(secretValue);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should render the actual value when not concealed', () => {
|
|
49
|
+
const visibleValue = 'visible-value-123';
|
|
50
|
+
const wrapper = mount(DetailText, {
|
|
51
|
+
props: {
|
|
52
|
+
value: visibleValue,
|
|
53
|
+
conceal: false,
|
|
54
|
+
label: 'Data',
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
global: {
|
|
58
|
+
mocks: defaultMocks,
|
|
59
|
+
directives: {
|
|
60
|
+
'clean-html': (el: HTMLElement, binding: { value: string }) => {
|
|
61
|
+
el.innerHTML = binding.value;
|
|
62
|
+
},
|
|
63
|
+
'clean-tooltip': () => {},
|
|
64
|
+
t: () => {},
|
|
65
|
+
},
|
|
66
|
+
stubs: {
|
|
67
|
+
CopyToClipboard: true,
|
|
68
|
+
CodeMirror: true,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const contentSpan = wrapper.find('[data-testid="detail-top_html"]');
|
|
74
|
+
|
|
75
|
+
expect(contentSpan.exists()).toBe(true);
|
|
76
|
+
expect(contentSpan.classes()).not.toContain('conceal');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should not render JSON secret values in CodeMirror when concealed', () => {
|
|
80
|
+
const jsonSecret = '{"api_key": "secret-key-123"}';
|
|
81
|
+
const wrapper = mount(DetailText, {
|
|
82
|
+
props: {
|
|
83
|
+
value: jsonSecret,
|
|
84
|
+
conceal: true,
|
|
85
|
+
label: 'Config',
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
global: {
|
|
89
|
+
mocks: defaultMocks,
|
|
90
|
+
directives: {
|
|
91
|
+
'clean-html': () => {},
|
|
92
|
+
'clean-tooltip': () => {},
|
|
93
|
+
t: () => {},
|
|
94
|
+
},
|
|
95
|
+
stubs: {
|
|
96
|
+
CopyToClipboard: true,
|
|
97
|
+
CodeMirror: true,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const codeMirror = wrapper.findComponent({ name: 'CodeMirror' });
|
|
103
|
+
|
|
104
|
+
expect(codeMirror.exists()).toBe(false);
|
|
105
|
+
|
|
106
|
+
const concealedSpan = wrapper.find('[data-testid="detail-top_html"]');
|
|
107
|
+
|
|
108
|
+
expect(concealedSpan.exists()).toBe(true);
|
|
109
|
+
expect(concealedSpan.classes()).toContain('conceal');
|
|
110
|
+
expect(concealedSpan.text()).not.toContain('secret-key-123');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -24,6 +24,7 @@ import DeveloperLoadExtensionDialog from '@shell/dialog/DeveloperLoadExtensionDi
|
|
|
24
24
|
import AddExtensionReposDialog from '@shell/dialog/AddExtensionReposDialog.vue';
|
|
25
25
|
import InstallExtensionDialog from '@shell/dialog/InstallExtensionDialog.vue';
|
|
26
26
|
import UninstallExtensionDialog from '@shell/dialog/UninstallExtensionDialog.vue';
|
|
27
|
+
import UninstallExistingExtensionDialog from '@shell/dialog/UninstallExistingExtensionDialog.vue';
|
|
27
28
|
import KnownHostsEditDialog from '@shell/dialog/KnownHostsEditDialog.vue';
|
|
28
29
|
import ImportDialog from '@shell/dialog/ImportDialog.vue';
|
|
29
30
|
import SearchDialog from '@shell/dialog/SearchDialog.vue';
|
|
@@ -110,6 +111,7 @@ describe('component: PromptModal', () => {
|
|
|
110
111
|
['AddExtensionReposDialog', AddExtensionReposDialog],
|
|
111
112
|
['InstallExtensionDialog', InstallExtensionDialog],
|
|
112
113
|
['UninstallExtensionDialog', UninstallExtensionDialog],
|
|
114
|
+
['UninstallExistingExtensionDialog', UninstallExistingExtensionDialog],
|
|
113
115
|
['KnownHostsEditDialog', KnownHostsEditDialog],
|
|
114
116
|
['ImportDialog', ImportDialog],
|
|
115
117
|
['SearchDialog', SearchDialog],
|
|
@@ -124,6 +124,15 @@ export default {
|
|
|
124
124
|
this.update();
|
|
125
125
|
}
|
|
126
126
|
},
|
|
127
|
+
|
|
128
|
+
allClusters(clusters: any[]) {
|
|
129
|
+
if (clusters.length) {
|
|
130
|
+
// Resolve metadata.name values to nameDisplay for UI display
|
|
131
|
+
this.selectedClusters = this.selectedClusters.map(
|
|
132
|
+
(name) => this.resolveClusterDisplayName(name)
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
},
|
|
127
136
|
},
|
|
128
137
|
|
|
129
138
|
computed: {
|
|
@@ -159,7 +168,7 @@ export default {
|
|
|
159
168
|
clustersOptions() {
|
|
160
169
|
return this.allClusters
|
|
161
170
|
.filter((x) => x.metadata.namespace === this.namespace && !isHarvesterCluster(x))
|
|
162
|
-
.map((x) => ({ label: x.nameDisplay, value: x.
|
|
171
|
+
.map((x) => ({ label: x.nameDisplay, value: x.nameDisplay }));
|
|
163
172
|
},
|
|
164
173
|
|
|
165
174
|
clusterGroupsOptions() {
|
|
@@ -352,6 +361,14 @@ export default {
|
|
|
352
361
|
return undefined;
|
|
353
362
|
},
|
|
354
363
|
|
|
364
|
+
resolveClusterDisplayName(name: string): string {
|
|
365
|
+
const cluster = this.allClusters.find(
|
|
366
|
+
(c: any) => c.metadata.namespace === this.namespace && c.metadata.name === name
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
return cluster ? cluster.nameDisplay : name;
|
|
370
|
+
},
|
|
371
|
+
|
|
355
372
|
reset() {
|
|
356
373
|
this.targetMode = 'all';
|
|
357
374
|
this.selectedClusters = [];
|
|
@@ -573,4 +573,75 @@ describe('component: FleetClusters', () => {
|
|
|
573
573
|
expect(additionalSubRow.exists()).toBe(false);
|
|
574
574
|
});
|
|
575
575
|
});
|
|
576
|
+
|
|
577
|
+
describe('labels visibility regardless of error state', () => {
|
|
578
|
+
it('should pass sub-rows prop as true to ResourceTable so labels always render', () => {
|
|
579
|
+
const wrapper = createWrapper();
|
|
580
|
+
|
|
581
|
+
// sub-rows=true ensures SortableTable.showSubRow() returns true,
|
|
582
|
+
// which makes the #additional-sub-row slot render regardless of stateDescription.
|
|
583
|
+
// Without this, labels only appear when there is an error (stateDescription).
|
|
584
|
+
const resourceTableStub = wrapper.findComponent('.resource-table') as any;
|
|
585
|
+
|
|
586
|
+
expect(resourceTableStub.props('subRows')).toBe(true);
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
it('should render labels when cluster has no stateDescription (no error)', () => {
|
|
590
|
+
const rows = [{
|
|
591
|
+
customLabels: ['env:prod', 'team:backend'],
|
|
592
|
+
displayCustomLabels: false,
|
|
593
|
+
stateDescription: undefined,
|
|
594
|
+
}];
|
|
595
|
+
|
|
596
|
+
const wrapper = createWrapper({ rows });
|
|
597
|
+
const tags = wrapper.findAll('.tag');
|
|
598
|
+
|
|
599
|
+
expect(tags).toHaveLength(2);
|
|
600
|
+
expect(tags[0].text()).toBe('env:prod');
|
|
601
|
+
expect(tags[1].text()).toBe('team:backend');
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it('should render labels when cluster has a stateDescription (error)', () => {
|
|
605
|
+
const rows = [{
|
|
606
|
+
customLabels: ['env:prod', 'team:backend'],
|
|
607
|
+
displayCustomLabels: false,
|
|
608
|
+
stateDescription: 'Something went wrong',
|
|
609
|
+
}];
|
|
610
|
+
|
|
611
|
+
const wrapper = createWrapper({ rows });
|
|
612
|
+
const tags = wrapper.findAll('.tag');
|
|
613
|
+
|
|
614
|
+
expect(tags).toHaveLength(2);
|
|
615
|
+
expect(tags[0].text()).toBe('env:prod');
|
|
616
|
+
expect(tags[1].text()).toBe('team:backend');
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it('should render labels when stateDescription is empty string', () => {
|
|
620
|
+
const rows = [{
|
|
621
|
+
customLabels: ['env:staging'],
|
|
622
|
+
displayCustomLabels: false,
|
|
623
|
+
stateDescription: '',
|
|
624
|
+
}];
|
|
625
|
+
|
|
626
|
+
const wrapper = createWrapper({ rows });
|
|
627
|
+
const tags = wrapper.findAll('.tag');
|
|
628
|
+
|
|
629
|
+
expect(tags).toHaveLength(1);
|
|
630
|
+
expect(tags[0].text()).toBe('env:staging');
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it('should render labels when stateDescription is null', () => {
|
|
634
|
+
const rows = [{
|
|
635
|
+
customLabels: ['region:eu-west'],
|
|
636
|
+
displayCustomLabels: false,
|
|
637
|
+
stateDescription: null,
|
|
638
|
+
}];
|
|
639
|
+
|
|
640
|
+
const wrapper = createWrapper({ rows });
|
|
641
|
+
const tags = wrapper.findAll('.tag');
|
|
642
|
+
|
|
643
|
+
expect(tags).toHaveLength(1);
|
|
644
|
+
expect(tags[0].text()).toBe('region:eu-west');
|
|
645
|
+
});
|
|
646
|
+
});
|
|
576
647
|
});
|
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
import labeledFormElement from '@shell/mixins/labeled-form-element';
|
|
3
2
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
4
3
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
5
4
|
import Select from '@shell/components/form/Select';
|
|
5
|
+
import { computed } from 'vue';
|
|
6
|
+
import { _VIEW, _EDIT } from '@shell/config/query-params';
|
|
7
|
+
|
|
6
8
|
export default {
|
|
7
|
-
name:
|
|
9
|
+
name: 'InputWithSelect',
|
|
10
|
+
|
|
11
|
+
inheritAttrs: false,
|
|
12
|
+
|
|
8
13
|
emits: ['update:value'],
|
|
9
14
|
components: {
|
|
10
15
|
LabeledInput,
|
|
11
16
|
LabeledSelect,
|
|
12
17
|
Select,
|
|
13
18
|
},
|
|
14
|
-
|
|
15
|
-
props: {
|
|
19
|
+
props: {
|
|
16
20
|
disabled: {
|
|
17
21
|
type: Boolean,
|
|
18
22
|
default: false,
|
|
@@ -84,10 +88,20 @@ export default {
|
|
|
84
88
|
selectRules: {
|
|
85
89
|
default: () => [],
|
|
86
90
|
type: Array,
|
|
91
|
+
},
|
|
92
|
+
mode: {
|
|
93
|
+
type: String,
|
|
94
|
+
default: _EDIT,
|
|
87
95
|
}
|
|
88
96
|
|
|
89
97
|
},
|
|
90
98
|
|
|
99
|
+
setup(props) {
|
|
100
|
+
const isView = computed(() => props.mode === _VIEW);
|
|
101
|
+
|
|
102
|
+
return { isView };
|
|
103
|
+
},
|
|
104
|
+
|
|
91
105
|
data() {
|
|
92
106
|
return {
|
|
93
107
|
selected: this.selectValue || this.options[0].value,
|
|
@@ -95,12 +109,6 @@ export default {
|
|
|
95
109
|
};
|
|
96
110
|
},
|
|
97
111
|
|
|
98
|
-
computed: {
|
|
99
|
-
canPaginate() {
|
|
100
|
-
return false;
|
|
101
|
-
}
|
|
102
|
-
},
|
|
103
|
-
|
|
104
112
|
methods: {
|
|
105
113
|
focus() {
|
|
106
114
|
const comp = this.$refs.text;
|
|
@@ -778,11 +778,16 @@ export default {
|
|
|
778
778
|
@onInput="onInputMarkdownMultiline(i, $event)"
|
|
779
779
|
@onFocus="onFocusMarkdownMultiline(i, $event)"
|
|
780
780
|
/>
|
|
781
|
+
<div
|
|
782
|
+
v-else-if="valueConcealed"
|
|
783
|
+
class="concealed-value conceal"
|
|
784
|
+
data-testid="concealed-value"
|
|
785
|
+
:aria-label="t('generic.ariaLabel.value', {index: i+1})"
|
|
786
|
+
/>
|
|
781
787
|
<TextAreaAutoGrow
|
|
782
788
|
v-else-if="valueMultiline && row[valueName] !== undefined"
|
|
783
789
|
v-model:value="row[valueName]"
|
|
784
790
|
data-testid="value-multiline"
|
|
785
|
-
:class="{'conceal': valueConcealed}"
|
|
786
791
|
:disabled="disabled"
|
|
787
792
|
:mode="mode"
|
|
788
793
|
:placeholder="_valuePlaceholder"
|
|
@@ -809,6 +814,7 @@ export default {
|
|
|
809
814
|
class="btn btn-sm role-secondary file-selector"
|
|
810
815
|
:label="t('generic.upload')"
|
|
811
816
|
:include-file-name="true"
|
|
817
|
+
:accept="readAccept"
|
|
812
818
|
:aria-label="t('generic.ariaLabel.value', {index: i+1})"
|
|
813
819
|
@selected="onValueFileSelected(i, $event)"
|
|
814
820
|
/>
|
|
@@ -896,6 +902,7 @@ export default {
|
|
|
896
902
|
class="role-tertiary"
|
|
897
903
|
:label="t('generic.readFromFile')"
|
|
898
904
|
:include-file-name="true"
|
|
905
|
+
:accept="readAccept"
|
|
899
906
|
data-testid="read_all_key_value_button"
|
|
900
907
|
@selected="onFileSelected"
|
|
901
908
|
/>
|
|
@@ -941,6 +948,15 @@ export default {
|
|
|
941
948
|
padding: 10px 10px 10px 10px;
|
|
942
949
|
}
|
|
943
950
|
|
|
951
|
+
.concealed-value {
|
|
952
|
+
padding: 10px;
|
|
953
|
+
min-height: 40px;
|
|
954
|
+
user-select: none;
|
|
955
|
+
&::before {
|
|
956
|
+
content: '••••••••••••••••••••';
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
944
960
|
.text-monospace:not(.conceal) {
|
|
945
961
|
font-family: monospace, monospace;
|
|
946
962
|
}
|