@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,129 @@
|
|
|
1
|
+
import { setProduct } from '@shell/utils/product';
|
|
2
|
+
import { getProductFromRoute } from '@shell/utils/router';
|
|
3
|
+
import { NAME as EXPLORER } from '@shell/config/product/explorer';
|
|
4
|
+
import { RouteLocation } from 'vue-router';
|
|
5
|
+
|
|
6
|
+
// Mock dependencies
|
|
7
|
+
jest.mock('@shell/utils/router', () => ({ getProductFromRoute: jest.fn() }));
|
|
8
|
+
|
|
9
|
+
const mockGetProductFromRoute = getProductFromRoute as jest.Mock;
|
|
10
|
+
|
|
11
|
+
describe('utils/product', () => {
|
|
12
|
+
describe('setProduct', () => {
|
|
13
|
+
let mockStore: any;
|
|
14
|
+
let mockTo: RouteLocation;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
jest.clearAllMocks();
|
|
18
|
+
|
|
19
|
+
// A stateful mock store to better simulate Vuex behavior
|
|
20
|
+
mockStore = {
|
|
21
|
+
productId: 'some-old-product',
|
|
22
|
+
products: {
|
|
23
|
+
'some-old-product': { name: 'some-old-product', inStore: 'store_a' },
|
|
24
|
+
'new-product-same-store': { name: 'new-product-same-store', inStore: 'store_a' },
|
|
25
|
+
'new-product-diff-store': { name: 'new-product-diff-store', inStore: 'store_b' },
|
|
26
|
+
[EXPLORER]: { name: EXPLORER, inStore: 'explorer_store' },
|
|
27
|
+
},
|
|
28
|
+
getters: {
|
|
29
|
+
'type-map/isProductRegistered': jest.fn().mockReturnValue(true),
|
|
30
|
+
'i18n/t': jest.fn((key, args) => `${ key } ${ JSON.stringify(args) || '' }`),
|
|
31
|
+
get currentProduct() {
|
|
32
|
+
return mockStore.products[mockStore.productId];
|
|
33
|
+
},
|
|
34
|
+
get productId() {
|
|
35
|
+
return mockStore.productId;
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
commit: jest.fn((type, payload) => {
|
|
39
|
+
if (type === 'setProduct') {
|
|
40
|
+
mockStore.productId = payload;
|
|
41
|
+
}
|
|
42
|
+
}),
|
|
43
|
+
dispatch: jest.fn(),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
mockTo = { matched: [{ path: '/c/:cluster/some-product' }] } as unknown as RouteLocation;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should dispatch loadingError if product is not found (wildcard route)', () => {
|
|
50
|
+
const product = 'invalid-product';
|
|
51
|
+
|
|
52
|
+
mockGetProductFromRoute.mockReturnValue(product);
|
|
53
|
+
mockTo.matched = [{ path: '/c/:cluster/:product' }] as any;
|
|
54
|
+
|
|
55
|
+
setProduct(mockStore, mockTo);
|
|
56
|
+
|
|
57
|
+
expect(mockStore.dispatch).toHaveBeenCalledWith('loadingError', expect.any(Error));
|
|
58
|
+
expect(mockStore.getters['i18n/t']).toHaveBeenCalledWith('nav.failWhale.productNotFound', { productNotFound: product }, true);
|
|
59
|
+
expect(mockStore.commit).not.toHaveBeenCalled();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should dispatch loadingError if product exists but route has no matches', () => {
|
|
63
|
+
const product = 'invalid-product';
|
|
64
|
+
|
|
65
|
+
mockGetProductFromRoute.mockReturnValue(product);
|
|
66
|
+
mockTo.matched = [] as any; // No matched routes
|
|
67
|
+
|
|
68
|
+
setProduct(mockStore, mockTo);
|
|
69
|
+
|
|
70
|
+
expect(mockStore.dispatch).toHaveBeenCalledWith('loadingError', expect.any(Error));
|
|
71
|
+
expect(mockStore.getters['i18n/t']).toHaveBeenCalledWith('nav.failWhale.productNotFound', { productNotFound: product }, true);
|
|
72
|
+
expect(mockStore.commit).not.toHaveBeenCalled();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should dispatch loadingError if product is not registered', () => {
|
|
76
|
+
const product = 'unregistered-product';
|
|
77
|
+
|
|
78
|
+
mockGetProductFromRoute.mockReturnValue(product);
|
|
79
|
+
mockStore.getters['type-map/isProductRegistered'].mockReturnValue(false);
|
|
80
|
+
|
|
81
|
+
setProduct(mockStore, mockTo);
|
|
82
|
+
|
|
83
|
+
expect(mockStore.dispatch).toHaveBeenCalledWith('loadingError', expect.any(Error));
|
|
84
|
+
expect(mockStore.getters['i18n/t']).toHaveBeenCalledWith('nav.failWhale.productNotFound', { productNotFound: product }, true);
|
|
85
|
+
expect(mockStore.commit).not.toHaveBeenCalled();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should default to EXPLORER product and reset catalog if store changes', () => {
|
|
89
|
+
mockGetProductFromRoute.mockReturnValue(null);
|
|
90
|
+
|
|
91
|
+
setProduct(mockStore, mockTo);
|
|
92
|
+
|
|
93
|
+
expect(mockStore.commit).toHaveBeenCalledWith('setProduct', EXPLORER);
|
|
94
|
+
expect(mockStore.commit).toHaveBeenCalledWith('catalog/reset');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should not call any commits if the product has not changed', () => {
|
|
98
|
+
const product = 'some-old-product';
|
|
99
|
+
|
|
100
|
+
mockGetProductFromRoute.mockReturnValue(product);
|
|
101
|
+
|
|
102
|
+
setProduct(mockStore, mockTo);
|
|
103
|
+
|
|
104
|
+
expect(mockStore.commit).not.toHaveBeenCalled();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should change product but not reset catalog if inStore is the same', () => {
|
|
108
|
+
const newProduct = 'new-product-same-store';
|
|
109
|
+
|
|
110
|
+
mockGetProductFromRoute.mockReturnValue(newProduct);
|
|
111
|
+
|
|
112
|
+
setProduct(mockStore, mockTo);
|
|
113
|
+
|
|
114
|
+
expect(mockStore.commit).toHaveBeenCalledWith('setProduct', newProduct);
|
|
115
|
+
expect(mockStore.commit).not.toHaveBeenCalledWith('catalog/reset');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should change product and reset catalog if inStore is different', () => {
|
|
119
|
+
const newProduct = 'new-product-diff-store';
|
|
120
|
+
|
|
121
|
+
mockGetProductFromRoute.mockReturnValue(newProduct);
|
|
122
|
+
|
|
123
|
+
setProduct(mockStore, mockTo);
|
|
124
|
+
|
|
125
|
+
expect(mockStore.commit).toHaveBeenCalledWith('setProduct', newProduct);
|
|
126
|
+
expect(mockStore.commit).toHaveBeenCalledWith('catalog/reset');
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { validateResource } from '@shell/utils/resource';
|
|
2
|
+
import { canViewResource } from '@shell/utils/auth';
|
|
3
|
+
import { getResourceFromRoute } from '@shell/utils/router';
|
|
4
|
+
import { RouteLocation } from 'vue-router';
|
|
5
|
+
|
|
6
|
+
// Mock dependencies from other modules
|
|
7
|
+
jest.mock('@shell/utils/auth', () => ({ canViewResource: jest.fn() }));
|
|
8
|
+
|
|
9
|
+
jest.mock('@shell/utils/router', () => ({ getResourceFromRoute: jest.fn() }));
|
|
10
|
+
|
|
11
|
+
// Typed mocks for better intellisense and type-checking
|
|
12
|
+
const mockCanViewResource = canViewResource as jest.Mock;
|
|
13
|
+
const mockGetResourceFromRoute = getResourceFromRoute as jest.Mock;
|
|
14
|
+
|
|
15
|
+
describe('validateResource', () => {
|
|
16
|
+
let mockStore: any;
|
|
17
|
+
let mockTo: RouteLocation;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
// Reset mocks before each test to ensure test isolation
|
|
21
|
+
jest.clearAllMocks();
|
|
22
|
+
|
|
23
|
+
// A basic mock for the Vuex store
|
|
24
|
+
mockStore = {
|
|
25
|
+
getters: {
|
|
26
|
+
currentProduct: { name: 'explorer' },
|
|
27
|
+
'i18n/t': jest.fn((key, args) => `i18n: ${ key } ${ JSON.stringify(args) || '' }`),
|
|
28
|
+
},
|
|
29
|
+
dispatch: jest.fn(),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// A mock for the route location object
|
|
33
|
+
mockTo = {
|
|
34
|
+
name: 'c-cluster-product-resource-id',
|
|
35
|
+
params: {
|
|
36
|
+
cluster: 'local',
|
|
37
|
+
product: 'explorer',
|
|
38
|
+
resource: 'pod',
|
|
39
|
+
id: 'my-pod'
|
|
40
|
+
}
|
|
41
|
+
} as unknown as RouteLocation;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should return false if no product is set in the store', () => {
|
|
45
|
+
mockStore.getters.currentProduct = null;
|
|
46
|
+
mockGetResourceFromRoute.mockReturnValue('pod');
|
|
47
|
+
|
|
48
|
+
const result = validateResource(mockStore, mockTo);
|
|
49
|
+
|
|
50
|
+
expect(result).toBe(false);
|
|
51
|
+
expect(mockStore.dispatch).not.toHaveBeenCalled();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should return false if no resource is found in the route', () => {
|
|
55
|
+
mockGetResourceFromRoute.mockReturnValue(null);
|
|
56
|
+
|
|
57
|
+
const result = validateResource(mockStore, mockTo);
|
|
58
|
+
|
|
59
|
+
expect(result).toBe(false);
|
|
60
|
+
expect(mockStore.dispatch).not.toHaveBeenCalled();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should return false if the user is authorized to view the resource', () => {
|
|
64
|
+
mockGetResourceFromRoute.mockReturnValue('pod');
|
|
65
|
+
mockCanViewResource.mockReturnValue(true);
|
|
66
|
+
|
|
67
|
+
const result = validateResource(mockStore, mockTo);
|
|
68
|
+
|
|
69
|
+
expect(result).toBe(false);
|
|
70
|
+
expect(mockStore.dispatch).not.toHaveBeenCalled();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should throw an error and dispatch loadingError if user cannot view the resource', () => {
|
|
74
|
+
const resource = 'pod';
|
|
75
|
+
|
|
76
|
+
mockGetResourceFromRoute.mockReturnValue(resource);
|
|
77
|
+
mockCanViewResource.mockReturnValue(false);
|
|
78
|
+
|
|
79
|
+
const errorMessage = `i18n: nav.failWhale.resourceNotFound {"resource":"${ resource }"}`;
|
|
80
|
+
|
|
81
|
+
mockStore.getters['i18n/t'].mockReturnValue(errorMessage);
|
|
82
|
+
|
|
83
|
+
expect(() => validateResource(mockStore, mockTo)).toThrow(errorMessage);
|
|
84
|
+
expect(mockStore.dispatch).toHaveBeenCalledWith('loadingError', expect.any(Error));
|
|
85
|
+
expect(mockStore.getters['i18n/t']).toHaveBeenCalledWith('nav.failWhale.resourceNotFound', { resource }, true);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -19,8 +19,8 @@ export const fetchAlertManagerConfigSpecs = async($store) => {
|
|
|
19
19
|
await schema.fetchResourceFields();
|
|
20
20
|
|
|
21
21
|
return {
|
|
22
|
-
receiverSchema: schema.schemaDefinitions?.[`${ schema.schemaDefinition.
|
|
23
|
-
routeSchema: schema.schemaDefinitions?.[`${ schema.schemaDefinition.
|
|
22
|
+
receiverSchema: schema.schemaDefinitions?.[`${ schema.schemaDefinition.type }.spec.receivers`],
|
|
23
|
+
routeSchema: schema.schemaDefinitions?.[`${ schema.schemaDefinition.type }.spec.route`],
|
|
24
24
|
};
|
|
25
25
|
};
|
|
26
26
|
|
package/utils/auth.js
CHANGED
|
@@ -5,8 +5,6 @@ import {
|
|
|
5
5
|
} from '@shell/config/query-params';
|
|
6
6
|
import { MANAGEMENT, NORMAN } from '@shell/config/types';
|
|
7
7
|
import { allHash } from '@shell/utils/promise';
|
|
8
|
-
import { getProductFromRoute, getResourceFromRoute } from '@shell/utils/router';
|
|
9
|
-
import { NAME as EXPLORER } from '@shell/config/product/explorer';
|
|
10
8
|
import { findBy } from '@shell/utils/array';
|
|
11
9
|
import { onExtensionsReady } from '@shell/utils/uiplugins';
|
|
12
10
|
|
|
@@ -201,6 +199,9 @@ export const checkPermissions = (types, getters) => {
|
|
|
201
199
|
return allHash(hash);
|
|
202
200
|
};
|
|
203
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Checks if the current user has access to the specified resource type
|
|
204
|
+
*/
|
|
204
205
|
export const canViewResource = (store, resource) => {
|
|
205
206
|
// Note - don't use the current products store... because products can override stores for resources with `typeStoreMap`
|
|
206
207
|
const inStore = store.getters['currentStore'](resource);
|
|
@@ -218,80 +219,6 @@ export const canViewResource = (store, resource) => {
|
|
|
218
219
|
return !!validResource;
|
|
219
220
|
};
|
|
220
221
|
|
|
221
|
-
// ************************************************************
|
|
222
|
-
//
|
|
223
|
-
// BELOW ARE METHODS THAT ARE A PART OF THE AUTHENTICATED MIDDLEWARE REMOVAL. THIS IS A TEMPORARY HOME FOR THESE UTILS AND SHOULD BE REWRITTEN, MOVED OR DELETED.
|
|
224
|
-
//
|
|
225
|
-
// TODO: Remove and refactor everything below for more clarity and better organization. https://github.com/rancher/dashboard/issues/11111
|
|
226
|
-
//
|
|
227
|
-
// ************************************************************
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Attempt to set the product in our datastore if the route matches a known product. Otherwise show an error page instead.
|
|
231
|
-
*/
|
|
232
|
-
export function setProduct(store, to) {
|
|
233
|
-
let product = getProductFromRoute(to);
|
|
234
|
-
|
|
235
|
-
// since all products are hardcoded as routes (ex: c-local-explorer), if we match the wildcard route it means that the product does not exist
|
|
236
|
-
if ((product && (!to.matched.length || (to.matched.length && to.matched[0].path === '/c/:cluster/:product'))) ||
|
|
237
|
-
// if the product grabbed from the route is not registered, then we don't have it!
|
|
238
|
-
(product && !store.getters['type-map/isProductRegistered'](product))) {
|
|
239
|
-
const error = new Error(store.getters['i18n/t']('nav.failWhale.productNotFound', { productNotFound: product }, true));
|
|
240
|
-
|
|
241
|
-
return store.dispatch('loadingError', error);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
if ( !product ) {
|
|
245
|
-
product = EXPLORER;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
const oldProduct = store.getters['productId'];
|
|
249
|
-
const oldStore = store.getters['currentProduct']?.inStore;
|
|
250
|
-
|
|
251
|
-
if ( product !== oldProduct ) {
|
|
252
|
-
store.commit('setProduct', product);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const neuStore = store.getters['currentProduct']?.inStore;
|
|
256
|
-
|
|
257
|
-
if ( neuStore !== oldStore ) {
|
|
258
|
-
// If the product store changes, clear the catalog.
|
|
259
|
-
// There might be management catalog items in it vs cluster.
|
|
260
|
-
store.commit('catalog/reset');
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* Check that the resource is valid, if not redirect to fail whale
|
|
266
|
-
*
|
|
267
|
-
* This requires that
|
|
268
|
-
* - product is set
|
|
269
|
-
* - product's store is set and setup (so we can check schema's within it)
|
|
270
|
-
* - product's store has the schemaFor getter (extension stores might not have it)
|
|
271
|
-
* - there's a resource associated with route (meta or param)
|
|
272
|
-
*/
|
|
273
|
-
export function validateResource(store, to) {
|
|
274
|
-
const product = store.getters['currentProduct'];
|
|
275
|
-
const resource = getResourceFromRoute(to);
|
|
276
|
-
|
|
277
|
-
// In order to check a resource is valid we need these
|
|
278
|
-
if (!product || !resource) {
|
|
279
|
-
return false;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (canViewResource(store, resource)) {
|
|
283
|
-
return false;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// Unknown resource, redirect to fail whale
|
|
287
|
-
|
|
288
|
-
const error = new Error(store.getters['i18n/t']('nav.failWhale.resourceNotFound', { resource }, true));
|
|
289
|
-
|
|
290
|
-
store.dispatch('loadingError', error);
|
|
291
|
-
|
|
292
|
-
throw error;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
222
|
/**
|
|
296
223
|
* Attempt to load the current user's principal
|
|
297
224
|
*/
|
|
@@ -336,7 +263,7 @@ export async function tryInitialSetup(store, password = 'admin') {
|
|
|
336
263
|
*/
|
|
337
264
|
export async function isLoggedIn(store, userData) {
|
|
338
265
|
store.commit('auth/hasAuth', true);
|
|
339
|
-
store.
|
|
266
|
+
store.dispatch('auth/loggedInAs', userData.id);
|
|
340
267
|
|
|
341
268
|
// Init the notification center now that we know who the user is
|
|
342
269
|
await store.dispatch('notifications/init', userData);
|
package/utils/product.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { getProductFromRoute } from '@shell/utils/router';
|
|
2
|
+
import { NAME as EXPLORER } from '@shell/config/product/explorer';
|
|
3
|
+
import { RouteLocation } from 'vue-router';
|
|
4
|
+
import { Store } from 'vuex';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Attempt to set the product in our datastore if the route matches a known product. Otherwise show an error page instead.
|
|
8
|
+
*/
|
|
9
|
+
export function setProduct(store: Store<any>, to: RouteLocation) {
|
|
10
|
+
let product = getProductFromRoute(to);
|
|
11
|
+
|
|
12
|
+
// since all products are hardcoded as routes (ex: c-local-explorer), if we match the wildcard route it means that the product does not exist
|
|
13
|
+
if ((product && (!to.matched.length || (to.matched.length && to.matched[0].path === '/c/:cluster/:product'))) ||
|
|
14
|
+
// if the product grabbed from the route is not registered, then we don't have it!
|
|
15
|
+
(product && !store.getters['type-map/isProductRegistered'](product))) {
|
|
16
|
+
const error = new Error(store.getters['i18n/t']('nav.failWhale.productNotFound', { productNotFound: product }, true));
|
|
17
|
+
|
|
18
|
+
return store.dispatch('loadingError', error);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if ( !product ) {
|
|
22
|
+
product = EXPLORER;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const oldProduct = store.getters['productId'];
|
|
26
|
+
const oldStore = store.getters['currentProduct']?.inStore;
|
|
27
|
+
|
|
28
|
+
if ( product !== oldProduct ) {
|
|
29
|
+
store.commit('setProduct', product);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const neuStore = store.getters['currentProduct']?.inStore;
|
|
33
|
+
|
|
34
|
+
if ( neuStore !== oldStore ) {
|
|
35
|
+
// If the product store changes, clear the catalog.
|
|
36
|
+
// There might be management catalog items in it vs cluster.
|
|
37
|
+
store.commit('catalog/reset');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { canViewResource } from '@shell/utils/auth';
|
|
2
|
+
import { getResourceFromRoute } from '@shell/utils/router';
|
|
3
|
+
import { RouteLocation } from 'vue-router';
|
|
4
|
+
import { Store } from 'vuex';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Check that the resource is valid, if not redirect to fail whale
|
|
8
|
+
*
|
|
9
|
+
* This requires that
|
|
10
|
+
* - product is set
|
|
11
|
+
* - product's store is set and setup (so we can check schema's within it)
|
|
12
|
+
* - product's store has the schemaFor getter (extension stores might not have it)
|
|
13
|
+
* - there's a resource associated with route (meta or param)
|
|
14
|
+
*/
|
|
15
|
+
export function validateResource(store: Store<any>, to: RouteLocation) {
|
|
16
|
+
const product = store.getters['currentProduct'];
|
|
17
|
+
const resource = getResourceFromRoute(to);
|
|
18
|
+
|
|
19
|
+
// In order to check a resource is valid we need these
|
|
20
|
+
if (!product || !resource) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (canViewResource(store, resource)) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Unknown resource, redirect to fail whale
|
|
29
|
+
|
|
30
|
+
const error = new Error(store.getters['i18n/t']('nav.failWhale.resourceNotFound', { resource }, true));
|
|
31
|
+
|
|
32
|
+
store.dispatch('loadingError', error);
|
|
33
|
+
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
package/utils/select.js
CHANGED
|
@@ -1,27 +1,3 @@
|
|
|
1
|
-
export function onClickOption(option, e) {
|
|
2
|
-
if (!this.$attrs.multiple) {
|
|
3
|
-
return;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
const getValue = (opt) => (this.optionKey ? this.get(opt, this.optionKey) : this.getOptionLabel(opt));
|
|
7
|
-
const optionValue = getValue(option);
|
|
8
|
-
const value = this.value || [];
|
|
9
|
-
const optionIndex = value.findIndex((option) => getValue(option) === optionValue);
|
|
10
|
-
|
|
11
|
-
if (optionIndex < 0) {
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
this.value.splice(optionIndex, 1);
|
|
16
|
-
|
|
17
|
-
this.$emit('update:value', this.value);
|
|
18
|
-
e.preventDefault();
|
|
19
|
-
e.stopPropagation();
|
|
20
|
-
|
|
21
|
-
if (this.closeOnSelect) {
|
|
22
|
-
this.$refs['select-input'].closeSearchOptions();
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
1
|
|
|
26
2
|
// This is a simpler positionner for the dropdown for a select control
|
|
27
3
|
// We used to use popper for these, but it does not suppotr fractional pixel placements which
|
|
@@ -114,6 +114,8 @@ describe('formRules', () => {
|
|
|
114
114
|
['git@github.com:rancher/dashboard/', undefined],
|
|
115
115
|
['git@github.com:rancher/%20dashboard/', undefined],
|
|
116
116
|
['git@github.com:rancher/dashboard/%20', undefined],
|
|
117
|
+
['git@git.apps.local:fleet/fleet-local.git', undefined],
|
|
118
|
+
['git@git.apps.local:33333/fleet/fleet-local.git', undefined],
|
|
117
119
|
|
|
118
120
|
// Not valid HTTP(s)
|
|
119
121
|
['https://github.com/rancher/ dashboard.git', message],
|
|
@@ -140,6 +142,7 @@ describe('formRules', () => {
|
|
|
140
142
|
['git@githubcomrancher/dashboard', message],
|
|
141
143
|
['%20git@github.comrancher/dashboard', message],
|
|
142
144
|
['git@git%20hub.comrancher/dashboard', message],
|
|
145
|
+
['git@git.apps.local:/fleet/fleet-local.git', message],
|
|
143
146
|
['git@.git', message],
|
|
144
147
|
['git@', message],
|
|
145
148
|
|
|
@@ -191,6 +191,7 @@ export default function(
|
|
|
191
191
|
protocol,
|
|
192
192
|
authority,
|
|
193
193
|
host,
|
|
194
|
+
port,
|
|
194
195
|
path
|
|
195
196
|
} = parse(url);
|
|
196
197
|
|
|
@@ -205,7 +206,7 @@ export default function(
|
|
|
205
206
|
}
|
|
206
207
|
|
|
207
208
|
// Test ssh, authority must be valid (SSH user + host)
|
|
208
|
-
if (!protocol && !authority.endsWith(':')) {
|
|
209
|
+
if (!protocol && !port && (!authority.endsWith(':') || path.startsWith('/'))) {
|
|
209
210
|
return message;
|
|
210
211
|
}
|
|
211
212
|
|
package/vue.config.js
CHANGED
|
@@ -546,7 +546,7 @@ module.exports = function(dir, appConfig = {}) {
|
|
|
546
546
|
config.plugins.push(getVirtualModulesAutoImport(dir));
|
|
547
547
|
config.plugins.push(getPackageImport(dir));
|
|
548
548
|
config.plugins.push(createEnvVariablesPlugin(routerBasePath, rancherEnv));
|
|
549
|
-
config.plugins.push(new NodePolyfillPlugin()); // required from Webpack 5 to polyfill node modules
|
|
549
|
+
config.plugins.push(new NodePolyfillPlugin({ additionalAliases: ['process'] })); // required from Webpack 5 to polyfill node modules
|
|
550
550
|
|
|
551
551
|
// The static assets need to be in the built assets directory in order to get served (primarily the favicon)
|
|
552
552
|
config.plugins.push(new CopyWebpackPlugin({ patterns: [{ from: path.join(SHELL_ABS, 'static'), to: '.' }] }));
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
export interface RectangleProps {
|
|
3
|
-
outline?: boolean;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
const props = withDefaults(
|
|
7
|
-
defineProps<RectangleProps>(),
|
|
8
|
-
{ outline: false }
|
|
9
|
-
);
|
|
10
|
-
|
|
11
|
-
</script>
|
|
12
|
-
|
|
13
|
-
<template>
|
|
14
|
-
<div
|
|
15
|
-
class="rectangle"
|
|
16
|
-
:class="{outline: props.outline}"
|
|
17
|
-
>
|
|
18
|
-
<slot />
|
|
19
|
-
</div>
|
|
20
|
-
</template>
|
|
21
|
-
|
|
22
|
-
<style lang="scss" scoped>
|
|
23
|
-
.rectangle {
|
|
24
|
-
border: 1px solid var(--tag-bg);
|
|
25
|
-
border-radius: 4px;
|
|
26
|
-
padding: 0 8px;
|
|
27
|
-
height: 23px;
|
|
28
|
-
line-height: 23px;
|
|
29
|
-
|
|
30
|
-
&:not(.outline) {
|
|
31
|
-
background-color: var(--tag-bg);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
</style>
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { mount } from '@vue/test-utils';
|
|
2
|
-
import Rectangle from '@shell/components/Resource/Detail/Metadata/Rectangle.vue';
|
|
3
|
-
|
|
4
|
-
describe('component: Rectangle', () => {
|
|
5
|
-
it('should render container with class title and missing outline when passed outline:false', async() => {
|
|
6
|
-
const wrapper = mount(Rectangle, { props: { outline: false } });
|
|
7
|
-
|
|
8
|
-
expect(wrapper.find('.rectangle').exists()).toBeTruthy();
|
|
9
|
-
expect(wrapper.find('.rectangle.outline').exists()).toBeFalsy();
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it('should render outline class when passed outline:true', async() => {
|
|
13
|
-
const wrapper = mount(Rectangle, { props: { outline: true } });
|
|
14
|
-
|
|
15
|
-
expect(wrapper.find('.rectangle.outline').exists()).toBeTruthy();
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('should render default slot', async() => {
|
|
19
|
-
const content = 'CONTENT';
|
|
20
|
-
const wrapper = mount(Rectangle, { slots: { default: content } });
|
|
21
|
-
|
|
22
|
-
expect(wrapper.find('.rectangle').element.innerHTML).toStrictEqual(content);
|
|
23
|
-
});
|
|
24
|
-
});
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { mount, RouterLinkStub } from '@vue/test-utils';
|
|
2
|
-
import { _VIEW } from '@shell/config/query-params';
|
|
3
|
-
import Legacy from '@shell/components/ResourceDetail/Masthead/legacy.vue';
|
|
4
|
-
import { createStore } from 'vuex';
|
|
5
|
-
|
|
6
|
-
const mockedStore = () => {
|
|
7
|
-
return {
|
|
8
|
-
getters: {
|
|
9
|
-
currentStore: () => 'current_store',
|
|
10
|
-
currentProduct: { inStore: 'cluster' },
|
|
11
|
-
isExplorer: false,
|
|
12
|
-
currentCluster: {},
|
|
13
|
-
'type-map/labelFor': jest.fn(),
|
|
14
|
-
'type-map/optionsFor': jest.fn(),
|
|
15
|
-
'current_store/schemaFor': jest.fn(),
|
|
16
|
-
},
|
|
17
|
-
};
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const requiredSetup = () => {
|
|
21
|
-
const store = createStore({ getters: { 'management/byId': () => jest.fn() } });
|
|
22
|
-
|
|
23
|
-
return {
|
|
24
|
-
stubs: {
|
|
25
|
-
'router-link': RouterLinkStub,
|
|
26
|
-
LiveDate: true
|
|
27
|
-
},
|
|
28
|
-
provide: { store },
|
|
29
|
-
mocks: { $store: mockedStore() }
|
|
30
|
-
};
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
describe('component: Masthead/legacy', () => {
|
|
34
|
-
it.each([
|
|
35
|
-
['hidden', '', false, { displayName: 'admin', location: { id: 'resource-id' } }, false, false],
|
|
36
|
-
['plain-text', 'admin', true, { displayName: 'admin', location: null }, false, true],
|
|
37
|
-
['link', 'foo', true, { displayName: 'foo', location: { id: 'resource-id' } }, true, false],
|
|
38
|
-
])('"Created By" should be %p, with text: %p', (
|
|
39
|
-
_,
|
|
40
|
-
text,
|
|
41
|
-
showCreatedBy,
|
|
42
|
-
createdBy,
|
|
43
|
-
showLink,
|
|
44
|
-
showPlainText,
|
|
45
|
-
) => {
|
|
46
|
-
const wrapper = mount(Legacy, {
|
|
47
|
-
props: {
|
|
48
|
-
mode: _VIEW,
|
|
49
|
-
value: {
|
|
50
|
-
showCreatedBy,
|
|
51
|
-
createdBy,
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
global: { ...requiredSetup() }
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
const container = wrapper.find('[data-testid="masthead-subheader-createdBy"]');
|
|
58
|
-
const link = wrapper.find('[data-testid="masthead-subheader-createdBy-link"]');
|
|
59
|
-
const plainText = wrapper.find('[data-testid="masthead-subheader-createdBy_plain-text"]');
|
|
60
|
-
|
|
61
|
-
expect(link.exists()).toBe(showLink);
|
|
62
|
-
expect(plainText.exists()).toBe(showPlainText);
|
|
63
|
-
expect(showLink || showPlainText ? container.element.textContent : '').toContain(text);
|
|
64
|
-
});
|
|
65
|
-
});
|
|
File without changes
|