@rancher/shell 3.0.5-rc.8 → 3.0.5-rc.9
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 +136 -14
- 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/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/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/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/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/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +2 -1
- package/edit/__tests__/resources.cattle.io.restore.test.ts +106 -0
- 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/resources.cattle.io.restore.vue +5 -8
- 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__/chart.test.ts +139 -1
- 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.project.js +12 -0
- package/models/namespace.js +30 -0
- package/models/workload.js +3 -0
- package/package.json +10 -10
- 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/dashboard-store/resource-class.js +49 -0
- 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/store/__tests__/catalog.test.ts +63 -0
- package/store/catalog.js +2 -2
- package/store/type-map.js +3 -15
- package/types/extension-manager.ts +26 -0
- package/types/shell/index.d.ts +121 -16
- 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 +3 -76
- 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/components/{ForceDirectedTreeChart.vue → ForceDirectedTreeChart/index.vue} +0 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { useI18n } from '@shell/composables/useI18n';
|
|
3
|
+
import { copyTextToClipboard } from '@shell/utils/clipboard';
|
|
4
|
+
import { ref } from 'vue';
|
|
5
|
+
import { useStore } from 'vuex';
|
|
6
|
+
|
|
7
|
+
export interface Props {
|
|
8
|
+
value: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const props = defineProps<Props>();
|
|
12
|
+
const store = useStore();
|
|
13
|
+
const i18n = useI18n(store);
|
|
14
|
+
const copied = ref(false);
|
|
15
|
+
const timeout = ref<null | ReturnType<typeof setTimeout>>(null);
|
|
16
|
+
|
|
17
|
+
const onClick = (ev: MouseEvent) => {
|
|
18
|
+
ev.stopPropagation();
|
|
19
|
+
|
|
20
|
+
copyTextToClipboard(props.value);
|
|
21
|
+
copied.value = true;
|
|
22
|
+
|
|
23
|
+
if (timeout.value) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
timeout.value = setTimeout(() => {
|
|
28
|
+
copied.value = false;
|
|
29
|
+
timeout.value = null;
|
|
30
|
+
}, 2000);
|
|
31
|
+
};
|
|
32
|
+
</script>
|
|
33
|
+
<template>
|
|
34
|
+
<button
|
|
35
|
+
class="copy-to-clipboard"
|
|
36
|
+
:class="{copied}"
|
|
37
|
+
:aria-label="i18n.t('component.resource.detail.copyToClipboard.ariaLabel.copy')"
|
|
38
|
+
@click="onClick"
|
|
39
|
+
>
|
|
40
|
+
<i class="icon icon-copy" />
|
|
41
|
+
</button>
|
|
42
|
+
</template>
|
|
43
|
+
|
|
44
|
+
<style lang="scss" scoped>
|
|
45
|
+
.copy-to-clipboard {
|
|
46
|
+
z-index: 2;
|
|
47
|
+
display: inline-flex;
|
|
48
|
+
$size: 36px;
|
|
49
|
+
width: $size;
|
|
50
|
+
height: $size;
|
|
51
|
+
font-size: 14px;
|
|
52
|
+
border-radius: 50%;
|
|
53
|
+
justify-content: center;
|
|
54
|
+
align-items: center;
|
|
55
|
+
|
|
56
|
+
border: 1px solid var(--primary);
|
|
57
|
+
color: var(--primary);
|
|
58
|
+
|
|
59
|
+
background-color: var(--body-bg);
|
|
60
|
+
|
|
61
|
+
&:hover {
|
|
62
|
+
color: var(--body-text);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
&.copied {
|
|
66
|
+
background-color: var(--success);
|
|
67
|
+
border-color: var(--success-border);
|
|
68
|
+
color: var(--success-text);
|
|
69
|
+
|
|
70
|
+
transition: all 0.25s;
|
|
71
|
+
transition-timing-function: ease;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
&:focus-visible {
|
|
75
|
+
@include focus-outline;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
</style>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useFetch } from '@shell/components/Resource/Detail/FetchLoader/composables';
|
|
2
|
+
import { nextTick } from 'vue';
|
|
3
|
+
|
|
4
|
+
describe('composables: useFetch', () => {
|
|
5
|
+
it('should be in a loading state initially and call the fetch function immediately', () => {
|
|
6
|
+
const mockFetch = jest.fn(() => new Promise(() => {})); // A promise that never resolves
|
|
7
|
+
|
|
8
|
+
const result = useFetch(mockFetch);
|
|
9
|
+
|
|
10
|
+
expect(result.value.loading).toBe(true);
|
|
11
|
+
expect(result.value.data).toBeUndefined();
|
|
12
|
+
expect(result.value.error).toBeUndefined();
|
|
13
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should successfully fetch data and update state', async() => {
|
|
17
|
+
const mockData = { id: 1, name: 'Test Data' };
|
|
18
|
+
const mockFetch = jest.fn(() => Promise.resolve(mockData));
|
|
19
|
+
|
|
20
|
+
const result = useFetch(mockFetch);
|
|
21
|
+
|
|
22
|
+
// Initial state check
|
|
23
|
+
expect(result.value.loading).toBe(true);
|
|
24
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
25
|
+
|
|
26
|
+
// Wait for the promise to resolve and Vue to react
|
|
27
|
+
await nextTick();
|
|
28
|
+
|
|
29
|
+
// Final state check
|
|
30
|
+
expect(result.value.loading).toBe(false);
|
|
31
|
+
expect(result.value.data).toStrictEqual(mockData);
|
|
32
|
+
expect(result.value.error).toBeUndefined();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should handle a fetch error and update state', async() => {
|
|
36
|
+
const mockError = new Error('Network Failed');
|
|
37
|
+
const mockFetch = jest.fn(() => Promise.reject(mockError));
|
|
38
|
+
|
|
39
|
+
const result = useFetch(mockFetch);
|
|
40
|
+
|
|
41
|
+
// Initial state check
|
|
42
|
+
expect(result.value.loading).toBe(true);
|
|
43
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
44
|
+
|
|
45
|
+
// Wait for the promise to reject and Vue to react
|
|
46
|
+
await nextTick();
|
|
47
|
+
|
|
48
|
+
// Final state check
|
|
49
|
+
expect(result.value.loading).toBe(false);
|
|
50
|
+
expect(result.value.data).toBeUndefined();
|
|
51
|
+
expect(result.value.error).toStrictEqual(mockError);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should correctly handle a fetch function that returns a non-promise value', async() => {
|
|
55
|
+
const mockData = 'just a string';
|
|
56
|
+
// The composable uses `await`, which will wrap non-promise values.
|
|
57
|
+
const mockFetch = jest.fn(() => mockData as any);
|
|
58
|
+
|
|
59
|
+
const result = useFetch(mockFetch);
|
|
60
|
+
|
|
61
|
+
expect(result.value.loading).toBe(true);
|
|
62
|
+
|
|
63
|
+
await nextTick();
|
|
64
|
+
|
|
65
|
+
expect(result.value.loading).toBe(false);
|
|
66
|
+
expect(result.value.data).toBe(mockData);
|
|
67
|
+
expect(result.value.error).toBeUndefined();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { computed, ref, toValue } from 'vue';
|
|
2
|
+
|
|
3
|
+
export const useFetch = <T>(fetch: () => Promise<T> ) => {
|
|
4
|
+
const loading = ref<boolean>(true);
|
|
5
|
+
const data = ref<T>();
|
|
6
|
+
const error = ref<any>();
|
|
7
|
+
|
|
8
|
+
const load = async() => {
|
|
9
|
+
try {
|
|
10
|
+
loading.value = true;
|
|
11
|
+
|
|
12
|
+
data.value = toValue(await fetch());
|
|
13
|
+
} catch (ex) {
|
|
14
|
+
error.value = ex;
|
|
15
|
+
} finally {
|
|
16
|
+
loading.value = false;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
load();
|
|
21
|
+
|
|
22
|
+
return computed(() => ({
|
|
23
|
+
loading: loading.value,
|
|
24
|
+
data: data.value,
|
|
25
|
+
error: error.value
|
|
26
|
+
}));
|
|
27
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mount } from '@vue/test-utils';
|
|
2
2
|
import Annotations from '@shell/components/Resource/Detail/Metadata/Annotations/index.vue';
|
|
3
3
|
import { createStore } from 'vuex';
|
|
4
|
+
jest.mock('@shell/utils/clipboard', () => ({ copyTextToClipboard: jest.fn() }));
|
|
4
5
|
|
|
5
6
|
describe('component: Metadata/Annotations', () => {
|
|
6
7
|
it('should render KeyValue with the appropriate props', async() => {
|
|
@@ -14,6 +15,5 @@ describe('component: Metadata/Annotations', () => {
|
|
|
14
15
|
|
|
15
16
|
expect(keyValue.props('propertyName')).toStrictEqual('component.resource.detail.metadata.annotations.title');
|
|
16
17
|
expect(keyValue.props('rows')).toStrictEqual(annotations);
|
|
17
|
-
expect(keyValue.props('outline')).toStrictEqual(true);
|
|
18
18
|
});
|
|
19
19
|
});
|
|
@@ -24,7 +24,7 @@ const i18n = useI18n(store);
|
|
|
24
24
|
<KeyValue
|
|
25
25
|
:propertyName="i18n.t('component.resource.detail.metadata.annotations.title')"
|
|
26
26
|
:rows="annotations"
|
|
27
|
-
|
|
27
|
+
type="active"
|
|
28
28
|
|
|
29
29
|
@show-configuration="(returnFocusSelector: string) => emit('show-configuration', returnFocusSelector)"
|
|
30
30
|
/>
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
-
useNamespace, useWorkspace, useLiveDate,
|
|
3
|
-
useResourceDetails
|
|
2
|
+
useNamespace, useWorkspace, useLiveDate, useProject, useResourceDetails
|
|
4
3
|
} from '@shell/components/Resource/Detail/Metadata/IdentifyingInformation/identifying-fields';
|
|
5
|
-
import { NAMESPACE, FLEET } from '@shell/config/types';
|
|
4
|
+
import { NAMESPACE, FLEET, MANAGEMENT } from '@shell/config/types';
|
|
6
5
|
import { NAME as FLEET_NAME } from '@shell/config/product/fleet';
|
|
7
6
|
|
|
8
7
|
const mockStore = {
|
|
@@ -33,29 +32,12 @@ describe('composables: IdentifyingFields', () => {
|
|
|
33
32
|
expect(result).toBeUndefined();
|
|
34
33
|
});
|
|
35
34
|
|
|
36
|
-
it('should return a valid namespace row
|
|
37
|
-
const resource = { namespace: 'NAMESPACE', namespaceLocation: 'NAMESPACE_LOCATION' };
|
|
38
|
-
const result = useNamespace(resource);
|
|
39
|
-
|
|
40
|
-
expect(result?.value.to).toStrictEqual(resource.namespaceLocation);
|
|
41
|
-
expect(result?.value.value).toStrictEqual(resource.namespace);
|
|
42
|
-
expect(result?.value.label).toStrictEqual('component.resource.detail.metadata.identifyingInformation.namespace');
|
|
43
|
-
expect(result?.value.valueDataTestid).toStrictEqual('masthead-subheader-namespace');
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('should return a valid namespace row with computed `to`', () => {
|
|
35
|
+
it('should return a valid namespace row', () => {
|
|
47
36
|
const resource = { namespace: 'NAMESPACE' };
|
|
48
37
|
const result = useNamespace(resource);
|
|
49
38
|
|
|
50
|
-
expect(result?.value.
|
|
51
|
-
|
|
52
|
-
params: {
|
|
53
|
-
product: 'PRODUCT_ID',
|
|
54
|
-
cluster: 'CLUSTER_ID',
|
|
55
|
-
resource: NAMESPACE,
|
|
56
|
-
id: resource.namespace
|
|
57
|
-
}
|
|
58
|
-
});
|
|
39
|
+
expect(result?.value.valueOverride?.props.type).toStrictEqual(NAMESPACE);
|
|
40
|
+
expect(result?.value.valueOverride?.props.id).toStrictEqual(resource.namespace);
|
|
59
41
|
expect(result?.value.value).toStrictEqual(resource.namespace);
|
|
60
42
|
expect(result?.value.label).toStrictEqual('component.resource.detail.metadata.identifyingInformation.namespace');
|
|
61
43
|
expect(result?.value.valueDataTestid).toStrictEqual('masthead-subheader-namespace');
|
|
@@ -122,41 +104,6 @@ describe('composables: IdentifyingFields', () => {
|
|
|
122
104
|
});
|
|
123
105
|
});
|
|
124
106
|
|
|
125
|
-
describe('useCreatedBy', () => {
|
|
126
|
-
it('should return undefined if showCreatedBy is falsy', () => {
|
|
127
|
-
const resource = {};
|
|
128
|
-
const result = useCreatedBy(resource);
|
|
129
|
-
|
|
130
|
-
expect(result).toBeUndefined();
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it('should return a valid createdBy row', () => {
|
|
134
|
-
mockStore.getters[`type-map/optionsFor`].mockReturnValue({ showAge: true });
|
|
135
|
-
|
|
136
|
-
const resource = { showCreatedBy: true, createdBy: { displayName: 'CREATED_BY' } };
|
|
137
|
-
const result = useCreatedBy(resource);
|
|
138
|
-
|
|
139
|
-
expect(result?.value.to).toBeUndefined();
|
|
140
|
-
expect(result?.value.value).toStrictEqual(resource.createdBy.displayName);
|
|
141
|
-
expect(result?.value.label).toStrictEqual('component.resource.detail.metadata.identifyingInformation.createdBy');
|
|
142
|
-
expect(result?.value.dataTestid).toStrictEqual('masthead-subheader-createdBy');
|
|
143
|
-
expect(result?.value.valueDataTestid).toStrictEqual('masthead-subheader-createdBy_plain-text');
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('should return a valid createdBy row with createdBy.location', () => {
|
|
147
|
-
mockStore.getters[`type-map/optionsFor`].mockReturnValue({ showAge: true });
|
|
148
|
-
|
|
149
|
-
const resource = { showCreatedBy: true, createdBy: { displayName: 'CREATED_BY', location: 'LOCATION' } };
|
|
150
|
-
const result = useCreatedBy(resource);
|
|
151
|
-
|
|
152
|
-
expect(result?.value.to).toStrictEqual(resource.createdBy.location);
|
|
153
|
-
expect(result?.value.value).toStrictEqual(resource.createdBy.displayName);
|
|
154
|
-
expect(result?.value.label).toStrictEqual('component.resource.detail.metadata.identifyingInformation.createdBy');
|
|
155
|
-
expect(result?.value.dataTestid).toStrictEqual('masthead-subheader-createdBy');
|
|
156
|
-
expect(result?.value.valueDataTestid).toStrictEqual('masthead-subheader-createdBy-link');
|
|
157
|
-
});
|
|
158
|
-
});
|
|
159
|
-
|
|
160
107
|
describe('useProject', () => {
|
|
161
108
|
it('should return undefined if type is not namespace', () => {
|
|
162
109
|
const resource = { type: 'anything' };
|
|
@@ -173,11 +120,16 @@ describe('composables: IdentifyingFields', () => {
|
|
|
173
120
|
});
|
|
174
121
|
|
|
175
122
|
it('should return a valid project row', () => {
|
|
176
|
-
const resource = {
|
|
123
|
+
const resource = {
|
|
124
|
+
type: NAMESPACE,
|
|
125
|
+
project: {
|
|
126
|
+
id: 'ID', nameDisplay: 'PROJECT', detailLocation: 'LOCATION'
|
|
127
|
+
}
|
|
128
|
+
};
|
|
177
129
|
const result = useProject(resource);
|
|
178
130
|
|
|
179
|
-
expect(result?.value.
|
|
180
|
-
expect(result?.value.
|
|
131
|
+
expect(result?.value.valueOverride?.props.type).toStrictEqual(MANAGEMENT.PROJECT);
|
|
132
|
+
expect(result?.value.valueOverride?.props.id).toStrictEqual(resource.project.id);
|
|
181
133
|
expect(result?.value.label).toStrictEqual('component.resource.detail.metadata.identifyingInformation.project');
|
|
182
134
|
});
|
|
183
135
|
});
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { mount, RouterLinkStub } from '@vue/test-utils';
|
|
2
2
|
import IdentifyingInformation from '@shell/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue';
|
|
3
|
-
import
|
|
3
|
+
import KeyValueRow from '@shell/components/Resource/Detail/Metadata/KeyValueRow.vue';
|
|
4
4
|
import { markRaw } from 'vue';
|
|
5
|
+
jest.mock('@shell/utils/clipboard', () => ({ copyTextToClipboard: jest.fn() }));
|
|
6
|
+
jest.mock('vuex', () => ({ useStore: () => { } }));
|
|
5
7
|
|
|
6
8
|
describe('component: Metadata/IdentifyingInformation', () => {
|
|
7
9
|
const label = 'LABEL';
|
|
@@ -76,10 +78,35 @@ describe('component: Metadata/IdentifyingInformation', () => {
|
|
|
76
78
|
expect(wrapper.find(`.value .status.${ status }`).exists()).toBeTruthy();
|
|
77
79
|
});
|
|
78
80
|
|
|
79
|
-
it('should render a valueOverride', async() => {
|
|
81
|
+
it('should render a .full-custom-value valueOverride', async() => {
|
|
80
82
|
const valueOverride = {
|
|
81
|
-
component: markRaw(
|
|
82
|
-
props: {
|
|
83
|
+
component: markRaw(KeyValueRow),
|
|
84
|
+
props: { type: 'interactive' }
|
|
85
|
+
};
|
|
86
|
+
const wrapper = mount(IdentifyingInformation, {
|
|
87
|
+
props: {
|
|
88
|
+
rows: [
|
|
89
|
+
{
|
|
90
|
+
label,
|
|
91
|
+
value,
|
|
92
|
+
valueOverride
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
},
|
|
96
|
+
global: { stubs: { 'router-link': RouterLinkStub, KeyValueRow: true } }
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
expect(wrapper.find('.label').element.innerHTML.trim()).toStrictEqual(label);
|
|
100
|
+
|
|
101
|
+
const testComponent = wrapper.find('.full-custom-value').getComponent(KeyValueRow);
|
|
102
|
+
|
|
103
|
+
expect(testComponent.props('type')).toStrictEqual(valueOverride.props.type);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should render a formatter valueOverride', async() => {
|
|
107
|
+
const valueOverride = {
|
|
108
|
+
component: 'router-link',
|
|
109
|
+
props: { to: '#' }
|
|
83
110
|
};
|
|
84
111
|
const wrapper = mount(IdentifyingInformation, {
|
|
85
112
|
props: {
|
|
@@ -96,8 +123,8 @@ describe('component: Metadata/IdentifyingInformation', () => {
|
|
|
96
123
|
|
|
97
124
|
expect(wrapper.find('.label').element.innerHTML.trim()).toStrictEqual(label);
|
|
98
125
|
|
|
99
|
-
const testComponent = wrapper.find('.value').getComponent(
|
|
126
|
+
const testComponent: any = wrapper.find('.value').getComponent('a');
|
|
100
127
|
|
|
101
|
-
expect(testComponent.props('
|
|
128
|
+
expect(testComponent.props('to')).toStrictEqual('#');
|
|
102
129
|
});
|
|
103
130
|
});
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { useI18n } from '@shell/composables/useI18n';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
computed, ComputedRef, defineAsyncComponent, markRaw, toValue
|
|
4
|
+
} from 'vue';
|
|
3
5
|
import Additional from '@shell/components/Resource/Detail/Additional.vue';
|
|
4
6
|
import { useStore } from 'vuex';
|
|
5
7
|
import {
|
|
6
|
-
NAMESPACE, FLEET, SERVICE_ACCOUNT, SECRET, CAPI
|
|
8
|
+
NAMESPACE, FLEET, SERVICE_ACCOUNT, SECRET, CAPI,
|
|
9
|
+
MANAGEMENT
|
|
7
10
|
} from '@shell/config/types';
|
|
8
11
|
import { Row } from '@shell/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue';
|
|
9
12
|
import { NAME as FLEET_NAME } from '@shell/config/product/fleet';
|
|
@@ -21,21 +24,18 @@ export const useNamespace = (resource: any): ComputedRef<Row> | undefined => {
|
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
return computed(() => {
|
|
24
|
-
const to = resourceValue.namespaceLocation || {
|
|
25
|
-
name: `c-cluster-product-resource-id`,
|
|
26
|
-
params: {
|
|
27
|
-
product: store.getters['productId'],
|
|
28
|
-
cluster: store.getters['clusterId'],
|
|
29
|
-
resource: NAMESPACE,
|
|
30
|
-
id: resourceValue.namespace
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
|
|
34
27
|
return {
|
|
35
28
|
label: i18n.t('component.resource.detail.metadata.identifyingInformation.namespace'),
|
|
36
29
|
value: resourceValue.namespace,
|
|
37
30
|
valueDataTestid: 'masthead-subheader-namespace',
|
|
38
|
-
|
|
31
|
+
valueOverride: {
|
|
32
|
+
component: markRaw(defineAsyncComponent(() => import('@shell/components/Resource/Detail/ResourcePopover/index.vue'))),
|
|
33
|
+
props: {
|
|
34
|
+
type: NAMESPACE,
|
|
35
|
+
id: resourceValue.namespace,
|
|
36
|
+
detailLocation: resourceValue.namespaceLocation
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
39
|
};
|
|
40
40
|
});
|
|
41
41
|
};
|
|
@@ -88,28 +88,6 @@ export const useLiveDate = (resource: any): ComputedRef<Row> | undefined => {
|
|
|
88
88
|
}));
|
|
89
89
|
};
|
|
90
90
|
|
|
91
|
-
export const useCreatedBy = (resource: any): ComputedRef<Row> | undefined => {
|
|
92
|
-
const store = useStore();
|
|
93
|
-
const i18n = useI18n(store);
|
|
94
|
-
const resourceValue = toValue(resource);
|
|
95
|
-
|
|
96
|
-
if (!resourceValue.showCreatedBy) {
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return computed(() => {
|
|
101
|
-
const to = resourceValue.createdBy.location || undefined;
|
|
102
|
-
|
|
103
|
-
return {
|
|
104
|
-
label: i18n.t('component.resource.detail.metadata.identifyingInformation.createdBy'),
|
|
105
|
-
value: resourceValue.createdBy.displayName,
|
|
106
|
-
to,
|
|
107
|
-
dataTestid: 'masthead-subheader-createdBy',
|
|
108
|
-
valueDataTestid: to ? 'masthead-subheader-createdBy-link' : 'masthead-subheader-createdBy_plain-text'
|
|
109
|
-
};
|
|
110
|
-
});
|
|
111
|
-
};
|
|
112
|
-
|
|
113
91
|
export const useProject = (resource: any): ComputedRef<Row> | undefined => {
|
|
114
92
|
const store = useStore();
|
|
115
93
|
const i18n = useI18n(store);
|
|
@@ -126,9 +104,17 @@ export const useProject = (resource: any): ComputedRef<Row> | undefined => {
|
|
|
126
104
|
|
|
127
105
|
return computed(() => {
|
|
128
106
|
return {
|
|
129
|
-
label:
|
|
130
|
-
value:
|
|
131
|
-
|
|
107
|
+
label: i18n.t('component.resource.detail.metadata.identifyingInformation.project'),
|
|
108
|
+
value: resourceValue.project?.nameDisplay,
|
|
109
|
+
valueDataTestid: 'masthead-subheader-project',
|
|
110
|
+
valueOverride: {
|
|
111
|
+
component: markRaw(defineAsyncComponent(() => import('@shell/components/Resource/Detail/ResourcePopover/index.vue'))),
|
|
112
|
+
props: {
|
|
113
|
+
type: MANAGEMENT.PROJECT,
|
|
114
|
+
id: resourceValue.project?.id,
|
|
115
|
+
currentStore: 'management'
|
|
116
|
+
}
|
|
117
|
+
}
|
|
132
118
|
};
|
|
133
119
|
});
|
|
134
120
|
};
|
|
@@ -39,8 +39,22 @@ const getRowValueId = (row:Row): string => `value-${ row.label }:${ row.value }`
|
|
|
39
39
|
>
|
|
40
40
|
{{ row.label }}
|
|
41
41
|
</label>
|
|
42
|
+
<!-- A custom component specified as an object, responsible for it's own styling -->
|
|
42
43
|
<div
|
|
43
|
-
v-if="row.valueOverride?.component && row.value"
|
|
44
|
+
v-if="typeof row.valueOverride?.component !== 'string' && row.valueOverride?.component && row.value"
|
|
45
|
+
:id="getRowValueId(row)"
|
|
46
|
+
class="full-custom-value"
|
|
47
|
+
>
|
|
48
|
+
<component
|
|
49
|
+
:is="row.valueOverride?.component"
|
|
50
|
+
v-if="row.valueOverride?.component"
|
|
51
|
+
v-bind="row.valueOverride?.props"
|
|
52
|
+
:data-testid="row.valueDataTestid"
|
|
53
|
+
/>
|
|
54
|
+
</div>
|
|
55
|
+
<!-- A formatter with a component specified as a string -->
|
|
56
|
+
<div
|
|
57
|
+
v-else-if="row.valueOverride?.component && row.value"
|
|
44
58
|
:id="getRowValueId(row)"
|
|
45
59
|
class="value"
|
|
46
60
|
>
|
|
@@ -91,16 +105,22 @@ const getRowValueId = (row:Row): string => `value-${ row.label }:${ row.value }`
|
|
|
91
105
|
.row {
|
|
92
106
|
margin-bottom: 8px;
|
|
93
107
|
|
|
108
|
+
.full-custom-value {
|
|
109
|
+
flex: 1;
|
|
110
|
+
}
|
|
111
|
+
|
|
94
112
|
.value {
|
|
95
113
|
display: flex;
|
|
96
114
|
flex-direction: row;
|
|
97
115
|
align-items: center;
|
|
116
|
+
flex: 1;
|
|
98
117
|
|
|
99
|
-
|
|
118
|
+
& > div, & > span {
|
|
100
119
|
max-width: 100%;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
&, & > div, & > span {
|
|
123
|
+
@include clip;
|
|
104
124
|
}
|
|
105
125
|
}
|
|
106
126
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { computed, toRefs } from 'vue';
|
|
3
|
-
import
|
|
3
|
+
import KeyValueRow from '@shell/components/Resource/Detail/Metadata/KeyValueRow.vue';
|
|
4
4
|
import { useStore } from 'vuex';
|
|
5
5
|
import { useI18n } from '@shell/composables/useI18n';
|
|
6
|
+
import { Type } from '@components/Pill/types';
|
|
6
7
|
|
|
7
8
|
export type KeyValueType = {[key: string]: string};
|
|
8
9
|
|
|
@@ -14,9 +15,9 @@ export interface Row {
|
|
|
14
15
|
export interface KeyValueProps {
|
|
15
16
|
propertyName: string;
|
|
16
17
|
rows: Row[];
|
|
17
|
-
|
|
18
|
-
outline?: boolean;
|
|
18
|
+
type: Type;
|
|
19
19
|
|
|
20
|
+
maxRows?: number;
|
|
20
21
|
onShowConfiguration?: (returnFocusSelector: string) => void;
|
|
21
22
|
}
|
|
22
23
|
</script>
|
|
@@ -24,13 +25,9 @@ export interface KeyValueProps {
|
|
|
24
25
|
<script setup lang="ts">
|
|
25
26
|
const props = withDefaults(
|
|
26
27
|
defineProps<KeyValueProps>(),
|
|
27
|
-
{
|
|
28
|
-
outline: false, maxRows: 4, onShowConfiguration: undefined
|
|
29
|
-
}
|
|
28
|
+
{ maxRows: 4, onShowConfiguration: undefined }
|
|
30
29
|
);
|
|
31
|
-
const {
|
|
32
|
-
propertyName, rows, maxRows, outline
|
|
33
|
-
} = toRefs(props);
|
|
30
|
+
const { propertyName, rows, maxRows } = toRefs(props);
|
|
34
31
|
|
|
35
32
|
const store = useStore();
|
|
36
33
|
const i18n = useI18n(store);
|
|
@@ -80,12 +77,11 @@ const showConfigurationMoreFocusSelector = computed(() => `[data-testid="${ show
|
|
|
80
77
|
:key="displayValue(row)"
|
|
81
78
|
class="row"
|
|
82
79
|
>
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
</Rectangle>
|
|
80
|
+
<KeyValueRow
|
|
81
|
+
class="rectangle"
|
|
82
|
+
:type="props.type"
|
|
83
|
+
:row="row"
|
|
84
|
+
/>
|
|
89
85
|
</div>
|
|
90
86
|
<a
|
|
91
87
|
v-if="showShowAllButton"
|
|
@@ -116,6 +112,7 @@ const showConfigurationMoreFocusSelector = computed(() => `[data-testid="${ show
|
|
|
116
112
|
.row {
|
|
117
113
|
display: block;
|
|
118
114
|
width: 100%;
|
|
115
|
+
display: inline-block;
|
|
119
116
|
|
|
120
117
|
&:not(:nth-child(2)) {
|
|
121
118
|
margin-top: 4px;
|
|
@@ -125,14 +122,6 @@ const showConfigurationMoreFocusSelector = computed(() => `[data-testid="${ show
|
|
|
125
122
|
margin-top: 8px;
|
|
126
123
|
}
|
|
127
124
|
|
|
128
|
-
.rectangle {
|
|
129
|
-
display: inline-block;
|
|
130
|
-
max-width: 100%;
|
|
131
|
-
overflow: hidden;
|
|
132
|
-
text-overflow: ellipsis;
|
|
133
|
-
white-space: nowrap;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
125
|
.no-rows {
|
|
137
126
|
line-height: 21px;
|
|
138
127
|
}
|