@rancher/shell 3.0.5-rc.3 → 3.0.5-rc.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.
Files changed (200) hide show
  1. package/assets/images/icons/document.svg +3 -0
  2. package/assets/images/vendor/cognito.svg +1 -0
  3. package/assets/styles/app.scss +1 -0
  4. package/assets/styles/base/_basic.scss +10 -0
  5. package/assets/styles/base/_spacing.scss +29 -0
  6. package/assets/styles/global/_layout.scss +1 -1
  7. package/assets/styles/themes/_dark.scss +25 -0
  8. package/assets/styles/themes/_light.scss +65 -0
  9. package/assets/translations/en-us.yaml +322 -24
  10. package/assets/translations/zh-hans.yaml +8 -5
  11. package/components/Certificates.vue +5 -0
  12. package/components/FilterPanel.vue +156 -0
  13. package/components/{fleet/ForceDirectedTreeChart/index.vue → ForceDirectedTreeChart.vue} +47 -41
  14. package/components/IconOrSvg.vue +14 -35
  15. package/components/PromptRemove.vue +5 -1
  16. package/components/Resource/Detail/Card/PodsCard/Bubble.vue +13 -0
  17. package/components/Resource/Detail/Card/PodsCard/composable.ts +30 -0
  18. package/components/Resource/Detail/Card/PodsCard/index.vue +118 -0
  19. package/components/Resource/Detail/Card/ResourceUsageCard/composable.ts +51 -0
  20. package/components/Resource/Detail/Card/ResourceUsageCard/index.vue +79 -0
  21. package/components/Resource/Detail/Card/Scaler.vue +89 -0
  22. package/components/Resource/Detail/Card/StateCard/composables.ts +112 -0
  23. package/components/Resource/Detail/Card/StateCard/index.vue +39 -0
  24. package/components/Resource/Detail/Card/VerticalGap.vue +11 -0
  25. package/components/Resource/Detail/Card/__tests__/Card.test.ts +36 -0
  26. package/components/Resource/Detail/Card/__tests__/PodsCard.test.ts +84 -0
  27. package/components/Resource/Detail/Card/__tests__/ResourceUsageCard.test.ts +72 -0
  28. package/components/Resource/Detail/Card/__tests__/Scaler.test.ts +87 -0
  29. package/components/Resource/Detail/Card/__tests__/StateCard.test.ts +53 -0
  30. package/components/Resource/Detail/Card/__tests__/VerticalGap.test.ts +14 -0
  31. package/components/Resource/Detail/Card/__tests__/index.test.ts +36 -0
  32. package/components/Resource/Detail/Card/index.vue +56 -0
  33. package/components/Resource/Detail/Metadata/Annotations/__tests__/index.test.ts +19 -0
  34. package/components/Resource/Detail/Metadata/Annotations/composable.ts +12 -0
  35. package/components/Resource/Detail/Metadata/Annotations/index.vue +26 -0
  36. package/components/Resource/Detail/Metadata/IdentifyingInformation/__tests__/index.test.ts +103 -0
  37. package/components/Resource/Detail/Metadata/IdentifyingInformation/composable.ts +281 -0
  38. package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +111 -0
  39. package/components/Resource/Detail/Metadata/KeyValue.vue +130 -0
  40. package/components/Resource/Detail/Metadata/Labels/__tests__/index.test.ts +18 -0
  41. package/components/Resource/Detail/Metadata/Labels/composable.ts +12 -0
  42. package/components/Resource/Detail/Metadata/Labels/index.vue +27 -0
  43. package/components/Resource/Detail/Metadata/Rectangle.vue +32 -0
  44. package/components/Resource/Detail/Metadata/__tests__/KeyValue.test.ts +107 -0
  45. package/components/Resource/Detail/Metadata/__tests__/Rectangle.test.ts +24 -0
  46. package/components/Resource/Detail/Metadata/__tests__/index.test.ts +91 -0
  47. package/components/Resource/Detail/Metadata/composables.ts +29 -0
  48. package/components/Resource/Detail/Metadata/index.vue +66 -0
  49. package/components/Resource/Detail/Page.vue +22 -0
  50. package/components/Resource/Detail/PercentageBar.vue +40 -0
  51. package/components/Resource/Detail/ResourceRow.vue +119 -0
  52. package/components/Resource/Detail/SpacedRow.vue +14 -0
  53. package/components/Resource/Detail/StatusBar.vue +59 -0
  54. package/components/Resource/Detail/StatusRow.vue +61 -0
  55. package/components/Resource/Detail/TitleBar/Title.vue +13 -0
  56. package/components/Resource/Detail/TitleBar/Top.vue +14 -0
  57. package/components/Resource/Detail/TitleBar/__tests__/Title.test.ts +17 -0
  58. package/components/Resource/Detail/TitleBar/__tests__/Top.test.ts +17 -0
  59. package/components/Resource/Detail/TitleBar/__tests__/index.test.ts +142 -0
  60. package/components/Resource/Detail/TitleBar/composable.ts +31 -0
  61. package/components/Resource/Detail/TitleBar/index.vue +124 -0
  62. package/components/Resource/Detail/Top/index.vue +34 -0
  63. package/components/Resource/Detail/__tests__/Page.test.ts +32 -0
  64. package/components/ResourceDetail/__tests__/index.test.ts +114 -0
  65. package/components/ResourceDetail/index.vue +64 -562
  66. package/components/ResourceDetail/legacy.vue +545 -0
  67. package/components/ResourceTable.vue +41 -7
  68. package/components/SlideInPanelManager.vue +76 -8
  69. package/components/SortableTable/index.vue +13 -2
  70. package/components/SortableTable/selection.js +21 -8
  71. package/components/StatusBadge.vue +6 -4
  72. package/components/SubtleLink.vue +25 -0
  73. package/components/Wizard.vue +12 -1
  74. package/components/YamlEditor.vue +1 -1
  75. package/components/__tests__/FilterPanel.test.ts +81 -0
  76. package/components/auth/AuthBanner.vue +2 -3
  77. package/components/auth/RoleDetailEdit.vue +45 -3
  78. package/components/auth/login/oidc.vue +6 -1
  79. package/components/fleet/FleetApplications.vue +181 -0
  80. package/components/fleet/FleetHelmOps.vue +115 -0
  81. package/components/fleet/FleetIntro.vue +58 -28
  82. package/components/fleet/FleetNoWorkspaces.vue +5 -1
  83. package/components/fleet/FleetOCIStorageSecret.vue +171 -0
  84. package/components/fleet/FleetRepos.vue +38 -76
  85. package/components/fleet/FleetResources.vue +50 -22
  86. package/components/fleet/FleetSummary.vue +26 -51
  87. package/components/fleet/__tests__/FleetOCIStorageSecret.test.ts +213 -0
  88. package/components/fleet/__tests__/FleetSummary.test.ts +39 -39
  89. package/components/fleet/dashboard/Empty.vue +73 -0
  90. package/components/fleet/dashboard/ResourceCard.vue +183 -0
  91. package/components/fleet/dashboard/ResourceCardSummary.vue +199 -0
  92. package/components/fleet/dashboard/ResourceDetails.vue +196 -0
  93. package/components/fleet/dashboard/ResourcePanel.vue +376 -0
  94. package/components/form/ArrayList.vue +6 -0
  95. package/components/form/SimpleSecretSelector.vue +8 -2
  96. package/components/form/ValueFromResource.vue +31 -19
  97. package/components/formatter/FleetApplicationClustersReady.vue +77 -0
  98. package/components/formatter/FleetApplicationSource.vue +71 -0
  99. package/components/formatter/FleetSummaryGraph.vue +7 -0
  100. package/components/nav/Header.vue +8 -7
  101. package/components/nav/TopLevelMenu.helper.ts +55 -34
  102. package/components/nav/TopLevelMenu.vue +11 -0
  103. package/components/nav/Type.vue +4 -1
  104. package/composables/useI18n.ts +12 -11
  105. package/config/labels-annotations.js +14 -11
  106. package/config/product/auth.js +1 -0
  107. package/config/product/fleet.js +70 -17
  108. package/config/query-params.js +3 -1
  109. package/config/roles.ts +1 -0
  110. package/config/router/routes.js +20 -2
  111. package/config/secret.ts +15 -0
  112. package/config/settings.ts +3 -2
  113. package/config/table-headers.js +52 -22
  114. package/config/types.js +2 -0
  115. package/core/plugin-helpers.ts +3 -2
  116. package/detail/fleet.cattle.io.cluster.vue +28 -15
  117. package/detail/fleet.cattle.io.gitrepo.vue +10 -1
  118. package/detail/fleet.cattle.io.helmop.vue +157 -0
  119. package/dialog/HelmOpForceUpdateDialog.vue +132 -0
  120. package/dialog/RedeployWorkloadDialog.vue +164 -0
  121. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +56 -67
  122. package/edit/auth/oidc.vue +159 -93
  123. package/edit/fleet.cattle.io.gitrepo.vue +26 -33
  124. package/edit/fleet.cattle.io.helmop.vue +997 -0
  125. package/edit/management.cattle.io.fleetworkspace.vue +43 -10
  126. package/list/fleet.cattle.io.gitrepo.vue +1 -1
  127. package/list/fleet.cattle.io.helmop.vue +108 -0
  128. package/list/namespace.vue +5 -2
  129. package/mixins/auth-config.js +8 -1
  130. package/mixins/preset.js +100 -0
  131. package/mixins/resource-fetch-api-pagination.js +2 -0
  132. package/mixins/resource-fetch.js +1 -1
  133. package/mixins/resource-table-watch.js +45 -0
  134. package/models/__tests__/chart.test.ts +273 -0
  135. package/models/__tests__/fleet.cattle.io.gitrepo.test.ts +1 -1
  136. package/models/chart.js +144 -2
  137. package/models/fleet-application.js +385 -0
  138. package/models/fleet.cattle.io.bundle.js +9 -8
  139. package/models/fleet.cattle.io.gitrepo.js +41 -365
  140. package/models/fleet.cattle.io.helmop.js +228 -0
  141. package/models/management.cattle.io.authconfig.js +1 -0
  142. package/models/management.cattle.io.fleetworkspace.js +12 -0
  143. package/models/workload.js +14 -18
  144. package/package.json +2 -1
  145. package/pages/auth/verify.vue +13 -1
  146. package/pages/c/_cluster/apps/charts/AddRepoLink.vue +37 -0
  147. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +80 -0
  148. package/pages/c/_cluster/apps/charts/AppChartCardSubHeader.vue +54 -0
  149. package/pages/c/_cluster/apps/charts/StatusLabel.vue +33 -0
  150. package/pages/c/_cluster/apps/charts/index.vue +302 -484
  151. package/pages/c/_cluster/explorer/EventsTable.vue +1 -1
  152. package/pages/c/_cluster/fleet/__tests__/index.test.ts +426 -0
  153. package/pages/c/_cluster/fleet/application/_resource/_id.vue +14 -0
  154. package/pages/c/_cluster/fleet/application/_resource/create.vue +14 -0
  155. package/pages/c/_cluster/fleet/application/create.vue +340 -0
  156. package/pages/c/_cluster/fleet/application/index.vue +139 -0
  157. package/pages/c/_cluster/fleet/graph/config.js +277 -0
  158. package/pages/c/_cluster/fleet/index.vue +772 -330
  159. package/pages/explorer/resource/detail/configmap.vue +19 -0
  160. package/plugins/dashboard-store/actions.js +31 -9
  161. package/plugins/dashboard-store/getters.js +34 -21
  162. package/plugins/dashboard-store/mutations.js +51 -7
  163. package/plugins/dashboard-store/resource-class.js +14 -2
  164. package/plugins/steve/__tests__/subscribe.spec.ts +66 -1
  165. package/plugins/steve/actions.js +3 -0
  166. package/plugins/steve/steve-pagination-utils.ts +14 -13
  167. package/plugins/steve/subscribe.js +229 -42
  168. package/rancher-components/BadgeState/BadgeState.vue +3 -1
  169. package/rancher-components/Form/Checkbox/Checkbox.vue +2 -2
  170. package/rancher-components/RcItemCard/RcItemCard.test.ts +189 -0
  171. package/rancher-components/RcItemCard/RcItemCard.vue +425 -0
  172. package/rancher-components/RcItemCard/RcItemCardAction.vue +24 -0
  173. package/rancher-components/RcItemCard/index.ts +2 -0
  174. package/store/auth.js +1 -0
  175. package/store/catalog.js +62 -24
  176. package/store/index.js +33 -14
  177. package/store/slideInPanel.ts +6 -0
  178. package/store/type-map.js +1 -0
  179. package/types/fleet.d.ts +35 -0
  180. package/types/resources/settings.d.ts +19 -1
  181. package/types/shell/index.d.ts +339 -272
  182. package/types/store/dashboard-store.types.ts +17 -3
  183. package/types/store/pagination.types.ts +6 -1
  184. package/types/store/subscribe.types.ts +50 -0
  185. package/utils/auth.js +32 -3
  186. package/utils/fleet-types.ts +0 -0
  187. package/utils/fleet.ts +200 -1
  188. package/utils/pagination-utils.ts +26 -1
  189. package/utils/pagination-wrapper.ts +132 -50
  190. package/utils/settings.ts +4 -1
  191. package/utils/style.ts +39 -0
  192. package/utils/validators/formRules/__tests__/index.test.ts +36 -3
  193. package/utils/validators/formRules/index.ts +10 -3
  194. package/utils/window.js +11 -7
  195. package/components/__tests__/ApplicationCard.test.ts +0 -27
  196. package/components/cards/ApplicationCard.vue +0 -145
  197. package/components/fleet/ForceDirectedTreeChart/chartIcons.js +0 -17
  198. package/config/secret.js +0 -14
  199. package/pages/c/_cluster/fleet/GitRepoGraphConfig.js +0 -249
  200. /package/{components/form/SSHKnownHosts → dialog}/__tests__/KnownHostsEditDialog.test.ts +0 -0
@@ -0,0 +1,119 @@
1
+ <script lang="ts">
2
+ import SubtleLink from '@shell/components/SubtleLink.vue';
3
+ import { StateColor, stateColorCssVar } from '@shell/utils/style';
4
+ import { sortBy } from 'lodash';
5
+ import { RouteLocationRaw } from 'vue-router';
6
+
7
+ export interface Count {
8
+ label: string;
9
+ count: number;
10
+ }
11
+
12
+ export interface Props {
13
+ label: string;
14
+ to?: RouteLocationRaw;
15
+ color?: StateColor;
16
+ counts?: Count[];
17
+ }
18
+
19
+ export function extractCounts(labels: string[]): Count[] {
20
+ const accumulator: { [k: string]: number} = {};
21
+
22
+ labels.forEach((l: string) => {
23
+ accumulator[l] = accumulator[l] || 0;
24
+ accumulator[l]++;
25
+ });
26
+
27
+ const counts: Count[] = Object.entries(accumulator).map(([label, count]) => ({ label, count }));
28
+
29
+ return sortBy(counts, 'label');
30
+ }
31
+
32
+ </script>
33
+
34
+ <script setup lang="ts">
35
+ const {
36
+ label, to, counts, color
37
+ } = defineProps<Props>();
38
+ </script>
39
+
40
+ <template>
41
+ <div class="resource-row">
42
+ <div class="left">
43
+ <SubtleLink
44
+ v-if="to"
45
+ :to="to"
46
+ >
47
+ {{ label }}
48
+ </SubtleLink>
49
+ <span
50
+ v-else
51
+ class="text-muted"
52
+ >
53
+ {{ label }}
54
+ </span>
55
+ </div>
56
+ <div class="right">
57
+ <div
58
+ v-if="!counts || counts.length == 0"
59
+ class="text-muted"
60
+ >
61
+ &mdash;
62
+ </div>
63
+ <div
64
+ v-else
65
+ class="counts"
66
+ >
67
+ <span
68
+ v-if="color"
69
+ class="dot"
70
+ :style="{backgroundColor: stateColorCssVar(color)}"
71
+ >
72
+ &nbsp;
73
+ </span>
74
+ <span
75
+ v-for="count in counts"
76
+ :key="count.label"
77
+ class="count"
78
+ >
79
+ {{ count.count }} {{ count.label }}<span class="and">&nbsp;+&nbsp;</span>
80
+ </span>
81
+ </div>
82
+ </div>
83
+ </div>
84
+ </template>
85
+
86
+ <style lang="scss" scoped>
87
+ .resource-row {
88
+ display: flex;
89
+ flex-direction: row;
90
+ align-items: center;
91
+
92
+ .right {
93
+ flex-grow: 1;
94
+ text-align: right;
95
+ }
96
+
97
+ .counts {
98
+ display: inline-flex;
99
+ flex-direction: row;
100
+ justify-content: flex-end;
101
+ align-items: center;
102
+ }
103
+
104
+ .count:last-of-type .and {
105
+ display: none;
106
+ }
107
+
108
+ .dot {
109
+ display: inline-block;
110
+
111
+ $size: 6px;
112
+ width: $size;
113
+ height: $size;
114
+
115
+ border-radius: 50%;
116
+ margin-right: 10px;
117
+ }
118
+ }
119
+ </style>
@@ -0,0 +1,14 @@
1
+ <template>
2
+ <div class="spaced-row">
3
+ <slot />
4
+ </div>
5
+ </template>
6
+
7
+ <style lang="scss" scoped>
8
+ .spaced-row {
9
+ display: grid;
10
+ grid-template-columns: 1fr 1fr 1fr;
11
+ grid-auto-flow: dense;
12
+ grid-gap: 24px;
13
+ }
14
+ </style>
@@ -0,0 +1,59 @@
1
+ <script setup lang="ts">
2
+ import { stateColorCssVar, StateColor } from '@shell/utils/style';
3
+
4
+ export interface Segment {
5
+ color: StateColor;
6
+ percent: number;
7
+ }
8
+
9
+ export interface Props {
10
+ segments: Segment[]
11
+ }
12
+
13
+ const { segments } = defineProps<Props>();
14
+
15
+ const computeStyle = (segment: Segment) => {
16
+ return {
17
+ backgroundColor: stateColorCssVar(segment.color),
18
+ width: `${ segment.percent }%`
19
+ };
20
+ };
21
+ </script>
22
+
23
+ <template>
24
+ <div class="status-bar">
25
+ <div
26
+ v-for="(segment, i) in segments"
27
+ :key="i"
28
+ class="segment"
29
+ :style="computeStyle(segment)"
30
+ >
31
+ &nbsp;
32
+ </div>
33
+ </div>
34
+ </template>
35
+
36
+ <style lang="scss" scoped>
37
+ .status-bar {
38
+ display: flex;
39
+ flex-direction: row;
40
+ justify-content: center;
41
+
42
+ column-gap: 2px;
43
+ height: 21px;
44
+
45
+ .segment {
46
+ height: 4px;
47
+
48
+ &:first-of-type {
49
+ border-top-left-radius: 4px;
50
+ border-bottom-left-radius: 4px;
51
+ }
52
+
53
+ &:last-of-type {
54
+ border-top-right-radius: 4px;
55
+ border-bottom-right-radius: 4px;
56
+ }
57
+ }
58
+ }
59
+ </style>
@@ -0,0 +1,61 @@
1
+ <script setup lang="ts">
2
+ import Bubble from '@shell/components/Resource/Detail/Card/PodsCard/Bubble.vue';
3
+ import { StateColor, stateColorCssVar } from '@shell/utils/style';
4
+
5
+ export interface Props {
6
+ color: StateColor;
7
+ label: string;
8
+ count: number;
9
+ percent: number;
10
+ }
11
+
12
+ const {
13
+ color, label, count, percent
14
+ } = defineProps<Props>();
15
+ </script>
16
+
17
+ <template>
18
+ <div class="status-row">
19
+ <div
20
+ class="indicator"
21
+ :style="{backgroundColor: stateColorCssVar(color)}"
22
+ />
23
+ <div class="label">
24
+ {{ label }}
25
+ </div>
26
+ <div class="count">
27
+ <Bubble>{{ count }}</Bubble>
28
+ </div>
29
+ <div class="percent text-muted">
30
+ {{ percent.toFixed(1) }}%
31
+ </div>
32
+ </div>
33
+ </template>
34
+
35
+ <style lang="scss" scoped>
36
+ .status-row {
37
+ display: flex;
38
+ flex-direction: row;
39
+ align-items: center;
40
+
41
+ &:not(:first-of-type) {
42
+ margin-top: 8px;
43
+ }
44
+
45
+ .label {
46
+ flex-grow: 1;
47
+ }
48
+
49
+ .indicator {
50
+ height: 4px;
51
+ border-radius: 4px;
52
+ width: 20px;
53
+ margin-right: 10px;
54
+ }
55
+
56
+ .percent {
57
+ width: 60px;
58
+ text-align: right;
59
+ }
60
+ }
61
+ </style>
@@ -0,0 +1,13 @@
1
+ <template>
2
+ <h1 class="title">
3
+ <slot name="default" />
4
+ </h1>
5
+ </template>
6
+
7
+ <style lang="scss" scoped>
8
+ h1.title {
9
+ display: inline-block;
10
+ align-items: center;
11
+ line-height: 18px;
12
+ }
13
+ </style>
@@ -0,0 +1,14 @@
1
+ <template>
2
+ <div class="top">
3
+ <slot name="default" />
4
+ </div>
5
+ </template>
6
+
7
+ <style lang="scss" scoped>
8
+ .top {
9
+ display: flex;
10
+ flex-direction: row;
11
+ justify-content: space-between;
12
+ align-items: center;
13
+ }
14
+ </style>
@@ -0,0 +1,17 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import Title from '@shell/components/Resource/Detail/TitleBar/Title.vue';
3
+
4
+ describe('component: TitleBar/Title', () => {
5
+ it('shoulder render container with class title', async() => {
6
+ const wrapper = mount(Title);
7
+
8
+ expect(wrapper.find('.title').exists()).toBeTruthy();
9
+ });
10
+
11
+ it('should render default slot', async() => {
12
+ const content = 'CONTENT';
13
+ const wrapper = mount(Title, { slots: { default: content } });
14
+
15
+ expect(wrapper.find('.title').element.innerHTML).toStrictEqual(content);
16
+ });
17
+ });
@@ -0,0 +1,17 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import Top from '@shell/components/Resource/Detail/TitleBar/Top.vue';
3
+
4
+ describe('component: TitleBar/Top', () => {
5
+ it('shoulder render container with class top', async() => {
6
+ const wrapper = mount(Top);
7
+
8
+ expect(wrapper.find('.top').exists()).toBeTruthy();
9
+ });
10
+
11
+ it('should render default slot', async() => {
12
+ const content = 'CONTENT';
13
+ const wrapper = mount(Top, { slots: { default: content } });
14
+
15
+ expect(wrapper.find('.top').element.innerHTML).toStrictEqual(content);
16
+ });
17
+ });
@@ -0,0 +1,142 @@
1
+ import { mount, RouterLinkStub } from '@vue/test-utils';
2
+ import TitleBar from '@shell/components/Resource/Detail/TitleBar/index.vue';
3
+ import ActionMenu from '@shell/components/ActionMenuShell.vue';
4
+ import { createStore } from 'vuex';
5
+
6
+ jest.mock(`@shell/assets/images/icons/document.svg`, () => `@shell/assets/images/icons/document.svg`);
7
+
8
+ describe('component: TitleBar/index', () => {
9
+ const resourceTypeLabel = 'RESOURCE_TYPE_LABEL';
10
+ const resourceTo = 'RESOURCE_TO';
11
+ const resourceName = 'RESOURCE_NAME';
12
+ const store = createStore({});
13
+
14
+ it('shoulder render container with class title-bar', async() => {
15
+ const wrapper = mount(TitleBar, {
16
+ props: {
17
+ resourceTypeLabel,
18
+ resourceName
19
+ },
20
+ global: { stubs: { 'router-link': RouterLinkStub }, provide: { store } }
21
+ });
22
+
23
+ expect(wrapper.find('.title-bar').exists()).toBeTruthy();
24
+ });
25
+
26
+ it('should render type label as a link when resourceTo is provided', async() => {
27
+ const wrapper = mount(TitleBar, {
28
+ props: {
29
+ resourceTypeLabel, resourceTo, resourceName
30
+ },
31
+ global: { stubs: { 'router-link': RouterLinkStub }, provide: { store } }
32
+ });
33
+
34
+ const link = wrapper.find('.top .title .resource-link').findComponent(RouterLinkStub);
35
+
36
+ expect(link.props('to')).toStrictEqual(resourceTo);
37
+ expect(link.element.innerHTML.trim()).toStrictEqual(`${ resourceTypeLabel }:`);
38
+ });
39
+
40
+ it('should render type label as text when resourceTo is not provided', async() => {
41
+ const wrapper = mount(TitleBar, {
42
+ props: { resourceTypeLabel, resourceName },
43
+ global: { stubs: { 'router-link': RouterLinkStub }, provide: { store } }
44
+ });
45
+
46
+ const span = wrapper.find('.top > .title > .resource-text');
47
+
48
+ expect(span.element.innerHTML.trim()).toStrictEqual(`${ resourceTypeLabel }:`);
49
+ });
50
+
51
+ it('should render resourceName', async() => {
52
+ const wrapper = mount(TitleBar, {
53
+ props: { resourceTypeLabel, resourceName },
54
+ global: { stubs: { 'router-link': RouterLinkStub }, provide: { store } }
55
+ });
56
+
57
+ const span = wrapper.find('.top > .title > .resource-name');
58
+
59
+ expect(span.element.innerHTML).toStrictEqual(resourceName);
60
+ });
61
+
62
+ it('should hide the ShowConfiguration button if onShowConfiguration is not defined', async() => {
63
+ const wrapper = mount(TitleBar, {
64
+ props: { resourceTypeLabel, resourceName },
65
+ global: { stubs: { 'router-link': RouterLinkStub }, provide: { store } }
66
+ });
67
+
68
+ expect(wrapper.find('.top > .actions > .show-configuration').exists()).toBeFalsy();
69
+ });
70
+
71
+ it('should pass appropriate props to RcButton if onShowConfiguration is defined and emits show-configuration', async() => {
72
+ const wrapper = mount(TitleBar, {
73
+ props: {
74
+ resourceTypeLabel, resourceName, onShowConfiguration: () => {}
75
+ },
76
+ global: { stubs: { 'router-link': RouterLinkStub, RcButton: true }, provide: { store } }
77
+ });
78
+
79
+ const button = wrapper.find('.top > .actions > .show-configuration');
80
+ const buttonComponent = button.getComponent<any>('rc-button-stub');
81
+
82
+ expect(buttonComponent.props('primary')).toStrictEqual(true);
83
+ button.trigger('click');
84
+
85
+ expect(wrapper.emitted()).toHaveProperty('show-configuration');
86
+ });
87
+
88
+ it('should hide ActionMenu if actionMenuResource is not defined', async() => {
89
+ const wrapper = mount(TitleBar, {
90
+ props: { resourceTypeLabel, resourceName },
91
+ global: { stubs: { 'router-link': RouterLinkStub, ActionMenu }, provide: { store } }
92
+ });
93
+
94
+ expect(wrapper.find('.top > .actions > [data-testid="masthead-action-menu"]').exists()).toBeFalsy();
95
+ });
96
+
97
+ it('should pass appropriate props to ActionMenu if actionMenuResource', async() => {
98
+ const actionMenuResource = { resource: 'test' };
99
+ const wrapper = mount(TitleBar, {
100
+ props: {
101
+ resourceTypeLabel, resourceName, actionMenuResource
102
+ },
103
+ global: {
104
+ stubs: { 'router-link': RouterLinkStub, ActionMenu: true },
105
+ provide: { store }
106
+ }
107
+ });
108
+
109
+ const actions = wrapper.find('.top > .actions');
110
+ const actionMenuComponent = actions.getComponent<any>('action-menu-stub');
111
+
112
+ expect(actionMenuComponent.props('buttonRole')).toStrictEqual('multiAction');
113
+ expect(actionMenuComponent.props('resource')).toStrictEqual(actionMenuResource);
114
+ });
115
+
116
+ it('should hide the description element if description is not passed', async() => {
117
+ const wrapper = mount(TitleBar, {
118
+ props: {
119
+ resourceTypeLabel,
120
+ resourceName
121
+ },
122
+ global: { stubs: { 'router-link': RouterLinkStub }, provide: { store } }
123
+ });
124
+
125
+ expect(wrapper.find('.bottom.description').exists()).toBeFalsy();
126
+ });
127
+
128
+ it('should show the description element if description is passed', async() => {
129
+ const description = 'DESCRIPTION';
130
+ const wrapper = mount(TitleBar, {
131
+ props: {
132
+ resourceTypeLabel,
133
+ resourceName,
134
+ description
135
+ },
136
+ global: { stubs: { 'router-link': RouterLinkStub }, provide: { store } }
137
+ });
138
+
139
+ expect(wrapper.find('.bottom.description').exists()).toBeTruthy();
140
+ expect(wrapper.find('.bottom.description').element.innerHTML).toStrictEqual(description);
141
+ });
142
+ });
@@ -0,0 +1,31 @@
1
+ import { TitleBarProps } from '@shell/components/Resource/Detail/TitleBar/index.vue';
2
+ import { computed, Ref, toValue } from 'vue';
3
+ import { useRoute } from 'vue-router';
4
+ import { useStore } from 'vuex';
5
+
6
+ export const useDefaultTitleBarData = (resource: any): Ref<TitleBarProps> => {
7
+ const route = useRoute();
8
+ const store = useStore();
9
+
10
+ const resourceValue = toValue(resource);
11
+
12
+ return computed(() => ({
13
+ resourceTypeLabel: store.getters['type-map/labelFor']({ id: resourceValue.type }),
14
+ resourceTo: {
15
+ name: 'c-cluster-product-resource',
16
+ params: {
17
+ product: 'explorer',
18
+ cluster: route.params.cluster,
19
+ namespace: resourceValue.namespace,
20
+ resource: resourceValue.type
21
+ }
22
+ },
23
+ resourceName: resourceValue.nameDisplay,
24
+ actionMenuResource: resourceValue,
25
+ badge: {
26
+ color: resourceValue.stateBackground,
27
+ label: resourceValue.stateDisplay
28
+ },
29
+ onShowConfiguration: () => resourceValue.goToEdit()
30
+ }));
31
+ };
@@ -0,0 +1,124 @@
1
+ <script lang="ts">
2
+ import BadgeState from '@pkg/rancher-components/src/components/BadgeState/BadgeState.vue';
3
+ import { RouteLocationRaw } from 'vue-router';
4
+ import Title from '@shell/components/Resource/Detail/TitleBar/Title.vue';
5
+ import Top from '@shell/components/Resource/Detail/TitleBar/Top.vue';
6
+ import ActionMenu from '@shell/components/ActionMenuShell.vue';
7
+ import { useStore } from 'vuex';
8
+ import { useI18n } from '@shell/composables/useI18n';
9
+ import RcButton from '~/pkg/rancher-components/src/components/RcButton/RcButton.vue';
10
+
11
+ export interface Badge {
12
+ color: 'bg-success' | 'bg-error' | 'bg-warning' | 'bg-info';
13
+ label: string;
14
+ }
15
+
16
+ export interface TitleBarProps {
17
+ resourceTypeLabel: string;
18
+ resourceName: string;
19
+
20
+ resourceTo?: RouteLocationRaw;
21
+ description?: string;
22
+ badge?: Badge;
23
+
24
+ // This should be replaced with a list of menu items we want to render.
25
+ // I don't have the time right now to swap this out though.
26
+ actionMenuResource?: any;
27
+
28
+ onShowConfiguration?: () => void;
29
+ }
30
+
31
+ const showConfigurationIcon = require(`@shell/assets/images/icons/document.svg`);
32
+ </script>
33
+
34
+ <script setup lang="ts">
35
+ const {
36
+ resourceTypeLabel, resourceTo, resourceName, description, badge, onShowConfiguration
37
+ } = defineProps<TitleBarProps>();
38
+
39
+ const store = useStore();
40
+ const i18n = useI18n(store);
41
+
42
+ const emit = defineEmits(['show-configuration']);
43
+ </script>
44
+
45
+ <template>
46
+ <div class="title-bar">
47
+ <Top>
48
+ <Title>
49
+ <router-link
50
+ v-if="resourceTo"
51
+ :to="resourceTo"
52
+ class="resource-link"
53
+ >
54
+ {{ resourceTypeLabel }}:
55
+ </router-link>
56
+ <span
57
+ v-else
58
+ class="resource-text"
59
+ >
60
+ {{ resourceTypeLabel }}:
61
+ </span>
62
+ <span class="resource-name">
63
+ {{ resourceName }}
64
+ </span>
65
+ <BadgeState
66
+ v-if="badge"
67
+ :color="badge.color"
68
+ :label="badge.label"
69
+ />
70
+ </Title>
71
+ <div class="actions">
72
+ <RcButton
73
+ v-if="onShowConfiguration"
74
+ class="show-configuration"
75
+ :primary="true"
76
+ :aria-label="i18n.t('component.resource.detail.titleBar.ariaLabel.showConfiguration', { resource: resourceName })"
77
+ @click="emit('show-configuration')"
78
+ >
79
+ <img
80
+ :src="showConfigurationIcon"
81
+ class="mmr-3"
82
+ >
83
+ {{ i18n.t('component.resource.detail.titleBar.showConfiguration') }}
84
+ </RcButton>
85
+ <ActionMenu
86
+ v-if="actionMenuResource"
87
+ class="title-bar-action-menu"
88
+ button-role="multiAction"
89
+ :resource="actionMenuResource"
90
+ data-testid="masthead-action-menu"
91
+ />
92
+ </div>
93
+ </Top>
94
+ <div
95
+ v-if="description"
96
+ class="bottom description"
97
+ >
98
+ {{ description }}
99
+ </div>
100
+ </div>
101
+ </template>
102
+
103
+ <style lang="scss" scoped>
104
+ .title-bar {
105
+ &:deep() .badge-state {
106
+ font-size: 16px;
107
+ margin-left: 4px;
108
+ top: -4px;
109
+ position: relative;
110
+ }
111
+
112
+ &:deep() button[data-testid="masthead-action-menu"] {
113
+ border-radius: 4px;
114
+ width: 35px;
115
+ height: 40px;
116
+ margin-left: 16px;
117
+
118
+ display: inline-flex;
119
+ flex-direction: row;
120
+ justify-content: center;
121
+ align-items: center;
122
+ }
123
+ }
124
+ </style>
@@ -0,0 +1,34 @@
1
+ <script setup lang="ts">
2
+ </script>
3
+
4
+ <template>
5
+ <div class="resource-detail-page-top">
6
+ <div
7
+ v-if="$slots['header']"
8
+ class="header"
9
+ >
10
+ <slot name="header" />
11
+ </div>
12
+ <div
13
+ v-if="$slots['system-messages']"
14
+ class="system-messages"
15
+ >
16
+ <slot name="system-messages" />
17
+ </div>
18
+ <div
19
+ v-if="$slots['metadata']"
20
+ class="metadata"
21
+ >
22
+ <slot name="metadata" />
23
+ </div>
24
+ <div
25
+ v-if="$slots['extension-content']"
26
+ class="extension-content"
27
+ >
28
+ <slot name="extension-content" />
29
+ </div>
30
+ </div>
31
+ </template>
32
+
33
+ <style lang="scss" scoped>
34
+ </style>
@@ -0,0 +1,32 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import Page from '@shell/components/Resource/Detail/Page.vue';
3
+
4
+ describe('component: ResourceDetailPage', () => {
5
+ it('should render container with resource-detail-class and hide the slots not used', async() => {
6
+ const wrapper = mount(Page);
7
+
8
+ expect(wrapper.find('.resource-detail-page').exists()).toBeTruthy();
9
+
10
+ expect(wrapper.find('.top-area').exists()).toBeFalsy();
11
+ expect(wrapper.find('.middle-area').exists()).toBeFalsy();
12
+ expect(wrapper.find('.bottom-area').exists()).toBeFalsy();
13
+ });
14
+
15
+ it('should render each of the slots with appropriate content', async() => {
16
+ const topArea = 'TOP_AREA';
17
+ const middleArea = 'MIDDLE_AREA';
18
+ const bottomArea = 'BOTTOM_AREA';
19
+
20
+ const wrapper = mount(Page, {
21
+ slots: {
22
+ 'top-area': topArea,
23
+ 'middle-area': middleArea,
24
+ 'bottom-area': bottomArea
25
+ }
26
+ });
27
+
28
+ expect(wrapper.find('.top-area').element.innerHTML.trim()).toStrictEqual(topArea);
29
+ expect(wrapper.find('.middle-area').element.innerHTML.trim()).toStrictEqual(middleArea);
30
+ expect(wrapper.find('.bottom-area').element.innerHTML.trim()).toStrictEqual(bottomArea);
31
+ });
32
+ });