@rancher/shell 3.0.5-rc.8 → 3.0.5
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/styles/base/_color.scss +4 -1
- package/assets/styles/global/_tooltip.scss +7 -4
- package/assets/styles/themes/_dark.scss +11 -0
- package/assets/styles/themes/_light.scss +13 -1
- package/assets/styles/themes/_modern.scss +22 -0
- package/assets/translations/en-us.yaml +147 -19
- package/assets/translations/zh-hans.yaml +0 -1
- package/chart/monitoring/grafana/index.vue +8 -2
- package/components/ActionMenuShell.vue +3 -1
- package/components/Cron/CronExpressionEditor.vue +299 -0
- package/components/Cron/CronExpressionEditorModal.vue +247 -0
- package/components/Cron/CronTooltip.vue +87 -0
- package/components/Cron/types.ts +13 -0
- package/components/ForceDirectedTreeChart/composable.ts +11 -0
- package/components/PodSecurityAdmission.vue +2 -0
- package/components/PromptModal.vue +1 -1
- package/components/Resource/Detail/Card/__tests__/StateCard.test.ts +1 -0
- package/components/Resource/Detail/CopyToClipboard.vue +78 -0
- package/components/Resource/Detail/FetchLoader/__tests__/composables.test.ts +69 -0
- package/components/Resource/Detail/FetchLoader/composables.ts +27 -0
- package/components/Resource/Detail/Metadata/Annotations/__tests__/index.test.ts +1 -1
- package/components/Resource/Detail/Metadata/Annotations/index.vue +1 -1
- package/components/Resource/Detail/Metadata/IdentifyingInformation/__tests__/identifying-fields.test.ts +13 -61
- package/components/Resource/Detail/Metadata/IdentifyingInformation/__tests__/index.test.ts +33 -6
- package/components/Resource/Detail/Metadata/IdentifyingInformation/identifying-fields.ts +24 -38
- package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +25 -5
- package/components/Resource/Detail/Metadata/KeyValue.vue +12 -23
- package/components/Resource/Detail/Metadata/KeyValueRow.vue +144 -0
- package/components/Resource/Detail/Metadata/Labels/__tests__/index.test.ts +1 -0
- package/components/Resource/Detail/Metadata/Labels/index.vue +1 -0
- package/components/Resource/Detail/Metadata/__tests__/KeyValue.test.ts +30 -32
- package/components/Resource/Detail/Metadata/__tests__/KeyValueRow.test.ts +108 -0
- package/components/Resource/Detail/Metadata/__tests__/composables.test.ts +0 -3
- package/components/Resource/Detail/Metadata/__tests__/index.test.ts +12 -5
- package/components/Resource/Detail/Metadata/composables.ts +1 -4
- package/components/Resource/Detail/Metadata/index.vue +1 -0
- package/components/Resource/Detail/Preview/Content.vue +63 -0
- package/components/Resource/Detail/Preview/Preview.vue +128 -0
- package/components/Resource/Detail/Preview/__tests__/Content.spec.ts +71 -0
- package/components/Resource/Detail/Preview/__tests__/Preview.spec.ts +121 -0
- package/components/Resource/Detail/ResourcePopover/ResourcePopoverCard.vue +141 -0
- package/components/Resource/Detail/ResourcePopover/__tests__/ResourcePopoverCard.test.ts +136 -0
- package/components/Resource/Detail/ResourcePopover/__tests__/index.test.ts +245 -0
- package/components/Resource/Detail/ResourcePopover/index.vue +226 -0
- package/components/Resource/Detail/SpacedRow.vue +1 -0
- package/components/Resource/Detail/TitleBar/__tests__/composables.test.ts +0 -5
- package/components/Resource/Detail/TitleBar/__tests__/index.test.ts +1 -1
- package/components/Resource/Detail/TitleBar/composables.ts +1 -3
- package/components/Resource/Detail/TitleBar/index.vue +2 -29
- package/components/Resource/Detail/ViewOptions/composable.ts +9 -0
- package/components/Resource/Detail/ViewOptions/index.vue +41 -0
- package/components/Resource/Detail/__tests__/CopyToClipboard.spec.ts +82 -0
- package/components/ResourceDetail/Masthead/legacy.vue +0 -19
- package/components/ResourceDetail/index.vue +1 -26
- package/components/ResourceTable.vue +24 -0
- package/components/SortableTable/index.vue +7 -1
- package/components/SortableTable/paging.js +3 -0
- package/components/Tabbed/Tab.vue +43 -1
- package/components/Tabbed/index.vue +3 -1
- package/components/__tests__/Cron/CronExpressionEditor.test.ts +151 -0
- package/components/__tests__/Cron/CronExpressionEditorModal.test.ts +81 -0
- package/components/auth/login/saml.vue +86 -0
- package/components/form/LabeledSelect.vue +8 -8
- package/components/form/ProjectMemberEditor.vue +2 -0
- package/components/form/ResourceTabs/composable.ts +54 -0
- package/components/form/ResourceTabs/index.vue +10 -7
- package/components/form/Select.vue +13 -10
- package/components/form/__tests__/LabeledSelect.test.ts +133 -0
- package/components/form/__tests__/Select.test.ts +134 -0
- package/components/nav/Header.vue +6 -5
- package/composables/useExtensionManager.ts +17 -0
- package/config/home-links.js +12 -0
- package/config/labels-annotations.js +0 -1
- package/config/page-actions.js +0 -1
- package/config/product/explorer.js +3 -1
- package/config/product/fleet.js +2 -7
- package/config/product/manager.js +0 -5
- package/config/query-params.js +1 -0
- package/config/router/navigation-guards/clusters.js +2 -1
- package/config/router/navigation-guards/products.js +1 -1
- package/config/store.js +2 -0
- package/core/extension-manager-impl.js +518 -0
- package/core/plugins.js +35 -468
- package/core/types.ts +8 -2
- package/detail/__tests__/autoscaling.horizontalpodautoscaler.test.ts +1 -0
- package/detail/catalog.cattle.io.app.vue +7 -4
- package/detail/fleet.cattle.io.bundle.vue +1 -5
- package/detail/fleet.cattle.io.cluster.vue +3 -2
- package/detail/fleet.cattle.io.gitrepo.vue +76 -49
- package/detail/fleet.cattle.io.helmop.vue +78 -49
- package/dialog/AddonConfigConfirmationDialog.vue +1 -1
- package/dialog/GenericPrompt.vue +1 -1
- package/dialog/ImportDialog.vue +9 -2
- package/dialog/InstallExtensionDialog.vue +18 -10
- package/dialog/SloDialog.vue +1 -1
- package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +2 -1
- package/edit/__tests__/resources.cattle.io.restore.test.ts +106 -0
- package/edit/auth/oidc.vue +106 -6
- package/edit/auth/saml.vue +5 -5
- package/edit/cloudcredential.vue +31 -17
- package/edit/constraints.gatekeeper.sh.constraint/index.vue +10 -2
- package/edit/fleet.cattle.io.cluster.vue +19 -0
- package/edit/fleet.cattle.io.gitrepo.vue +23 -16
- package/edit/monitoring.coreos.com.alertmanagerconfig/index.vue +12 -11
- package/edit/monitoring.coreos.com.alertmanagerconfig/receiverConfig.vue +11 -1
- package/edit/provisioning.cattle.io.cluster/index.vue +14 -19
- package/edit/provisioning.cattle.io.cluster/rke2.vue +11 -3
- package/edit/provisioning.cattle.io.cluster/tabs/AddOnAdditionalManifest.vue +1 -0
- package/edit/provisioning.cattle.io.cluster/tabs/AddOnConfig.vue +1 -0
- package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +1 -0
- package/edit/provisioning.cattle.io.cluster/tabs/etcd/S3Config.vue +1 -0
- package/edit/provisioning.cattle.io.cluster/tabs/registries/index.vue +2 -0
- package/edit/provisioning.cattle.io.cluster/tabs/upgrade/DrainOptions.vue +6 -0
- package/edit/resources.cattle.io.restore.vue +5 -8
- package/initialize/install-plugins.js +1 -3
- package/list/__tests__/workload.test.ts +1 -0
- package/list/workload.vue +8 -1
- package/machine-config/components/GCEImage.vue +6 -5
- package/machine-config/google.vue +11 -6
- package/mixins/__tests__/auth-config.test.ts +4 -6
- package/mixins/__tests__/chart.test.ts +139 -1
- package/mixins/auth-config.js +33 -10
- package/mixins/chart.js +58 -18
- package/models/__tests__/namespace.test.ts +69 -0
- package/models/apps.statefulset.js +8 -10
- package/models/chart.js +5 -1
- package/models/fleet-application.js +16 -46
- package/models/fleet.cattle.io.bundle.js +1 -38
- package/models/fleet.cattle.io.gitrepo.js +4 -0
- package/models/fleet.cattle.io.helmop.js +4 -0
- package/models/management.cattle.io.cluster.js +1 -1
- package/models/management.cattle.io.project.js +12 -0
- package/models/namespace.js +30 -0
- package/models/workload.js +4 -1
- package/package.json +10 -10
- package/pages/auth/login.vue +8 -3
- package/pages/auth/logout.vue +6 -5
- package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +26 -11
- package/pages/c/_cluster/apps/charts/chart.vue +29 -20
- package/pages/c/_cluster/apps/charts/index.vue +1 -0
- package/pages/c/_cluster/apps/charts/install.vue +6 -5
- package/pages/c/_cluster/explorer/tools/__tests__/index.test.ts +102 -12
- package/pages/c/_cluster/explorer/tools/index.vue +145 -254
- package/pages/c/_cluster/manager/cloudCredential/index.vue +18 -1
- package/pages/c/_cluster/manager/drivers/kontainerDriver/index.vue +12 -2
- package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +1 -1
- package/pages/c/_cluster/uiplugins/__tests__/index.spec.ts +318 -0
- package/pages/c/_cluster/uiplugins/index.vue +221 -363
- package/pages/home.vue +1 -9
- package/plugins/axios.js +3 -2
- package/plugins/dashboard-store/resource-class.js +49 -0
- package/plugins/ember-cookie.js +7 -3
- package/plugins/steve/subscribe.js +4 -2
- package/public/index.html +2 -1
- package/rancher-components/Card/Card.vue +1 -1
- package/rancher-components/Form/Checkbox/Checkbox.vue +1 -1
- package/rancher-components/Form/Radio/RadioButton.vue +1 -1
- package/rancher-components/Form/Radio/RadioGroup.vue +1 -1
- package/rancher-components/LabeledTooltip/LabeledTooltip.vue +1 -11
- package/rancher-components/Pill/RcCounterBadge/RcCounterBadge.test.ts +53 -0
- package/rancher-components/Pill/RcCounterBadge/RcCounterBadge.vue +65 -0
- package/rancher-components/Pill/RcCounterBadge/index.ts +1 -0
- package/rancher-components/Pill/RcCounterBadge/types.ts +7 -0
- package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +1 -1
- package/rancher-components/Pill/RcStatusBadge/index.ts +1 -1
- package/rancher-components/Pill/RcStatusIndicator/RcStatusIndicator.vue +3 -3
- package/rancher-components/Pill/RcStatusIndicator/types.ts +1 -1
- package/rancher-components/Pill/RcTag/RcTag.test.ts +64 -0
- package/rancher-components/Pill/RcTag/RcTag.vue +94 -0
- package/rancher-components/Pill/RcTag/index.ts +1 -0
- package/rancher-components/Pill/RcTag/types.ts +9 -0
- package/rancher-components/Pill/types.ts +1 -0
- package/rancher-components/RcItemCard/RcItemCard.vue +1 -0
- package/rancher-components/RcItemCard/RcItemCardAction.vue +12 -0
- package/scripts/test-plugins-build.sh +0 -1
- package/store/__tests__/catalog.test.ts +63 -0
- package/store/__tests__/cookies.test.ts +72 -0
- package/store/auth.js +33 -10
- package/store/catalog.js +2 -2
- package/store/cookies.ts +30 -0
- package/store/prefs.js +10 -5
- package/store/type-map.js +3 -15
- package/types/extension-manager.ts +26 -0
- package/types/shell/index.d.ts +123 -27
- package/utils/__tests__/product.test.ts +129 -0
- package/utils/__tests__/resource.test.ts +87 -0
- package/utils/alertmanagerconfig.js +2 -2
- package/utils/auth.js +4 -77
- package/utils/product.ts +39 -0
- package/utils/resource.ts +35 -0
- package/utils/select.js +0 -24
- package/utils/validators/formRules/__tests__/index.test.ts +3 -0
- package/utils/validators/formRules/index.ts +2 -1
- package/vue.config.js +1 -1
- package/components/Resource/Detail/Metadata/Rectangle.vue +0 -34
- package/components/Resource/Detail/Metadata/__tests__/Rectangle.test.ts +0 -24
- package/components/ResourceDetail/Masthead/__tests__/legacy.test.ts +0 -65
- package/utils/cookie-universal.js +0 -10
- /package/components/{ForceDirectedTreeChart.vue → ForceDirectedTreeChart/index.vue} +0 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils';
|
|
2
|
+
import CopyToClipboard from '@shell/components/Resource/Detail/CopyToClipboard.vue';
|
|
3
|
+
import { copyTextToClipboard } from '@shell/utils/clipboard';
|
|
4
|
+
|
|
5
|
+
// Mock the clipboard utility
|
|
6
|
+
jest.mock('@shell/utils/clipboard', () => ({ copyTextToClipboard: jest.fn() }));
|
|
7
|
+
jest.mock('vuex', () => ({ useStore: () => { } }));
|
|
8
|
+
|
|
9
|
+
describe('component: CopyToClipboard', () => {
|
|
10
|
+
beforeAll(() => {
|
|
11
|
+
jest.useFakeTimers();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterAll(() => {
|
|
15
|
+
jest.useRealTimers();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
jest.clearAllMocks();
|
|
20
|
+
jest.clearAllTimers();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should render a button', () => {
|
|
24
|
+
const wrapper = mount(CopyToClipboard, { props: { value: 'test-value' } });
|
|
25
|
+
|
|
26
|
+
const button = wrapper.find('button');
|
|
27
|
+
|
|
28
|
+
expect(button.exists()).toBe(true);
|
|
29
|
+
expect(button.classes()).toContain('copy-to-clipboard');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should call copyTextToClipboard with the correct value on click', async() => {
|
|
33
|
+
const testValue = 'my-secret-text';
|
|
34
|
+
const wrapper = mount(CopyToClipboard, { props: { value: testValue } });
|
|
35
|
+
|
|
36
|
+
await wrapper.find('button').trigger('click');
|
|
37
|
+
|
|
38
|
+
expect(copyTextToClipboard).toHaveBeenCalledTimes(1);
|
|
39
|
+
expect(copyTextToClipboard).toHaveBeenCalledWith(testValue);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should add the "copied" class on click and remove it after a timeout', async() => {
|
|
43
|
+
const wrapper = mount(CopyToClipboard, { props: { value: 'test-value' } });
|
|
44
|
+
|
|
45
|
+
const button = wrapper.find('button');
|
|
46
|
+
|
|
47
|
+
await button.trigger('click');
|
|
48
|
+
|
|
49
|
+
expect(button.classes()).toContain('copied');
|
|
50
|
+
|
|
51
|
+
// Advance timers by 2 seconds
|
|
52
|
+
jest.advanceTimersByTime(2000);
|
|
53
|
+
await wrapper.vm.$nextTick();
|
|
54
|
+
await wrapper.vm.$nextTick();
|
|
55
|
+
|
|
56
|
+
expect(wrapper.find('button').classes()).not.toContain('copied');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should not reset the timeout if clicked multiple times', async() => {
|
|
60
|
+
const wrapper = mount(CopyToClipboard, { props: { value: 'test-value' } });
|
|
61
|
+
|
|
62
|
+
const button = wrapper.find('button');
|
|
63
|
+
|
|
64
|
+
// First click
|
|
65
|
+
await button.trigger('click');
|
|
66
|
+
expect(wrapper.find('button').classes()).toContain('copied');
|
|
67
|
+
|
|
68
|
+
// Advance time by 1 second
|
|
69
|
+
jest.advanceTimersByTime(1000);
|
|
70
|
+
|
|
71
|
+
// Second click
|
|
72
|
+
await button.trigger('click');
|
|
73
|
+
expect(wrapper.find('button').classes()).toContain('copied');
|
|
74
|
+
|
|
75
|
+
// The timeout should not have been reset. After another 1 second, the original timeout should fire.
|
|
76
|
+
jest.advanceTimersByTime(1000);
|
|
77
|
+
await wrapper.vm.$nextTick();
|
|
78
|
+
await wrapper.vm.$nextTick();
|
|
79
|
+
|
|
80
|
+
expect(wrapper.find('button').classes()).not.toContain('copied');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -526,25 +526,6 @@ export default {
|
|
|
526
526
|
:value="value.creationTimestamp"
|
|
527
527
|
/>
|
|
528
528
|
</span>
|
|
529
|
-
<span
|
|
530
|
-
v-if="value.showCreatedBy"
|
|
531
|
-
data-testid="masthead-subheader-createdBy"
|
|
532
|
-
>
|
|
533
|
-
{{ t("resourceDetail.masthead.createdBy") }}:
|
|
534
|
-
<router-link
|
|
535
|
-
v-if="value.createdBy.location"
|
|
536
|
-
:to="value.createdBy.location"
|
|
537
|
-
data-testid="masthead-subheader-createdBy-link"
|
|
538
|
-
>
|
|
539
|
-
{{ value.createdBy.displayName }}
|
|
540
|
-
</router-link>
|
|
541
|
-
<span
|
|
542
|
-
v-else
|
|
543
|
-
data-testid="masthead-subheader-createdBy_plain-text"
|
|
544
|
-
>
|
|
545
|
-
{{ value.createdBy.displayName }}
|
|
546
|
-
</span>
|
|
547
|
-
</span>
|
|
548
529
|
</div>
|
|
549
530
|
</div>
|
|
550
531
|
<slot name="right">
|
|
@@ -4,7 +4,7 @@ import Loading from '@shell/components/Loading';
|
|
|
4
4
|
import ResourceYaml from '@shell/components/ResourceYaml';
|
|
5
5
|
import {
|
|
6
6
|
_VIEW, _EDIT, _CLONE, _IMPORT, _STAGE, _CREATE,
|
|
7
|
-
AS, _YAML, _DETAIL, _CONFIG,
|
|
7
|
+
AS, _YAML, _DETAIL, _CONFIG, PREVIEW, MODE,
|
|
8
8
|
} from '@shell/config/query-params';
|
|
9
9
|
import { SCHEMA } from '@shell/config/types';
|
|
10
10
|
import { createYaml } from '@shell/utils/create-yaml';
|
|
@@ -12,7 +12,6 @@ import Masthead from '@shell/components/ResourceDetail/Masthead';
|
|
|
12
12
|
import DetailTop from '@shell/components/DetailTop';
|
|
13
13
|
import { clone, diff } from '@shell/utils/object';
|
|
14
14
|
import IconMessage from '@shell/components/IconMessage';
|
|
15
|
-
import ForceDirectedTreeChart from '@shell/components/ForceDirectedTreeChart';
|
|
16
15
|
import { stringify } from '@shell/utils/error';
|
|
17
16
|
import { Banner } from '@components/Banner';
|
|
18
17
|
|
|
@@ -45,7 +44,6 @@ export default {
|
|
|
45
44
|
components: {
|
|
46
45
|
Loading,
|
|
47
46
|
DetailTop,
|
|
48
|
-
ForceDirectedTreeChart,
|
|
49
47
|
ResourceYaml,
|
|
50
48
|
Masthead,
|
|
51
49
|
IconMessage,
|
|
@@ -106,8 +104,6 @@ export default {
|
|
|
106
104
|
// know about: view, edit, create (stage, import and clone become "create")
|
|
107
105
|
const mode = ([_CLONE, _IMPORT, _STAGE].includes(realMode) ? _CREATE : realMode);
|
|
108
106
|
|
|
109
|
-
const getGraphConfig = store.getters['type-map/hasGraph'](resourceType);
|
|
110
|
-
const hasGraph = !!getGraphConfig;
|
|
111
107
|
const hasCustomDetail = store.getters['type-map/hasCustomDetail'](resourceType, id);
|
|
112
108
|
const hasCustomEdit = store.getters['type-map/hasCustomEdit'](resourceType, id);
|
|
113
109
|
|
|
@@ -120,8 +116,6 @@ export default {
|
|
|
120
116
|
|
|
121
117
|
if ( mode === _VIEW && hasCustomDetail && (!requested || requested === _DETAIL) ) {
|
|
122
118
|
as = _DETAIL;
|
|
123
|
-
} else if ( mode === _VIEW && hasGraph && requested === _GRAPH) {
|
|
124
|
-
as = _GRAPH;
|
|
125
119
|
} else if ( hasCustomEdit && (!requested || requested === _CONFIG) ) {
|
|
126
120
|
as = _CONFIG;
|
|
127
121
|
} else {
|
|
@@ -213,10 +207,6 @@ export default {
|
|
|
213
207
|
}
|
|
214
208
|
}
|
|
215
209
|
|
|
216
|
-
if ( as === _GRAPH ) {
|
|
217
|
-
this.chartData = liveModel;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
210
|
if ( [_CLONE, _IMPORT, _STAGE].includes(realMode) ) {
|
|
221
211
|
model.cleanForNew();
|
|
222
212
|
yaml = model.cleanYaml(yaml, realMode);
|
|
@@ -231,8 +221,6 @@ export default {
|
|
|
231
221
|
}
|
|
232
222
|
|
|
233
223
|
const out = {
|
|
234
|
-
hasGraph,
|
|
235
|
-
getGraphConfig,
|
|
236
224
|
hasCustomDetail,
|
|
237
225
|
hasCustomEdit,
|
|
238
226
|
canViewYaml,
|
|
@@ -256,11 +244,9 @@ export default {
|
|
|
256
244
|
},
|
|
257
245
|
data() {
|
|
258
246
|
return {
|
|
259
|
-
chartData: null,
|
|
260
247
|
resourceSubtype: null,
|
|
261
248
|
|
|
262
249
|
// Set by fetch
|
|
263
|
-
hasGraph: null,
|
|
264
250
|
hasCustomDetail: null,
|
|
265
251
|
hasCustomEdit: null,
|
|
266
252
|
resourceType: null,
|
|
@@ -298,10 +284,6 @@ export default {
|
|
|
298
284
|
return this.as === _DETAIL;
|
|
299
285
|
},
|
|
300
286
|
|
|
301
|
-
isGraph() {
|
|
302
|
-
return this.as === _GRAPH;
|
|
303
|
-
},
|
|
304
|
-
|
|
305
287
|
offerPreview() {
|
|
306
288
|
return this.as === _YAML && [_EDIT, _CLONE, _IMPORT, _STAGE].includes(this.mode);
|
|
307
289
|
},
|
|
@@ -468,7 +450,6 @@ export default {
|
|
|
468
450
|
:mode="mode"
|
|
469
451
|
:real-mode="realMode"
|
|
470
452
|
:as="as"
|
|
471
|
-
:has-graph="hasGraph"
|
|
472
453
|
:has-detail="hasCustomDetail"
|
|
473
454
|
:has-edit="hasCustomEdit"
|
|
474
455
|
:can-view-yaml="canViewYaml"
|
|
@@ -498,12 +479,6 @@ export default {
|
|
|
498
479
|
/>
|
|
499
480
|
</div>
|
|
500
481
|
|
|
501
|
-
<ForceDirectedTreeChart
|
|
502
|
-
v-if="isGraph"
|
|
503
|
-
:data="chartData"
|
|
504
|
-
:fdc-config="getGraphConfig"
|
|
505
|
-
/>
|
|
506
|
-
|
|
507
482
|
<ResourceYaml
|
|
508
483
|
v-else-if="isYaml"
|
|
509
484
|
ref="resourceyaml"
|
|
@@ -304,6 +304,7 @@ export default {
|
|
|
304
304
|
},
|
|
305
305
|
|
|
306
306
|
_headers() {
|
|
307
|
+
// :TableColumn[]
|
|
307
308
|
let headers;
|
|
308
309
|
const showNamespace = this.showNamespaceColumn;
|
|
309
310
|
|
|
@@ -339,6 +340,29 @@ export default {
|
|
|
339
340
|
|
|
340
341
|
// adding extension defined cols to the correct header config
|
|
341
342
|
extensionCols.forEach((col) => {
|
|
343
|
+
if (this.externalPaginationEnabled) {
|
|
344
|
+
// validate that the required settings are supplied to enable search and sort server-side
|
|
345
|
+
// these do not check other invalid scenarios like a path is a string but to a model property, or that the field supports sort/search via api (some basic non-breaking checks are done further on)
|
|
346
|
+
if (
|
|
347
|
+
col.search !== false && // search is explicitly disabled
|
|
348
|
+
(typeof col.search !== 'string' && !Array.isArray(col.search)) && // primary property path to search on
|
|
349
|
+
typeof col.value !== 'string' // secondary property path to search on
|
|
350
|
+
) {
|
|
351
|
+
console.warn(`Unable to support server-side search for extension provided column "${ col.name || col.label || col.labelKey }" (column must provide \`search\` or \`value\` property containing a path to a property in the resource. search can be an array).`); // eslint-disable-line no-console
|
|
352
|
+
|
|
353
|
+
col.search = false;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (
|
|
357
|
+
col.sort !== false && // sort is explicitly disabled
|
|
358
|
+
(typeof col.sort !== 'string' && !Array.isArray(col.sort)) // primary property path to sort on
|
|
359
|
+
) {
|
|
360
|
+
console.warn(`Unable to support server-side sort for extension provided column "${ col.name || col.label || col.labelKey }" (column must provide \`sort\` property containing a path to a property, or array of paths, in the resource)`); // eslint-disable-line no-console
|
|
361
|
+
|
|
362
|
+
col.sort = false;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
342
366
|
// we need the 'value' prop to be populated in order for the rows to show the values
|
|
343
367
|
if (!col.value && col.getValue) {
|
|
344
368
|
col.value = col.getValue;
|
|
@@ -26,6 +26,7 @@ import ButtonMultiAction from '@shell/components/ButtonMultiAction.vue';
|
|
|
26
26
|
import ActionMenu from '@shell/components/ActionMenuShell.vue';
|
|
27
27
|
import { useRuntimeFlag } from '@shell/composables/useRuntimeFlag';
|
|
28
28
|
import ActionDropdownShell from '@shell/components/ActionDropdownShell.vue';
|
|
29
|
+
import { useTabCountUpdater } from '@shell/components/form/ResourceTabs/composable';
|
|
29
30
|
|
|
30
31
|
// Uncomment for table performance debugging
|
|
31
32
|
// import tableDebug from './debug';
|
|
@@ -51,7 +52,7 @@ export default {
|
|
|
51
52
|
'group-value-change',
|
|
52
53
|
'selection',
|
|
53
54
|
'rowClick',
|
|
54
|
-
'enter'
|
|
55
|
+
'enter'
|
|
55
56
|
],
|
|
56
57
|
|
|
57
58
|
components: {
|
|
@@ -432,6 +433,7 @@ export default {
|
|
|
432
433
|
$main?.addEventListener('scroll', this._onScroll);
|
|
433
434
|
|
|
434
435
|
this.debouncedPaginationChanged();
|
|
436
|
+
this.updateTabCount(this.totalRows);
|
|
435
437
|
},
|
|
436
438
|
|
|
437
439
|
beforeUnmount() {
|
|
@@ -445,6 +447,7 @@ export default {
|
|
|
445
447
|
const $main = document.querySelector('main');
|
|
446
448
|
|
|
447
449
|
$main?.removeEventListener('scroll', this._onScroll);
|
|
450
|
+
this.clearTabCount();
|
|
448
451
|
},
|
|
449
452
|
|
|
450
453
|
watch: {
|
|
@@ -559,10 +562,13 @@ export default {
|
|
|
559
562
|
|
|
560
563
|
const store = useStore();
|
|
561
564
|
const { featureDropdownMenu } = useRuntimeFlag(store);
|
|
565
|
+
const { updateTabCount, clearTabCount } = useTabCountUpdater();
|
|
562
566
|
|
|
563
567
|
return {
|
|
564
568
|
table,
|
|
565
569
|
featureDropdownMenu,
|
|
570
|
+
updateTabCount,
|
|
571
|
+
clearTabCount
|
|
566
572
|
};
|
|
567
573
|
},
|
|
568
574
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script>
|
|
2
|
+
import { useTabCountWatcher } from '@shell/components/form/ResourceTabs/composable';
|
|
3
|
+
|
|
2
4
|
export default {
|
|
3
5
|
inject: ['addTab', 'removeTab', 'sideTabs'],
|
|
4
6
|
|
|
@@ -43,6 +45,20 @@ export default {
|
|
|
43
45
|
required: false,
|
|
44
46
|
type: Number
|
|
45
47
|
},
|
|
48
|
+
/**
|
|
49
|
+
* False to hide the count from being displayed in a tab.
|
|
50
|
+
* Number override/display the number as the count on the tab.
|
|
51
|
+
*/
|
|
52
|
+
count: {
|
|
53
|
+
default: undefined,
|
|
54
|
+
type: [Number, Boolean]
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
setup(props) {
|
|
59
|
+
const { count, isCountVisible } = useTabCountWatcher();
|
|
60
|
+
|
|
61
|
+
return { inferredCount: count, isInferredCountVisible: isCountVisible };
|
|
46
62
|
},
|
|
47
63
|
|
|
48
64
|
data() {
|
|
@@ -50,7 +66,7 @@ export default {
|
|
|
50
66
|
},
|
|
51
67
|
|
|
52
68
|
computed: {
|
|
53
|
-
|
|
69
|
+
baseLabelDisplay() {
|
|
54
70
|
if ( this.labelKey ) {
|
|
55
71
|
return this.$store.getters['i18n/t'](this.labelKey);
|
|
56
72
|
}
|
|
@@ -62,12 +78,38 @@ export default {
|
|
|
62
78
|
return this.name;
|
|
63
79
|
},
|
|
64
80
|
|
|
81
|
+
labelDisplay() {
|
|
82
|
+
const baseLabel = this.baseLabelDisplay;
|
|
83
|
+
|
|
84
|
+
if ( this.displayCount === false ) {
|
|
85
|
+
return baseLabel;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return `${ baseLabel } (${ this.displayCount })`;
|
|
89
|
+
},
|
|
90
|
+
|
|
65
91
|
shouldShowHeader() {
|
|
66
92
|
if ( this.showHeader !== null ) {
|
|
67
93
|
return this.showHeader;
|
|
68
94
|
}
|
|
69
95
|
|
|
70
96
|
return this.sideTabs || false;
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
displayCount() {
|
|
100
|
+
if (this.count === false) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (typeof this.count === 'number') {
|
|
105
|
+
return this.count;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (this.isInferredCountVisible) {
|
|
109
|
+
return this.inferredCount;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return false;
|
|
71
113
|
}
|
|
72
114
|
},
|
|
73
115
|
|
|
@@ -299,7 +299,9 @@ export default {
|
|
|
299
299
|
@click.prevent="select(tab.name, $event)"
|
|
300
300
|
@keyup.enter.space="select(tab.name, $event)"
|
|
301
301
|
>
|
|
302
|
-
<span>
|
|
302
|
+
<span>
|
|
303
|
+
{{ tab.labelDisplay }}
|
|
304
|
+
</span>
|
|
303
305
|
<span
|
|
304
306
|
v-if="tab.badge"
|
|
305
307
|
class="tab-badge"
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/* eslint-disable jest/no-hooks */
|
|
2
|
+
import { mount, VueWrapper } from '@vue/test-utils';
|
|
3
|
+
import { nextTick } from 'vue';
|
|
4
|
+
import { createStore } from 'vuex';
|
|
5
|
+
import CronExpressionEditor from '@shell/components/Cron/CronExpressionEditor.vue';
|
|
6
|
+
import type { CronField } from '@shell/components/Cron/types';
|
|
7
|
+
|
|
8
|
+
const translations: Record<string, string> = {
|
|
9
|
+
'component.cron.expressionEditor.label.minute': 'Minute',
|
|
10
|
+
'component.cron.expressionEditor.label.hour': 'Hour',
|
|
11
|
+
'component.cron.expressionEditor.label.dayOfMonth': 'Day of Month',
|
|
12
|
+
'component.cron.expressionEditor.label.month': 'Month',
|
|
13
|
+
'component.cron.expressionEditor.label.dayOfWeek': 'Day of Week',
|
|
14
|
+
'component.cron.expressionEditor.invalidValue': 'Invalid value',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const store = createStore({});
|
|
18
|
+
|
|
19
|
+
interface CronExpressionEditorVm extends InstanceType<typeof CronExpressionEditor> {
|
|
20
|
+
cronValues: Record<CronField, string>;
|
|
21
|
+
handleInput: (field: CronField, value: string) => void;
|
|
22
|
+
isValid: boolean;
|
|
23
|
+
readableCron: string;
|
|
24
|
+
errors: Record<CronField, boolean>;
|
|
25
|
+
focusedField: Record<CronField, boolean>;
|
|
26
|
+
tooltipRefs: Record<CronField, unknown>;
|
|
27
|
+
popperInstances: Record<CronField, unknown>;
|
|
28
|
+
handleFocus: (field: CronField) => void;
|
|
29
|
+
handleBlur: (field: CronField) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('cronExpressionEditor', () => {
|
|
33
|
+
let wrapper: VueWrapper<CronExpressionEditorVm>;
|
|
34
|
+
|
|
35
|
+
const factory = (props: Partial<CronExpressionEditorVm> = {}) => mount(CronExpressionEditor, {
|
|
36
|
+
global: {
|
|
37
|
+
plugins: [store],
|
|
38
|
+
stubs: {
|
|
39
|
+
CronTooltip: true,
|
|
40
|
+
LabeledInput: {
|
|
41
|
+
name: 'LabeledInput',
|
|
42
|
+
props: ['label', 'tooltip', 'type', 'value'],
|
|
43
|
+
template: `
|
|
44
|
+
<div>
|
|
45
|
+
<label>{{ label }}</label>
|
|
46
|
+
<input ref="value" :value="value" />
|
|
47
|
+
</div>
|
|
48
|
+
`
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
mocks: { t: (key: string) => translations[key] || key },
|
|
52
|
+
},
|
|
53
|
+
props: { cronExpression: '0 0 * * *', ...props },
|
|
54
|
+
}) as VueWrapper<CronExpressionEditorVm>;
|
|
55
|
+
|
|
56
|
+
afterEach(() => wrapper?.unmount());
|
|
57
|
+
|
|
58
|
+
const getEmitted = (event: string) => wrapper.emitted(event) as unknown[][] || [];
|
|
59
|
+
|
|
60
|
+
it('renders 5 input fields with correct labels', () => {
|
|
61
|
+
wrapper = factory();
|
|
62
|
+
const labels = wrapper.findAll('label').map((l) => l.text());
|
|
63
|
+
|
|
64
|
+
expect(labels).toStrictEqual(['Minute', 'Hour', 'Day of Month', 'Month', 'Day of Week']);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('initializes cron values and emits initial events', () => {
|
|
68
|
+
wrapper = factory();
|
|
69
|
+
const vm = wrapper.vm;
|
|
70
|
+
|
|
71
|
+
expect(vm.cronValues).toStrictEqual({
|
|
72
|
+
minute: '0', hour: '0', dayOfMonth: '*', month: '*', dayOfWeek: '*'
|
|
73
|
+
});
|
|
74
|
+
expect(vm.isValid).toBe(true);
|
|
75
|
+
expect(vm.readableCron).toContain('12:00');
|
|
76
|
+
|
|
77
|
+
expect(getEmitted('update:cronExpression')[0][0]).toBe('0 0 * * *');
|
|
78
|
+
expect(typeof getEmitted('update:readableCron')[0][0]).toBe('string');
|
|
79
|
+
expect(getEmitted('update:isValid')[0][0]).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('emits correct events when cron value changes', async() => {
|
|
83
|
+
wrapper = factory();
|
|
84
|
+
const vm = wrapper.vm;
|
|
85
|
+
|
|
86
|
+
vm.handleInput('minute', '5');
|
|
87
|
+
await nextTick();
|
|
88
|
+
|
|
89
|
+
expect(getEmitted('update:cronExpression')[1][0]).toBe('5 0 * * *');
|
|
90
|
+
expect(getEmitted('update:readableCron')[1][0]).toBe(vm.readableCron);
|
|
91
|
+
expect(getEmitted('update:isValid')[1][0]).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('validates individual fields correctly', () => {
|
|
95
|
+
wrapper = factory();
|
|
96
|
+
const vm = wrapper.vm;
|
|
97
|
+
|
|
98
|
+
vm.handleInput('minute', '0');
|
|
99
|
+
expect(vm.errors.minute).toBe(false);
|
|
100
|
+
|
|
101
|
+
vm.handleInput('minute', '61');
|
|
102
|
+
expect(vm.errors.minute).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('handles invalid cron expressions gracefully', async() => {
|
|
106
|
+
wrapper = factory({ cronExpression: '61 * * * *' });
|
|
107
|
+
const vm = wrapper.vm;
|
|
108
|
+
|
|
109
|
+
await nextTick();
|
|
110
|
+
expect(vm.isValid).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('updates readableCron correctly for valid and invalid inputs', async() => {
|
|
114
|
+
wrapper = factory({ cronExpression: '0 12 * * *' });
|
|
115
|
+
const vm = wrapper.vm;
|
|
116
|
+
|
|
117
|
+
await nextTick();
|
|
118
|
+
expect(vm.readableCron).toContain('12:00');
|
|
119
|
+
|
|
120
|
+
vm.handleInput('hour', '25');
|
|
121
|
+
await nextTick();
|
|
122
|
+
expect(vm.isValid).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('shows tooltip and manages popper on focus/blur', async() => {
|
|
126
|
+
wrapper = factory();
|
|
127
|
+
const vm = wrapper.vm;
|
|
128
|
+
|
|
129
|
+
await vm.handleFocus('minute');
|
|
130
|
+
expect(vm.focusedField.minute).toBe(true);
|
|
131
|
+
expect(vm.tooltipRefs.minute).not.toBeNull();
|
|
132
|
+
expect(vm.popperInstances.minute).not.toBeNull();
|
|
133
|
+
|
|
134
|
+
vm.handleBlur('minute');
|
|
135
|
+
await nextTick();
|
|
136
|
+
expect(vm.focusedField.minute).toBe(false);
|
|
137
|
+
expect(vm.popperInstances.minute).toBeNull();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('displays error tooltip for invalid input', async() => {
|
|
141
|
+
wrapper = factory();
|
|
142
|
+
const vm = wrapper.vm;
|
|
143
|
+
|
|
144
|
+
vm.handleInput('minute', '61');
|
|
145
|
+
await nextTick();
|
|
146
|
+
|
|
147
|
+
const inputWrapper = wrapper.findComponent({ name: 'LabeledInput' });
|
|
148
|
+
|
|
149
|
+
expect(inputWrapper.props('tooltip')).toBe('Invalid value');
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/* eslint-disable jest/no-hooks */
|
|
2
|
+
import { mount, VueWrapper } from '@vue/test-utils';
|
|
3
|
+
import { createStore } from 'vuex';
|
|
4
|
+
import CronExpressionEditorModal from '@shell/components/Cron/CronExpressionEditorModal.vue';
|
|
5
|
+
|
|
6
|
+
interface CronExpressionEditorModalVm extends InstanceType<typeof CronExpressionEditorModal> {
|
|
7
|
+
localCron: string;
|
|
8
|
+
confirmCron?: () => void;
|
|
9
|
+
closeModal?: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const store = createStore({});
|
|
13
|
+
|
|
14
|
+
describe('cronExpressionEditorModal', () => {
|
|
15
|
+
let modalsDiv: HTMLElement;
|
|
16
|
+
let wrapper: VueWrapper<CronExpressionEditorModalVm>;
|
|
17
|
+
|
|
18
|
+
const factory = (props: Partial<CronExpressionEditorModalVm> = {}) => mount(CronExpressionEditorModal, {
|
|
19
|
+
global: {
|
|
20
|
+
plugins: [store],
|
|
21
|
+
stubs: {
|
|
22
|
+
AppModal: true,
|
|
23
|
+
CronExpressionEditor: true,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
props: {
|
|
27
|
+
cronExpression: '0 0 * * *',
|
|
28
|
+
show: true,
|
|
29
|
+
...props,
|
|
30
|
+
},
|
|
31
|
+
}) as VueWrapper<CronExpressionEditorModalVm>;
|
|
32
|
+
|
|
33
|
+
const getEmitted = (event: string) => wrapper.emitted(event) as unknown[][] || [];
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
modalsDiv = document.createElement('div');
|
|
37
|
+
modalsDiv.id = 'modals';
|
|
38
|
+
document.body.appendChild(modalsDiv);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
wrapper?.unmount();
|
|
43
|
+
modalsDiv.remove();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('renders modal with correct initial props', () => {
|
|
47
|
+
wrapper = factory();
|
|
48
|
+
expect(wrapper.props('cronExpression')).toBe('0 0 * * *');
|
|
49
|
+
expect(wrapper.props('show')).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('updates localCron when cronExpression prop changes', async() => {
|
|
53
|
+
wrapper = factory();
|
|
54
|
+
await wrapper.setProps({ cronExpression: '*/5 * * * *' });
|
|
55
|
+
expect(wrapper.vm.localCron).toBe('*/5 * * * *');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('emits update:cronExpression and update:show on confirm', async() => {
|
|
59
|
+
wrapper = factory();
|
|
60
|
+
await wrapper.vm.confirmCron?.();
|
|
61
|
+
|
|
62
|
+
const cronEmits = getEmitted('update:cronExpression');
|
|
63
|
+
const showEmits = getEmitted('update:show');
|
|
64
|
+
|
|
65
|
+
expect(cronEmits).toHaveLength(1);
|
|
66
|
+
expect(cronEmits[0][0]).toBe('0 0 * * *');
|
|
67
|
+
|
|
68
|
+
expect(showEmits).toHaveLength(1);
|
|
69
|
+
expect(showEmits[0][0]).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('emits update:show on cancel', async() => {
|
|
73
|
+
wrapper = factory();
|
|
74
|
+
await wrapper.vm.closeModal?.();
|
|
75
|
+
|
|
76
|
+
const showEmits = getEmitted('update:show');
|
|
77
|
+
|
|
78
|
+
expect(showEmits).toHaveLength(1);
|
|
79
|
+
expect(showEmits[0][0]).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
});
|