@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
@@ -1,36 +1,48 @@
1
1
  <script>
2
- import { mapState } from 'vuex';
2
+ import { getVersionData } from '@shell/config/version';
3
+ import { mapState, mapGetters } from 'vuex';
4
+ import { isEmpty } from '@shell/utils/object';
3
5
  import { FLEET } from '@shell/config/types';
4
6
  import { WORKSPACE } from '@shell/store/prefs';
5
- import {
6
- getStateLabel,
7
- primaryDisplayStatusFromCount,
8
- STATES,
9
- STATES_ENUM,
10
- } from '@shell/plugins/dashboard-store/resource-class';
11
7
  import Loading from '@shell/components/Loading';
12
- import CollapsibleCard from '@shell/components/CollapsibleCard.vue';
13
- import ResourceTable from '@shell/components/ResourceTable';
14
- import CompoundStatusBadge from '@shell/components/CompoundStatusBadge';
15
8
  import { checkPermissions, checkSchemasForFindAllHash } from '@shell/utils/auth';
16
9
  import { WORKSPACE_ANNOTATION } from '@shell/config/labels-annotations';
17
10
  import { filterBy } from '@shell/utils/array';
18
- import FleetNoWorkspaces from '@shell/components/fleet/FleetNoWorkspaces.vue';
19
- import { NAME } from '@shell/config/product/fleet';
20
- import { xOfy } from '@shell/utils/string';
11
+ import NoWorkspaces from '@shell/components/fleet/FleetNoWorkspaces.vue';
12
+ import { RcButton } from '@components/RcButton';
13
+ import ResourcePanel from '@shell/components/fleet/dashboard/ResourcePanel.vue';
14
+ import ResourceCard from '@shell/components/fleet/dashboard/ResourceCard.vue';
15
+ import ResourceDetails from '@shell/components/fleet/dashboard/ResourceDetails.vue';
16
+ import EmptyDashboard from '@shell/components/fleet/dashboard/Empty.vue';
17
+ import ButtonGroup from '@shell/components/ButtonGroup';
18
+ import Checkbox from '@components/Form/Checkbox/Checkbox.vue';
19
+ import FleetApplications from '@shell/components/fleet/FleetApplications.vue';
20
+ import FleetUtils from '@shell/utils/fleet';
21
+ import Preset from '@shell/mixins/preset';
22
+
23
+ const VIEW_MODE = {
24
+ TABLE: 'flat',
25
+ CARDS: 'cards'
26
+ };
21
27
 
22
28
  export default {
23
29
  name: 'FleetDashboard',
24
30
  components: {
31
+ ButtonGroup,
32
+ Checkbox,
33
+ EmptyDashboard,
34
+ FleetApplications,
25
35
  Loading,
26
- ResourceTable,
27
- CollapsibleCard,
28
- CompoundStatusBadge,
29
- FleetNoWorkspaces
36
+ NoWorkspaces,
37
+ RcButton,
38
+ ResourceCard,
39
+ ResourcePanel,
30
40
  },
31
41
 
42
+ mixins: [Preset],
43
+
32
44
  async fetch() {
33
- const hash = await checkSchemasForFindAllHash({
45
+ const schemas = {
34
46
  fleetWorkspaces: {
35
47
  inStoreType: 'management',
36
48
  type: FLEET.WORKSPACE,
@@ -51,17 +63,37 @@ export default {
51
63
  inStoreType: 'management',
52
64
  type: FLEET.GIT_REPO,
53
65
  },
66
+ helmOps: {
67
+ inStoreType: 'management',
68
+ type: FLEET.HELM_OP,
69
+ },
54
70
  fleetClusters: {
55
71
  inStoreType: 'management',
56
72
  type: FLEET.CLUSTER,
57
73
  }
58
- }, this.$store);
74
+ };
75
+
76
+ const hash = await checkSchemasForFindAllHash(schemas, this.$store);
77
+
78
+ this.fleetWorkspaces = hash.fleetWorkspaces || [];
59
79
 
60
- this.gitRepos = hash.gitRepos;
61
- this.fleetWorkspacesData = hash.fleetWorkspaces || [];
80
+ this[FLEET.GIT_REPO] = hash.gitRepos || [];
81
+ this[FLEET.HELM_OP] = hash.helmOps || [];
62
82
 
63
83
  try {
64
- const permissions = await checkPermissions({ workspaces: { type: FLEET.WORKSPACE }, gitRepos: { type: FLEET.GIT_REPO, schemaValidator: (schema) => schema.resourceMethods.includes('PUT') } }, this.$store.getters);
84
+ const permissionsSchemas = {
85
+ workspaces: { type: FLEET.WORKSPACE },
86
+ gitRepos: {
87
+ type: FLEET.GIT_REPO,
88
+ schemaValidator: (schema) => schema.resourceMethods.includes('PUT')
89
+ },
90
+ helmOps: {
91
+ type: FLEET.HELM_OP,
92
+ schemaValidator: (schema) => schema.resourceMethods.includes('PUT')
93
+ },
94
+ };
95
+
96
+ const permissions = await checkPermissions(permissionsSchemas, this.$store.getters);
65
97
 
66
98
  this.permissions = permissions;
67
99
  } catch (e) {
@@ -71,211 +103,328 @@ export default {
71
103
 
72
104
  data() {
73
105
  return {
74
- admissableAreas: ['clusters', 'bundles', 'resources'],
75
- headers: [
76
- {
77
- name: 'name',
78
- labelKey: 'tableHeaders.repoName',
79
- value: 'nameDisplay',
80
- sort: ['nameSort'],
81
- formatter: 'LinkDetail',
82
- canBeVariable: true,
83
- },
106
+ createRoute: { name: 'c-cluster-fleet-application-create' },
107
+ permissions: {},
108
+ FLEET,
109
+ [FLEET.REPO]: [],
110
+ [FLEET.HELM_OP]: [],
111
+ fleetWorkspaces: [],
112
+ VIEW_MODE,
113
+ viewModeOptions: [
84
114
  {
85
- name: 'clustersReady',
86
- labelKey: 'tableHeaders.clustersReady',
87
- value: 'status.readyClusters',
88
- sort: 'status.readyClusters',
89
- search: false,
115
+ tooltipKey: 'fleet.dashboard.viewMode.table',
116
+ icon: 'icon-list-flat',
117
+ value: VIEW_MODE.TABLE,
90
118
  },
91
119
  {
92
- name: 'bundlesReady',
93
- labelKey: 'tableHeaders.bundlesReady',
94
- value: 'status.readyClusters',
95
- sort: 'status.readyClusters',
96
- search: false,
120
+ tooltipKey: 'fleet.dashboard.viewMode.cards',
121
+ icon: 'icon-apps',
122
+ value: VIEW_MODE.CARDS,
97
123
  },
98
- {
99
- name: 'resourcesReady',
100
- labelKey: 'tableHeaders.resourcesReady',
101
- value: 'status.resourceCounts.ready',
102
- sort: 'status.resourceCounts.ready',
103
- }
104
124
  ],
105
- schema: {},
106
- gitRepos: [],
107
- fleetWorkspacesData: [],
108
- isCollapsed: {},
109
- permissions: {},
110
- getStartedLink: {
111
- name: 'c-cluster-product-resource-create',
112
- params: {
113
- product: NAME,
114
- resource: FLEET.GIT_REPO
115
- },
116
- }
125
+ CARDS_MIN: 50,
126
+ CARDS_SIZE: 50,
127
+ cardsCount: {},
128
+ viewMode: VIEW_MODE.CARDS,
129
+ isWorkspaceCollapsed: {},
130
+ isStateCollapsed: {},
131
+ typeFilter: {},
132
+ stateFilter: {},
133
+ selectedCard: null,
134
+ presetVersion: getVersionData()?.Version,
117
135
  };
118
136
  },
137
+
138
+ created() {
139
+ this.$store.dispatch('showWorkspaceSwitcher', false);
140
+ },
141
+
142
+ mounted() {
143
+ this.preset('cardsCount', 'object');
144
+ this.preset('viewMode', 'string');
145
+ },
146
+
147
+ beforeUnmount() {
148
+ this.$store.dispatch('showWorkspaceSwitcher', true);
149
+ },
150
+
119
151
  computed: {
120
152
  ...mapState(['workspace', 'allNamespaces']),
121
- fleetWorkspaces() {
122
- if (this.fleetWorkspacesData?.length) {
123
- return this.fleetWorkspacesData;
153
+ ...mapGetters({ isOpenSlideInPanel: 'slideInPanel/isOpen' }),
154
+ ...mapGetters({ isClosingSlideInPanel: 'slideInPanel/isClosing' }),
155
+
156
+ repoSchema() {
157
+ return this.$store.getters['management/schemaFor'](FLEET.GIT_REPO);
158
+ },
159
+
160
+ workspaces() {
161
+ if (this.fleetWorkspaces?.length) {
162
+ return this.fleetWorkspaces;
124
163
  }
125
164
 
126
165
  // When user doesn't have access to the workspaces fall back to namespaces
127
166
  return this.allNamespaces.filter((item) => {
128
167
  return item.metadata.annotations[WORKSPACE_ANNOTATION] === WORKSPACE;
129
168
  }).map(( obj ) => {
130
- const repos = filterBy(this.gitRepos, 'metadata.namespace', obj.id);
169
+ const repos = filterBy(this[FLEET.GIT_REPO], 'metadata.namespace', obj.id);
170
+ const helmOps = filterBy(this[FLEET.HELM_OP], 'metadata.namespace', obj.id);
131
171
 
132
172
  return {
133
173
  ...obj,
134
- counts: {
135
- clusters: '-',
136
- clusterGroups: '-',
137
- gitRepos: repos.length
138
- },
139
174
  repos,
140
- nameDisplay: obj.id
175
+ helmOps,
176
+ id: obj.id
141
177
  };
142
178
  });
143
179
  },
144
- workspacesData() {
145
- return this.fleetWorkspaces.filter((ws) => ws.counts.gitRepos > 0);
180
+
181
+ applicationStates() {
182
+ return this._groupByWorkspace((ws) => this._resourceStates([...ws.repos, ...ws.helmOps]));
146
183
  },
147
- emptyWorkspaces() {
148
- return this.fleetWorkspaces.filter((ws) => ws.counts.gitRepos === 0);
184
+
185
+ clusterStates() {
186
+ return this._groupByWorkspace((ws) => this._resourceStates(ws.clusters));
149
187
  },
150
- areAllCardsExpanded() {
151
- return Object.keys(this.isCollapsed).every((key) => !this.isCollapsed[key]);
188
+
189
+ clusterGroupsStates() {
190
+ return this._groupByWorkspace((ws) => this._resourceStates(ws.clusterGroups));
152
191
  },
153
- gitReposCounts() {
154
- return this.gitRepos.reduce((prev, gitRepo) => {
155
- prev[gitRepo.id] = {
156
- bundles: gitRepo.allBundlesStatuses,
157
- resources: gitRepo.allResourceStatuses,
158
- };
159
192
 
160
- return prev;
161
- }, {});
193
+ cardResources() {
194
+ return this._groupByWorkspace((ws) => {
195
+ const filtered = this.applicationStates[ws.id].reduce((acc, state) => ({
196
+ ...acc,
197
+ [state.stateDisplay]: this._filterResources(state),
198
+ }), {});
199
+
200
+ return filtered;
201
+ });
162
202
  },
163
- },
164
- methods: {
165
- setWorkspaceFilterAndLinkToGitRepo(value) {
166
- this.$store.commit('updateWorkspace', { value, getters: this.$store.getters } );
167
- this.$store.dispatch('prefs/set', { key: WORKSPACE, value });
168
-
169
- this.$router.push({
170
- name: 'c-cluster-product-resource',
171
- params: {
172
- product: NAME,
173
- resource: FLEET.GIT_REPO
174
- },
203
+
204
+ tableResources() {
205
+ return this._groupByWorkspace((ws) => {
206
+ const filtered = this.applicationStates[ws.id].reduce((acc, state) => ([
207
+ ...acc,
208
+ ...this._filterResources(state)
209
+ ]), []);
210
+
211
+ return filtered;
175
212
  });
176
213
  },
177
- getStatusInfo(area, row, rowCounts) {
178
- const defaultStatusInfo = {
179
- badgeClass: `${ STATES[STATES_ENUM.NOT_READY].color } badge-class-default`,
180
- icon: STATES[STATES_ENUM.NOT_READY].compoundIcon
181
- };
182
214
 
183
- // classes are defined in the themes SASS files...
184
- return this.getBadgeClassAndIcon(area, row, rowCounts) || defaultStatusInfo;
215
+ isEmptyDashboard() {
216
+ return this[FLEET.GIT_REPO]?.length === 0 && this[FLEET.HELM_OP]?.length === 0;
185
217
  },
186
- getBadgeClassAndIcon(area, row, rowCounts) {
187
- if (!this.admissableAreas.includes(area)) {
188
- return false;
189
- }
190
218
 
191
- let group;
219
+ allCardsExpanded() {
220
+ return Object.keys(this.isWorkspaceCollapsed).every((key) => !this.isWorkspaceCollapsed[key]);
221
+ },
222
+ },
192
223
 
193
- if (area === 'clusters') {
194
- const clusterInfo = row.clusterInfo;
195
- const state = clusterInfo.ready === clusterInfo.total ? STATES_ENUM.ACTIVE : STATES_ENUM.NOT_READY;
224
+ methods: {
225
+ selectStates(workspace, state) {
226
+ this._checkInit(workspace, 'stateFilter');
196
227
 
197
- return {
198
- badgeClass: `${ STATES[state].color } badge-class-area-${ area }`,
199
- icon: STATES[state].compoundIcon
200
- };
201
- } else if (area === 'bundles') {
202
- group = rowCounts[row.id].bundles;
203
- } else if (area === 'resources') {
204
- group = rowCounts[row.id].resources;
228
+ this._cleanStateFilter(workspace);
229
+
230
+ if (this.stateFilter[workspace][state]) {
231
+ delete this.stateFilter[workspace][state];
205
232
  } else {
206
- // unreachable
207
- return false;
233
+ this.stateFilter[workspace][state] = true;
208
234
  }
209
235
 
210
- if (group.total === group.states.ready) {
211
- return {
212
- badgeClass: STATES[STATES_ENUM.ACTIVE].color,
213
- icon: STATES[STATES_ENUM.ACTIVE].compoundIcon,
214
- };
236
+ if (this.isWorkspaceCollapsed[workspace]) {
237
+ this.toggleCard(workspace);
215
238
  }
216
- const state = primaryDisplayStatusFromCount(group.states);
217
239
 
218
- return {
219
- badgeClass: STATES[state].color ? STATES[state].color : `${ STATES[STATES_ENUM.UNKNOWN].color } bg-unmapped-state`,
220
- icon: STATES[state].compoundIcon ? STATES[state].compoundIcon : `${ STATES[STATES_ENUM.UNKNOWN].compoundIcon } unmapped-icon`
221
- };
240
+ this.$nextTick(() => {
241
+ this.toggleStateAll(workspace, 'expand');
242
+ });
222
243
  },
223
- getTooltipInfo(area, row, rowCounts) {
224
- if (!this.admissableAreas.includes(area)) {
225
- return {};
226
- }
227
244
 
228
- if (area === 'bundles') {
229
- return this.generateTooltipData(rowCounts[row.id].bundles.states);
230
- } else if (area === 'resources') {
231
- return this.generateTooltipData(rowCounts[row.id].resources.states);
232
- }
245
+ selectType(workspace, type, value) {
246
+ this._checkInit(workspace, 'typeFilter');
247
+
248
+ this.typeFilter[workspace][type] = value;
249
+
250
+ this.toggleStateAll(workspace, 'expand');
251
+ },
233
252
 
234
- return '';
253
+ toggleCard(key) {
254
+ this.isWorkspaceCollapsed[key] = !this.isWorkspaceCollapsed[key];
235
255
  },
236
- generateTooltipData(infoObj) {
237
- return Object.keys(infoObj)
238
- .filter((key) => infoObj[key] > 0) // filter zero values
239
- .map((key) => `${ getStateLabel(key) }: ${ infoObj[key] }<br>`).join('');
256
+
257
+ toggleCardAll(action) {
258
+ const val = action !== 'expand';
259
+
260
+ Object.keys(this.isWorkspaceCollapsed).forEach((key) => {
261
+ this.isWorkspaceCollapsed[key] = val;
262
+ });
263
+ },
264
+
265
+ toggleState(workspace, state) {
266
+ this._checkInit(workspace, 'isStateCollapsed');
267
+
268
+ this.isStateCollapsed[workspace][state] = !this.isStateCollapsed[workspace][state];
269
+ },
270
+
271
+ toggleStateAll(workspace, action) {
272
+ const val = action !== 'expand';
273
+
274
+ Object.keys(this.isStateCollapsed[workspace] || []).forEach((state) => {
275
+ this.isStateCollapsed[workspace][state] = val;
276
+ });
277
+ },
278
+
279
+ loadMore(workspace, state) {
280
+ this._checkInit(workspace, 'cardsCount');
281
+
282
+ const count = this.cardsCount[workspace][state] || this.CARDS_MIN;
283
+
284
+ const val = count + this.CARDS_SIZE;
285
+
286
+ this.cardsCount[workspace][state] = val;
287
+ },
288
+
289
+ loadLess(workspace, state) {
290
+ this._checkInit(workspace, 'cardsCount');
291
+
292
+ const count = this.cardsCount[workspace][state] || this.CARDS_MIN;
293
+
294
+ const val = count - this.CARDS_MIN < 0 ? this.CARDS_MIN : count - this.CARDS_SIZE;
295
+
296
+ this.cardsCount[workspace][state] = val;
240
297
  },
241
- getBadgeValue(area, row, rowCounts) {
242
- let value;
243
298
 
244
- if (!this.admissableAreas.includes(area)) {
245
- return 'N/A';
299
+ showResourceDetails(value, statePanel, workspace, selected) {
300
+ if (this.isClosingSlideInPanel) {
301
+ return;
246
302
  }
247
303
 
248
- if (area === 'clusters') {
249
- value = `${ row.clusterInfo.ready }/${ row.clusterInfo.total }`;
250
- } else if (area === 'bundles') {
251
- const bundles = rowCounts[row.id].bundles;
304
+ this.selectedCard = selected;
305
+
306
+ this.$shell.slideInPanel({
307
+ component: ResourceDetails,
308
+ componentProps: {
309
+ value,
310
+ statePanel,
311
+ workspace,
312
+ showHeader: false,
313
+ width: window.innerWidth / 3 > 530 ? `${ window.innerWidth / 3 }px` : '530px',
314
+ zIndex: 1,
315
+ triggerFocusTrap: true,
316
+ returnFocusSelector: `[data-testid="resource-card-${ value.id }"]`
317
+ }
318
+ });
319
+ },
252
320
 
253
- value = xOfy(bundles.states.ready || 0, bundles.total);
254
- } else if (area === 'resources') {
255
- const resources = rowCounts[row.id].resources;
321
+ _resourceStates(resources) {
322
+ const out = [];
323
+
324
+ resources.forEach((obj) => {
325
+ const {
326
+ stateDisplay,
327
+ stateSort
328
+ } = obj;
329
+
330
+ const exists = out.find((s) => s.stateDisplay === stateDisplay);
331
+
332
+ if (exists) {
333
+ exists.resources.push(obj);
334
+ } else {
335
+ out.push({
336
+ stateDisplay,
337
+ stateSort,
338
+ statePanel: FleetUtils.getDashboardState(obj),
339
+ resources: [obj]
340
+ });
341
+ }
342
+ });
256
343
 
257
- value = xOfy(resources.states.ready || 0, resources.total);
344
+ return out.sort((a, b) => a.stateSort.localeCompare(b.stateSort));
345
+ },
346
+
347
+ _filterResources(state) {
348
+ return state.resources.filter((item) => this._decodeTypeFilter(item.namespace, item.type) &&
349
+ this._decodeStateFilter(item.namespace, state)
350
+ );
351
+ },
352
+
353
+ _groupByWorkspace(callback) {
354
+ return this.workspaces.reduce((acc, ws) => ({
355
+ ...acc,
356
+ [ws.id]: callback(ws)
357
+ }), {});
358
+ },
359
+
360
+ _stateExistsInWorkspace(workspace, state) {
361
+ return !!this.applicationStates[workspace].find((s) => s.statePanel.id === state);
362
+ },
363
+
364
+ _decodeStateFilter(workspace, state) {
365
+ const stateFilter = Object.keys(this.stateFilter[workspace] || {});
366
+
367
+ if (stateFilter.length === 0) {
368
+ return true;
369
+ }
370
+
371
+ if (stateFilter.filter((key) => this.stateFilter[workspace][key] && this._stateExistsInWorkspace(workspace, key)).length === 0) {
372
+ return true;
373
+ }
374
+
375
+ if (this.stateFilter[workspace][state.statePanel.id]) {
376
+ return true;
258
377
  }
259
378
 
260
- return value;
379
+ return false;
261
380
  },
262
- toggleCollapse(val, key) {
263
- this.isCollapsed[key] = val;
381
+
382
+ _decodeTypeFilter(workspace, type) {
383
+ const emptyFilter = isEmpty(this.typeFilter) || !this.viewMode;
384
+
385
+ return emptyFilter || this.typeFilter[workspace]?.[type];
264
386
  },
265
- toggleAll(action) {
266
- const val = action !== 'expand';
267
387
 
268
- Object.keys(this.isCollapsed).forEach((key) => {
269
- this.isCollapsed[key] = val;
388
+ _cleanStateFilter(workspace) {
389
+ const all = [...Object.keys(this.stateFilter[workspace] || {})];
390
+
391
+ all.forEach((state) => {
392
+ const exists = this._stateExistsInWorkspace(workspace, state);
393
+
394
+ if (!exists) {
395
+ delete this.stateFilter[workspace][state];
396
+ }
270
397
  });
271
- }
398
+ },
399
+
400
+ _checkInit(workspace, name) {
401
+ if (!this[name][workspace]) {
402
+ this[name][workspace] = {};
403
+ }
404
+ },
272
405
  },
273
406
 
274
407
  watch: {
275
- fleetWorkspaces(value) {
276
- value?.filter((ws) => ws.repos?.length).forEach((ws) => {
277
- this.isCollapsed[ws.id] = false;
278
- });
408
+ workspaces(neu) {
409
+ if (neu) {
410
+ neu?.forEach((ws) => {
411
+ this.isWorkspaceCollapsed[ws.id] = neu.length > 1;
412
+
413
+ this.isStateCollapsed[ws.id] = { Active: true };
414
+
415
+ this.typeFilter[ws.id] = {
416
+ [FLEET.GIT_REPO]: true,
417
+ [FLEET.HELM_OP]: true,
418
+ };
419
+
420
+ this.stateFilter[ws.id] = {};
421
+ });
422
+
423
+ this.preset('isWorkspaceCollapsed', 'object');
424
+ this.preset('isStateCollapsed', 'object');
425
+ this.preset('typeFilter', 'object');
426
+ this.preset('stateFilter', 'object');
427
+ }
279
428
  }
280
429
  }
281
430
  };
@@ -284,223 +433,516 @@ export default {
284
433
  <template>
285
434
  <div>
286
435
  <Loading v-if="$fetchState.pending" />
287
- <!-- no git repos -->
288
- <FleetNoWorkspaces
289
- v-else-if="!fleetWorkspacesData.length"
436
+ <NoWorkspaces
437
+ v-else-if="!workspaces?.length"
290
438
  :can-view="permissions.workspaces"
291
439
  />
292
- <div
293
- v-else-if="!gitRepos.length"
294
- class="fleet-empty-dashboard"
295
- >
296
- <i class="icon-fleet mb-30" />
297
- <h1>{{ t('fleet.dashboard.welcome') }}</h1>
298
- <p class="mb-30">
299
- <span>{{ t('fleet.dashboard.gitOpsScale') }}</span>
300
- <a
301
- :href="t('fleet.dashboard.learnMoreLink')"
302
- target="_blank"
303
- rel="noopener noreferrer nofollow"
304
- >
305
- {{ t('fleet.dashboard.learnMore') }} <i class="icon icon-external-link" />
306
- </a>
307
- </p>
308
- <template v-if="permissions.gitRepos">
309
- <h3 class="mb-30">
310
- {{ t('fleet.dashboard.noRepo', null, true) }}
311
- </h3>
312
- <router-link
313
- :to="getStartedLink"
314
- class="btn role-secondary"
315
- >
316
- {{ t('fleet.dashboard.getStarted') }}
317
- </router-link>
318
- </template>
319
- </div>
320
- <!-- fleet dashboard with repos -->
440
+ <EmptyDashboard
441
+ v-else-if="isEmptyDashboard"
442
+ :permissions="permissions"
443
+ />
321
444
  <div
322
445
  v-else
323
- class="fleet-dashboard-data"
446
+ class="dashboard"
447
+ :data-testid="'fleet-dashboard-workspace-cards'"
324
448
  >
325
- <div class="title">
449
+ <div class="dashboard-header">
326
450
  <h1>
327
451
  <t k="fleet.dashboard.pageTitle" />
328
452
  </h1>
329
- <div>
330
- <p
331
- v-if="areAllCardsExpanded"
332
- @click="toggleAll('collapse')"
333
- >
334
- {{ t('fleet.dashboard.collapseAll') }}
335
- </p>
336
- <p
337
- v-else
338
- @click="toggleAll('expand')"
339
- >
340
- {{ t('fleet.dashboard.expandAll') }}
341
- </p>
453
+
454
+ <div class="dashboard-main-actions">
455
+ <div :data-testid="'fleet-dashboard-expand-all'">
456
+ <p
457
+ v-if="allCardsExpanded"
458
+ @click="toggleCardAll('collapse')"
459
+ >
460
+ {{ t('fleet.dashboard.collapseAll') }}
461
+ </p>
462
+ <p
463
+ v-else
464
+ @click="toggleCardAll('expand')"
465
+ >
466
+ {{ t('fleet.dashboard.expandAll') }}
467
+ </p>
468
+ </div>
469
+ <ButtonGroup
470
+ :data-testid="'view-button'"
471
+ :value="viewMode"
472
+ :options="viewModeOptions"
473
+ @update:value="viewMode = $event"
474
+ />
342
475
  </div>
343
476
  </div>
344
477
  <div
345
- v-if="emptyWorkspaces.length"
346
- class="title-footnote"
347
- >
348
- <p>{{ t('fleet.dashboard.thereIsMore', { count: emptyWorkspaces.length }) }}:&nbsp;</p>
349
- <p
350
- v-for="(ews, i) in emptyWorkspaces"
351
- :key="i"
352
- >
353
- {{ ews.nameDisplay }}<span v-if="i != (emptyWorkspaces.length - 1)">,&nbsp;</span>
354
- </p>
355
- </div>
356
- <CollapsibleCard
357
- v-for="(ws, i) in workspacesData"
478
+ v-for="(workspace, i) in workspaces"
358
479
  :key="i"
359
- class="mt-20 mb-40"
360
- :title="`${t('resourceDetail.masthead.workspace')}: ${ws.nameDisplay}`"
361
- :is-collapsed="isCollapsed[ws.id]"
362
- :is-title-clickable="true"
363
- :data-testid="`collapsible-card-${ ws.id }`"
364
- @toggleCollapse="toggleCollapse($event, ws.id)"
365
- @titleClick="setWorkspaceFilterAndLinkToGitRepo(ws.id)"
480
+ class="card-container m-0 mt-20"
481
+ :data-testid="`fleet-dashboard-workspace-card-${ workspace.id }`"
482
+ :show-actions="false"
483
+ :show-separator="false"
484
+ :show-highlight-border="false"
366
485
  >
367
- <template v-slot:header-right>
368
- <div class="header-icons">
369
- <p>
370
- <i class="icon icon-repository" />
371
- <span>{{ t('tableHeaders.repositories') }}: <span>{{ ws.counts.gitRepos }}</span></span>
372
- </p>
373
- <p>
374
- <i class="icon icon-storage" />
375
- <span>{{ t('tableHeaders.clusters') }}: <span>{{ ws.counts.clusters }}</span></span>
376
- </p>
377
- <p>
378
- <i class="icon icon-folder" />
379
- <span>{{ t('tableHeaders.clusterGroups') }}: <span>{{ ws.counts.clusterGroups }}</span></span>
380
- </p>
381
- </div>
382
- </template>
383
- <template v-slot:content>
384
- <ResourceTable
385
- :schema="schema"
386
- :headers="headers"
387
- :rows="ws.repos"
388
- key-field="_key"
389
- :search="false"
390
- :table-actions="false"
486
+ <div class="card-panel-main">
487
+ <div
488
+ class="card-panel-main-details"
489
+ :class="{ expand: !isWorkspaceCollapsed[workspace.id] }"
391
490
  >
392
- <template #cell:clustersReady="{row}">
393
- <span v-if="ws.type === 'namespace'"> - </span>
394
- <CompoundStatusBadge
395
- v-else
396
- data-testid="clusters-ready"
397
- :tooltip-text="getTooltipInfo('clusters', row, gitReposCounts)"
398
- :badge-class="getStatusInfo('clusters', row, gitReposCounts).badgeClass"
399
- :icon="getStatusInfo('clusters', row, gitReposCounts).icon"
400
- :value="getBadgeValue('clusters', row, gitReposCounts)"
491
+ <div class="title">
492
+ <h3 class="label label-secondary">
493
+ <i class="icon icon-folder" />
494
+ <span>{{ t('fleet.dashboard.workspace') }} : &nbsp;</span>
495
+ </h3>
496
+ <router-link
497
+ class="name"
498
+ role="link"
499
+ tabindex="0"
500
+ :aria-label="workspace.nameDisplay"
501
+ :to="workspace.detailLocation || {}"
502
+ >
503
+ {{ workspace.nameDisplay }}
504
+ </router-link>
505
+ </div>
506
+ <div class="body">
507
+ <ResourcePanel
508
+ v-if="workspace.repos?.length || workspace.helmOps?.length"
509
+ :data-testid="'resource-panel-applications'"
510
+ :states="applicationStates[workspace.id]"
511
+ :workspace="workspace.id"
512
+ :type="FLEET.APPLICATION"
513
+ :selected-states="stateFilter[workspace.id] || {}"
514
+ @click:state="selectStates(workspace.id, $event)"
401
515
  />
402
- </template>
403
- <template #cell:bundlesReady="{row}">
404
- <span v-if="ws.type === 'namespace'"> - </span>
405
- <CompoundStatusBadge
406
- v-else
407
- data-testid="bundles-ready"
408
- :tooltip-text="getTooltipInfo('bundles', row, gitReposCounts)"
409
- :badge-class="getStatusInfo('bundles', row, gitReposCounts).badgeClass"
410
- :icon="getStatusInfo('bundles', row, gitReposCounts).icon"
411
- :value="getBadgeValue('bundles', row, gitReposCounts)"
516
+ <ResourcePanel
517
+ v-if="workspace.clusters?.length"
518
+ :data-testid="'resource-panel-clusters'"
519
+ :states="clusterStates[workspace.id]"
520
+ :workspace="workspace.id"
521
+ :type="FLEET.CLUSTER"
522
+ :selectable="false"
412
523
  />
413
- </template>
414
- <template #cell:resourcesReady="{row}">
415
- <CompoundStatusBadge
416
- data-testid="resources-ready"
417
- :tooltip-text="getTooltipInfo('resources', row, gitReposCounts)"
418
- :badge-class="getStatusInfo('resources', row, gitReposCounts).badgeClass"
419
- :icon="getStatusInfo('resources', row, gitReposCounts).icon"
420
- :value="getBadgeValue('resources', row, gitReposCounts)"
524
+ <ResourcePanel
525
+ v-if="workspace.clusterGroups?.length"
526
+ :data-testid="'resource-panel-cluster-groups'"
527
+ :states="clusterGroupsStates[workspace.id]"
528
+ :workspace="workspace.id"
529
+ :type="FLEET.CLUSTER_GROUP"
530
+ :show-chart="false"
531
+ :selectable="false"
421
532
  />
422
- </template>
423
-
424
- <template #cell:target="{row}">
425
- {{ row.targetInfo.modeDisplay }}
426
- </template>
427
- </ResourceTable>
428
- </template>
429
- </CollapsibleCard>
533
+ </div>
534
+ </div>
535
+ <div class="card-panel-main-actions">
536
+ <div
537
+ v-if="workspace.repos?.length || workspace.helmOps?.length"
538
+ class="expand-button"
539
+ :data-testid="'expand-button'"
540
+ >
541
+ <RcButton
542
+ small
543
+ ghost
544
+ :aria-label="`workspace-expand-btn-${ workspace.id }`"
545
+ @click="toggleCard(workspace.id)"
546
+ >
547
+ <i
548
+ :class="{
549
+ ['icon icon-lg icon-chevron-right']: isWorkspaceCollapsed[workspace.id],
550
+ ['icon icon-lg icon-chevron-down']: !isWorkspaceCollapsed[workspace.id],
551
+ }"
552
+ />
553
+ </RcButton>
554
+ </div>
555
+ </div>
556
+ </div>
557
+ <div
558
+ v-if="!isWorkspaceCollapsed[workspace.id] && (workspace.repos?.length || workspace.helmOps?.length)"
559
+ class="panel-expand mt-10"
560
+ :data-testid="`fleet-dashboard-expanded-panel-${ workspace.id }`"
561
+ >
562
+ <div class="actions">
563
+ <div class="type-filters">
564
+ <Checkbox
565
+ :data-testid="'fleet-dashboard-filter-git-repos'"
566
+ :value="typeFilter[workspace.id]?.[FLEET.GIT_REPO]"
567
+ @update:value="selectType(workspace.id, FLEET.GIT_REPO, $event)"
568
+ >
569
+ <template #label>
570
+ <i class="icon icon-lg icon-git mr-5" />
571
+ <span class="label">{{ t('fleet.dashboard.cards.filters.gitRepos') }}</span>
572
+ </template>
573
+ </Checkbox>
574
+ <Checkbox
575
+ :data-testid="'fleet-dashboard-filter-helm-ops'"
576
+ :value="typeFilter[workspace.id]?.[FLEET.HELM_OP]"
577
+ @update:value="selectType(workspace.id, FLEET.HELM_OP, $event)"
578
+ >
579
+ <template #label>
580
+ <i class="icon icon-lg icon-helm mr-5" />
581
+ <span class="label">{{ t('fleet.dashboard.cards.filters.helmOps') }}</span>
582
+ </template>
583
+ </Checkbox>
584
+ </div>
585
+ <div
586
+ v-if="permissions.gitRepos || permissions.helmOps"
587
+ class="create-button"
588
+ >
589
+ <router-link
590
+ :to="createRoute"
591
+ class="btn role-primary"
592
+ >
593
+ {{ t('fleet.application.intro.add') }}
594
+ </router-link>
595
+ </div>
596
+ </div>
597
+ <div
598
+ v-if="viewMode === 'cards'"
599
+ class="cards-panel"
600
+ >
601
+ <div
602
+ v-for="(state, j) in applicationStates[workspace.id]"
603
+ :key="j"
604
+ :data-testid="`state-panel-${ state.stateDisplay }`"
605
+ >
606
+ <div
607
+ v-if="cardResources[workspace.id][state.stateDisplay]?.length"
608
+ class="card-panel"
609
+ >
610
+ <div
611
+ role="button"
612
+ tabindex="0"
613
+ class="title"
614
+ :aria-label="`state-expand-btn-${ state.stateDisplay }`"
615
+ @click="toggleState(workspace.id, state.stateDisplay)"
616
+ @keydown.space.enter.stop.prevent="toggleState(workspace.id, state.stateDisplay)"
617
+ >
618
+ <i
619
+ :class="{
620
+ ['icon icon-chevron-right']: isStateCollapsed[workspace.id]?.[state.stateDisplay],
621
+ ['icon icon-chevron-down']: !isStateCollapsed[workspace.id]?.[state.stateDisplay],
622
+ }"
623
+ />
624
+ <i
625
+ v-if="state.statePanel.id !== 'success'"
626
+ class="ml-5 state-icon"
627
+ :class="state.statePanel.icon"
628
+ :style="{ color: state.statePanel.color }"
629
+ />
630
+ <div class="label">
631
+ <span class="partial">
632
+ {{ state.stateDisplay }}&nbsp;&nbsp;{{ cardResources[workspace.id]?.[state.stateDisplay]?.length }}
633
+ </span>
634
+ <span class="total label-secondary">/{{ [ ...workspace.repos, ...workspace.helmOps ].length }}</span>
635
+ </div>
636
+ </div>
637
+ <div
638
+ v-if="!isStateCollapsed[workspace.id]?.[state.stateDisplay]"
639
+ class="card-panel-body"
640
+ >
641
+ <div class="resource-cards-container">
642
+ <div
643
+ v-for="(item, y) in cardResources[workspace.id][state.stateDisplay]"
644
+ :key="y"
645
+ class="resource-card"
646
+ :class="{
647
+ ['selected']: selectedCard === `${ item.id }-${ y }` && isOpenSlideInPanel
648
+ }"
649
+ :data-testid="`card-${ item.id }`"
650
+ >
651
+ <ResourceCard
652
+ v-if="y < (cardsCount[workspace.id]?.[state.stateDisplay] || CARDS_MIN)"
653
+ role="button"
654
+ tabindex="0"
655
+ :aria-label="`resource-card-${ item.id }`"
656
+ :data-testid="`resource-card-${ item.id }`"
657
+ :value="item"
658
+ :state-panel="state.statePanel"
659
+ @click="showResourceDetails(item, state.statePanel, workspace, `${ item.id }-${ y }`)"
660
+ />
661
+ </div>
662
+ </div>
663
+ <div class="resource-cards-action">
664
+ <p
665
+ v-if="(cardsCount[workspace.id]?.[state.stateDisplay] || 0) > CARDS_MIN"
666
+ @click="loadLess(workspace.id, state.stateDisplay)"
667
+ >
668
+ {{ t('generic.showLess') }}
669
+ </p>
670
+ <div />
671
+ <p
672
+ v-if="cardResources[workspace.id][state.stateDisplay]?.length > (cardsCount[workspace.id]?.[state.stateDisplay] || CARDS_MIN)"
673
+ @click="loadMore(workspace.id, state.stateDisplay)"
674
+ >
675
+ {{ t('generic.showMore') }}
676
+ </p>
677
+ </div>
678
+ </div>
679
+ </div>
680
+ </div>
681
+ </div>
682
+ <div
683
+ v-if="viewMode === VIEW_MODE.TABLE"
684
+ class="table-panel"
685
+ >
686
+ <FleetApplications
687
+ :workspace="workspace.id"
688
+ :rows="tableResources[workspace.id]"
689
+ :schema="{
690
+ id: FLEET.APPLICATION,
691
+ type: 'schema'
692
+ }"
693
+ :loading="$fetchState.pending"
694
+ :use-query-params-for-simple-filtering="true"
695
+ :show-intro="false"
696
+ />
697
+ </div>
698
+ </div>
699
+ </div>
430
700
  </div>
431
701
  </div>
432
702
  </template>
433
703
 
434
704
  <style lang="scss" scoped>
435
- .fleet-empty-dashboard {
436
- flex: 1;
705
+
706
+ .dashboard-main-actions {
437
707
  display: flex;
438
708
  align-items: center;
439
- justify-content: center;
440
- flex-direction: column;
441
- min-height: 100%;
709
+ justify-content: end;
710
+ gap: 15px;
711
+ }
442
712
 
443
- .icon-fleet {
444
- font-size: 100px;
445
- color: var(--disabled-text);
713
+ .dashboard-header {
714
+ display: flex;
715
+ align-items: center;
716
+ justify-content: space-between;
717
+
718
+ h1 {
719
+ margin: 0;
446
720
  }
447
721
 
448
- > p > span {
449
- color: var(--disabled-text);
722
+ > div {
723
+ display: flex;
724
+ align-items: center;
725
+
726
+ i {
727
+ color: var(--primary);
728
+ }
450
729
  }
451
730
  }
452
731
 
453
- .fleet-dashboard-data {
454
- .title {
732
+ .card-container {
733
+ display: flex;
734
+ flex-direction: column;
735
+ border: 1px solid var(--border);
736
+ border-radius: 16px;
737
+ background-color: var(--body-bg);
738
+ box-shadow: none;
739
+ min-width: 500px;
740
+ padding: 16px;
741
+
742
+ :focus-visible {
743
+ @include focus-outline;
744
+ }
745
+
746
+ .card-panel-main {
455
747
  display: flex;
456
748
  align-items: center;
457
749
  justify-content: space-between;
458
- min-height: 48px;
750
+ margin-top: auto;
751
+ margin-bottom: auto;
459
752
 
460
- > div {
753
+ .card-panel-main-details {
461
754
  display: flex;
462
755
  align-items: center;
463
756
 
464
- p{
465
- color: var(--primary);
757
+ .title {
758
+ margin: 0 20px 0 0;
759
+
760
+ .name {
761
+ font-size: 25px;
762
+ }
763
+
764
+ .label {
765
+ display: flex;
766
+ align-items: center;
767
+ min-width: 150px;
768
+ margin: 0 0 5px 0;
769
+
770
+ .icon {
771
+ margin-right: 5px;
772
+ }
773
+ }
774
+ }
775
+
776
+ .body {
777
+ display: flex;
778
+ justify-content: flex-start;
779
+ flex-wrap: wrap;
780
+ gap: 15px;
781
+
782
+ .spacer {
783
+ border-left: 1px solid var(--border);
784
+ }
785
+ }
786
+ }
787
+
788
+ .card-panel-main-actions {
789
+ display: flex;
790
+ flex-direction: column;
791
+ align-items: end;
792
+
793
+ .expand-button {
794
+ display: flex;
795
+ align-items: center;
466
796
 
467
797
  &:hover {
468
- text-decoration: underline;
469
798
  cursor: pointer;
470
799
  }
471
800
  }
472
801
  }
473
802
  }
474
803
 
475
- .title-footnote {
476
- display: flex;
477
- align-items: center;
478
- color: var(--darker);
479
- }
804
+ .panel-expand {
805
+ animation: slideInOut 0.5s ease-in-out;
480
806
 
481
- .header-icons {
482
- display: flex;
483
- align-items: center;
807
+ :focus-visible {
808
+ @include focus-outline;
809
+ }
484
810
 
485
- p {
486
- margin-right: 30px;
811
+ .actions {
487
812
  display: flex;
488
813
  align-items: center;
814
+ justify-content: space-between;
489
815
 
490
- > span {
491
- color: var(--disabled-text);
816
+ .type-filters {
817
+ display: flex;
818
+ flex-direction: column;
819
+ margin-top: 5px;
492
820
 
493
- > span {
494
- color: var(--body-text);
821
+ .checkbox-outer-container {
822
+ width: fit-content;
823
+ }
824
+
825
+ .label {
826
+ margin-top: 2px;
827
+ line-height: 20px;
828
+ }
829
+
830
+ .icon {
831
+ padding: 2px;
832
+ font-size: 25px;
495
833
  }
496
834
  }
835
+ }
836
+
837
+ .cards-panel {
838
+ .card-panel {
839
+ margin-top: 32px;
497
840
 
498
- i {
499
- color: var(--disabled-text);
500
- font-size: 20px;
501
- margin-right: 10px;
841
+ .title {
842
+ display: flex;
843
+ align-items: center;
844
+ cursor: pointer;
845
+ width: fit-content;
846
+ margin-bottom: 16px;
847
+
848
+ .icon {
849
+ margin-right: 5px;
850
+ }
851
+
852
+ .label {
853
+ display: flex;
854
+ align-items: baseline;
855
+ margin-left: 2px;
856
+
857
+ .partial {
858
+ margin: 0;
859
+ margin-right: 2px;
860
+ font-size: 22px;
861
+ }
862
+
863
+ p {
864
+ font-size: small;
865
+
866
+ .icon {
867
+ line-height: -1px;
868
+ }
869
+ }
870
+ }
871
+
872
+ .state-icon {
873
+ font-size: 1.75em;
874
+ }
875
+ }
876
+
877
+ .card-panel-body {
878
+ .resource-cards-container {
879
+ display: grid;
880
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
881
+ gap: 16px;
882
+ min-height: 100%;
883
+
884
+ .resource-card {
885
+ cursor: pointer;
886
+
887
+ &.selected {
888
+ .dashboard-resource-card {
889
+ border: 2px solid var(--primary);
890
+ margin: 0;
891
+ }
892
+ }
893
+ }
894
+ }
895
+
896
+ .resource-cards-action {
897
+ display: flex;
898
+ justify-content: space-between;
899
+
900
+ p {
901
+ width: fit-content;
902
+ margin-left: 15px;
903
+ }
904
+ }
905
+ }
502
906
  }
503
907
  }
908
+
909
+ .table-panel {
910
+ margin-top: 20px;
911
+ }
912
+ }
913
+ }
914
+
915
+ p {
916
+ color: var(--primary);
917
+ margin-right: 2px;
918
+
919
+ &:hover {
920
+ text-decoration: underline;
921
+ cursor: pointer;
922
+ }
923
+ }
924
+
925
+ .label-secondary{
926
+ color: var(--label-secondary);
927
+ }
928
+
929
+ @keyframes slideInOut {
930
+ 0% {
931
+ opacity: 0;
932
+ visibility: hidden;
933
+ transform: translateY(-10px);
934
+ }
935
+
936
+ 50% {
937
+ opacity: 0.5;
938
+ visibility: visible;
939
+ transform: translateY(0);
940
+ }
941
+
942
+ 100% {
943
+ opacity: 1;
944
+ visibility: visible;
945
+ transform: translateY(0);
504
946
  }
505
947
  }
506
948
  </style>