@rancher/shell 0.3.23 → 0.3.25

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 (149) hide show
  1. package/assets/styles/base/_variables.scss +1 -0
  2. package/assets/styles/themes/_dark.scss +1 -0
  3. package/assets/styles/themes/_light.scss +6 -5
  4. package/assets/translations/en-us.yaml +44 -17
  5. package/assets/translations/zh-hans.yaml +2 -2
  6. package/components/ClusterIconMenu.vue +143 -0
  7. package/components/CruResource.vue +7 -1
  8. package/components/ExplorerProjectsNamespaces.vue +11 -1
  9. package/components/FixedBanner.vue +17 -1
  10. package/components/Loading.vue +1 -1
  11. package/components/Markdown.vue +1 -1
  12. package/components/Questions/__tests__/Yaml.test.ts +3 -2
  13. package/components/SideNav.vue +1 -1
  14. package/components/SortableTable/index.vue +3 -2
  15. package/components/auth/RoleDetailEdit.vue +15 -2
  16. package/components/auth/login/saml.vue +12 -1
  17. package/components/form/LabeledSelect.vue +12 -5
  18. package/components/form/Members/ClusterPermissionsEditor.vue +1 -1
  19. package/components/form/Members/MembershipEditor.vue +6 -1
  20. package/components/form/SelectOrCreateAuthSecret.vue +7 -0
  21. package/components/form/__tests__/KeyValue.test.ts +6 -3
  22. package/components/form/__tests__/LabeledSelect.test.ts +18 -0
  23. package/components/formatter/PodsUsage.vue +11 -36
  24. package/components/formatter/PrincipalGroupBindings.vue +8 -5
  25. package/components/formatter/__tests__/PodsUsage.test.ts +36 -19
  26. package/components/nav/Group.vue +62 -34
  27. package/components/nav/Header.vue +13 -6
  28. package/components/nav/Pinned.vue +47 -0
  29. package/components/nav/TopLevelMenu.vue +673 -325
  30. package/components/nav/Type.vue +88 -8
  31. package/config/home-links.js +1 -1
  32. package/config/product/istio.js +15 -5
  33. package/config/router.js +3 -9
  34. package/config/table-headers.js +5 -6
  35. package/config/uiplugins.js +1 -0
  36. package/core/plugin-helpers.js +3 -0
  37. package/core/types.ts +6 -1
  38. package/creators/app/files/.vscode/settings.json +0 -1
  39. package/creators/pkg/init +2 -2
  40. package/detail/__tests__/autoscaling.horizontalpodautoscaler.test.ts +118 -0
  41. package/detail/autoscaling.horizontalpodautoscaler/index.vue +4 -4
  42. package/detail/provisioning.cattle.io.cluster.vue +7 -5
  43. package/edit/__tests__/management.cattle.io.clusterroletemplatebinding.test.ts +58 -0
  44. package/edit/__tests__/namespace.test.ts +5 -3
  45. package/edit/fleet.cattle.io.gitrepo.vue +43 -15
  46. package/edit/logging.banzaicloud.io.output/index.vue +7 -0
  47. package/edit/management.cattle.io.clusterroletemplatebinding.vue +3 -11
  48. package/edit/namespace.vue +8 -4
  49. package/edit/provisioning.cattle.io.cluster/Basics.vue +662 -0
  50. package/edit/provisioning.cattle.io.cluster/CustomCommand.vue +9 -8
  51. package/edit/provisioning.cattle.io.cluster/DrainOptions.vue +13 -8
  52. package/edit/provisioning.cattle.io.cluster/MachinePool.vue +11 -2
  53. package/edit/provisioning.cattle.io.cluster/MemberRoles.vue +40 -0
  54. package/edit/provisioning.cattle.io.cluster/__tests__/Basics.tests.ts +237 -0
  55. package/edit/provisioning.cattle.io.cluster/__tests__/CustomCommand.tests.ts +71 -23
  56. package/edit/provisioning.cattle.io.cluster/__tests__/DrainOptions.test.ts +52 -0
  57. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +65 -142
  58. package/edit/provisioning.cattle.io.cluster/rke2.vue +253 -582
  59. package/edit/workload/storage/ContainerMountPaths.vue +7 -5
  60. package/edit/workload/storage/__tests__/Storage.test.ts +2 -2
  61. package/edit/workload/storage/persistentVolumeClaim/__tests__/persistentvolumeclaim.test.ts +36 -0
  62. package/edit/workload/storage/persistentVolumeClaim/persistentvolumeclaim.vue +15 -7
  63. package/initialize/App.js +2 -0
  64. package/initialize/client.js +63 -51
  65. package/initialize/index.js +7 -5
  66. package/layouts/default.vue +10 -2
  67. package/layouts/home.vue +6 -2
  68. package/layouts/plain.vue +9 -2
  69. package/list/fleet.cattle.io.cluster.vue +2 -2
  70. package/list/management.cattle.io.feature.vue +1 -1
  71. package/machine-config/amazonec2.vue +1 -0
  72. package/machine-config/vmwarevsphere.vue +48 -7
  73. package/mixins/brand.js +0 -8
  74. package/mixins/child-hook.js +2 -2
  75. package/mixins/create-edit-view/impl.js +3 -3
  76. package/mixins/fetch.client.js +3 -3
  77. package/models/__tests__/management.cattle.io.node.ts +96 -0
  78. package/models/__tests__/node.ts +74 -0
  79. package/models/cluster/node.js +6 -5
  80. package/models/cluster.x-k8s.io.machinedeployment.js +2 -2
  81. package/models/management.cattle.io.cluster.js +22 -1
  82. package/models/management.cattle.io.clusterroletemplatebinding.js +3 -3
  83. package/models/management.cattle.io.globalrole.js +17 -2
  84. package/models/management.cattle.io.node.js +6 -4
  85. package/models/management.cattle.io.projectroletemplatebinding.js +3 -3
  86. package/models/management.cattle.io.roletemplate.js +17 -2
  87. package/package.json +2 -6
  88. package/pages/__tests__/prefs.test.ts +1 -1
  89. package/pages/about.vue +2 -0
  90. package/pages/auth/setup.vue +5 -4
  91. package/pages/c/_cluster/explorer/ConfigBadge.vue +1 -0
  92. package/pages/c/_cluster/monitoring/index.vue +8 -3
  93. package/pages/c/_cluster/uiplugins/CatalogList/CatalogLoadDialog.vue +9 -66
  94. package/pages/c/_cluster/uiplugins/CatalogList/CatalogUninstallDialog.vue +182 -0
  95. package/pages/c/_cluster/uiplugins/CatalogList/index.vue +15 -32
  96. package/pages/c/_cluster/uiplugins/UninstallDialog.vue +8 -46
  97. package/pages/c/_cluster/uiplugins/index.vue +64 -64
  98. package/pages/diagnostic.vue +0 -39
  99. package/pages/home.vue +1 -1
  100. package/pages/prefs.vue +3 -13
  101. package/plugins/dashboard-store/normalize.js +4 -4
  102. package/plugins/dashboard-store/resource-class.js +1 -1
  103. package/plugins/int-number.js +5 -2
  104. package/plugins/positive-int-number.js +19 -0
  105. package/plugins/steve/__tests__/getters.spec.ts +15 -0
  106. package/plugins/steve/getters.js +22 -10
  107. package/public/index.html +4 -2
  108. package/rancher-components/BadgeState/BadgeState.vue +5 -1
  109. package/rancher-components/Banner/Banner.test.ts +51 -1
  110. package/rancher-components/Banner/Banner.vue +134 -53
  111. package/rancher-components/Card/Card.test.ts +37 -0
  112. package/rancher-components/Card/Card.vue +24 -7
  113. package/rancher-components/Form/Checkbox/Checkbox.test.ts +20 -29
  114. package/rancher-components/Form/Checkbox/Checkbox.vue +45 -20
  115. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +2 -8
  116. package/rancher-components/Form/LabeledInput/LabeledInput.vue +22 -10
  117. package/rancher-components/Form/Radio/RadioButton.test.ts +31 -0
  118. package/rancher-components/Form/Radio/RadioButton.vue +30 -13
  119. package/rancher-components/Form/Radio/RadioGroup.vue +26 -7
  120. package/rancher-components/Form/TextArea/TextAreaAutoGrow.vue +7 -6
  121. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.test.ts +25 -38
  122. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +23 -11
  123. package/rancher-components/LabeledTooltip/LabeledTooltip.vue +19 -5
  124. package/rancher-components/StringList/StringList.test.ts +453 -49
  125. package/rancher-components/StringList/StringList.vue +92 -58
  126. package/scripts/extension/parse-tag-name +0 -0
  127. package/store/index.js +4 -0
  128. package/store/prefs.js +4 -4
  129. package/store/type-map.js +2 -16
  130. package/types/shell/index.d.ts +26 -14
  131. package/utils/__tests__/cluster.test.ts +55 -0
  132. package/utils/__tests__/object.test.ts +21 -2
  133. package/utils/__tests__/sort.test.ts +61 -0
  134. package/utils/cluster.js +47 -1
  135. package/utils/object.js +12 -5
  136. package/utils/string.js +12 -0
  137. package/utils/validators/formRules/__tests__/index.test.ts +13 -1
  138. package/utils/validators/formRules/index.ts +4 -0
  139. package/utils/validators/role-template.js +9 -1
  140. package/utils/version.js +1 -1
  141. package/vue.config.js +1 -4
  142. package/yarn-error.log +200 -0
  143. package/content/docs/en-us/getting-started.md +0 -224
  144. package/content/docs/en-us/whats-new.md +0 -29
  145. package/content/docs/zh-hans/getting-started.md +0 -224
  146. package/content/docs/zh-hans/whats-new.md +0 -28
  147. package/pages/docs/_doc.vue +0 -345
  148. package/pages/docs/toc.js +0 -27
  149. package/plugins/console.js +0 -34
package/pages/prefs.vue CHANGED
@@ -8,9 +8,11 @@ import ButtonGroup from '@shell/components/ButtonGroup';
8
8
  import { Checkbox } from '@components/Form/Checkbox';
9
9
  import LandingPagePreference from '@shell/components/LandingPagePreference';
10
10
  import {
11
- mapPref, THEME, KEYMAP, DATE_FORMAT, TIME_FORMAT, ROWS_PER_PAGE, HIDE_DESC, SHOW_PRE_RELEASE, MENU_MAX_CLUSTERS,
11
+ mapPref, THEME, KEYMAP, DATE_FORMAT, TIME_FORMAT, ROWS_PER_PAGE, HIDE_DESC, SHOW_PRE_RELEASE,
12
12
  VIEW_IN_API, ALL_NAMESPACES, THEME_SHORTCUT, PLUGIN_DEVELOPER, SCALE_POOL_PROMPT
13
+ , MENU_MAX_CLUSTERS
13
14
  } from '@shell/store/prefs';
15
+
14
16
  import LabeledSelect from '@shell/components/form/LabeledSelect';
15
17
  import { addObject } from '@shell/utils/array';
16
18
  import LocaleSelector from '@shell/components/LocaleSelector';
@@ -34,7 +36,6 @@ export default {
34
36
  perPage: mapPref(ROWS_PER_PAGE),
35
37
  hideDesc: mapPref(HIDE_DESC),
36
38
  showPreRelease: mapPref(SHOW_PRE_RELEASE),
37
- menuMaxClusters: mapPref(MENU_MAX_CLUSTERS),
38
39
  pluginDeveloper: mapPref(PLUGIN_DEVELOPER),
39
40
  scalingDownPrompt: mapPref(SCALE_POOL_PROMPT),
40
41
 
@@ -254,17 +255,6 @@ export default {
254
255
  placeholder="Select a row count"
255
256
  />
256
257
  </div>
257
- <div class="col span-4">
258
- <LabeledSelect
259
- v-model.number="menuMaxClusters"
260
- data-testid="prefs__displaySetting__menuMaxClusters"
261
- :label="t('prefs.clusterToShow.label')"
262
- :options="menuClusterOptions"
263
- option-key="value"
264
- option-label="label"
265
- placeholder="Select a row count"
266
- />
267
- </div>
268
258
  </div>
269
259
  </div>
270
260
  <!-- Confirmation setting -->
@@ -20,10 +20,10 @@ export function normalizeType(type) {
20
20
  // Detect and resolve conflicts from a 409 response.
21
21
  // If they are resolved, return a false-y value
22
22
  // Else they can't be resolved, return an array of errors to show to the user.
23
- export function handleConflict(initialValueJSON, value, liveValue, rootGetters, store) {
24
- const orig = store.dispatch(`cleanForDiff`, initialValueJSON);
25
- const user = store.dispatch(`cleanForDiff`, value.toJSON());
26
- const cur = store.dispatch(`cleanForDiff`, liveValue.toJSON());
23
+ export async function handleConflict(initialValueJSON, value, liveValue, rootGetters, store, storeNamespace) {
24
+ const orig = await store.dispatch(`${ storeNamespace }/cleanForDiff`, initialValueJSON, { root: true });
25
+ const user = await store.dispatch(`${ storeNamespace }/cleanForDiff`, value.toJSON(), { root: true });
26
+ const cur = await store.dispatch(`${ storeNamespace }/cleanForDiff`, liveValue.toJSON(), { root: true });
27
27
 
28
28
  const bgChange = changeset(orig, cur);
29
29
  const userChange = changeset(orig, user);
@@ -1464,7 +1464,7 @@ export default class Resource {
1464
1464
  }
1465
1465
 
1466
1466
  async saveYaml(yaml) {
1467
- this._saveYaml(yaml);
1467
+ await this._saveYaml(yaml);
1468
1468
  }
1469
1469
 
1470
1470
  async _saveYaml(yaml) {
@@ -5,9 +5,12 @@ export default Vue.directive('intNumber', {
5
5
  el.addEventListener('keypress', (e) => {
6
6
  e = e || window.event;
7
7
  const charcode = typeof e.charCode === 'number' ? e.charCode : e.keyCode;
8
- const re = /\d/;
8
+ const inputChar = String.fromCharCode(charcode);
9
9
 
10
- if (!re.test(String.fromCharCode(charcode)) && charcode > 9 && !e.ctrlKey) {
10
+ // Allow digits, minus sign at the beginning, and Ctrl key combinations
11
+ const re = /^-?\d*$/;
12
+
13
+ if (!re.test(inputChar) && charcode > 9 && !e.ctrlKey) {
11
14
  if (e.preventDefault) {
12
15
  e.preventDefault();
13
16
  } else {
@@ -0,0 +1,19 @@
1
+ import Vue from 'vue';
2
+
3
+ export default Vue.directive('positiveIntNumber', {
4
+ inserted(el) {
5
+ el.addEventListener('keypress', (e) => {
6
+ e = e || window.event;
7
+ const charcode = typeof e.charCode === 'number' ? e.charCode : e.keyCode;
8
+ const re = /^\d+$/; // Use regex to match positive numbers
9
+
10
+ if (!re.test(String.fromCharCode(charcode)) && charcode > 9 && !e.ctrlKey) {
11
+ if (e.preventDefault) {
12
+ e.preventDefault();
13
+ } else {
14
+ e.returnValue = false;
15
+ }
16
+ }
17
+ });
18
+ }
19
+ });
@@ -68,9 +68,15 @@ describe('steve: getters', () => {
68
68
  it('returns a string with a single filter statement applied if a single filter statement is applied', () => {
69
69
  expect(urlOptionsGetter('foo', { filter: { bar: 'baz' } })).toBe('foo?bar=baz');
70
70
  });
71
+ it('returns a string with a single filter statement applied and formatted for steve if a single filter statement is applied and the url starts with "/v1"', () => {
72
+ expect(urlOptionsGetter('/v1/foo', { filter: { bar: 'baz' } })).toBe('/v1/foo?filter=bar=baz&exclude=metadata.managedFields');
73
+ });
71
74
  it('returns a string with a multiple filter statements applied if a single filter statement is applied', () => {
72
75
  expect(urlOptionsGetter('foo', { filter: { bar: 'baz', far: 'faz' } })).toBe('foo?bar=baz&far=faz');
73
76
  });
77
+ it('returns a string with a multiple filter statements applied and formatted for steve if a single filter statement is applied and the url starts with "/v1"', () => {
78
+ expect(urlOptionsGetter('/v1/foo', { filter: { bar: 'baz', far: 'faz' } })).toBe('/v1/foo?filter=bar=baz&far=faz&exclude=metadata.managedFields');
79
+ });
74
80
  it('returns a string with an exclude statement for "bar" and "metadata.managedFields" if excludeFields is a single element array with the string "bar" and the url starts with "/v1/"', () => {
75
81
  expect(urlOptionsGetter('/v1/foo', { excludeFields: ['bar'] })).toBe('/v1/foo?exclude=bar&exclude=metadata.managedFields');
76
82
  });
@@ -86,8 +92,17 @@ describe('steve: getters', () => {
86
92
  it('returns a string with a sorting criteria if the sort option is provided', () => {
87
93
  expect(urlOptionsGetter('foo', { sortBy: 'bar' })).toBe('foo?sort=bar');
88
94
  });
95
+ it('returns a string with a sorting criteria formatted for steve if the sort option is provided and the url starts with "/v1"', () => {
96
+ expect(urlOptionsGetter('/v1/foo', { sortBy: 'bar' })).toBe('/v1/foo?exclude=metadata.managedFields&sort=bar');
97
+ });
89
98
  it('returns a string with a sorting criteria if the sort option is provided and an order if sortOrder is provided', () => {
90
99
  expect(urlOptionsGetter('foo', { sortBy: 'bar', sortOrder: 'baz' })).toBe('foo?sort=bar&order=baz');
91
100
  });
101
+ it('returns a string with a sorting criteria formatted for steve if the sort option is provided and an order if sortOrder is provided and the url starts with "/v1"', () => {
102
+ expect(urlOptionsGetter('/v1/foo', { sortBy: 'bar', sortOrder: 'baz' })).toBe('/v1/foo?exclude=metadata.managedFields&sort=bar');
103
+ });
104
+ it('returns a string with a sorting criteria formatted for steve if the sort option is provided and an order if sortOrder is "desc" and the url starts with "/v1"', () => {
105
+ expect(urlOptionsGetter('/v1/foo', { sortBy: 'bar', sortOrder: 'desc' })).toBe('/v1/foo?exclude=metadata.managedFields&sort=-bar');
106
+ });
92
107
  });
93
108
  });
@@ -31,8 +31,8 @@ export default {
31
31
  const isSteve = parsedUrl.path.startsWith('/v1');
32
32
 
33
33
  // Filter
34
- // Steve's filter options work differently nowadays (https://github.com/rancher/steve#filter) #9341
35
34
  if ( opt.filter ) {
35
+ url += `${ (url.includes('?') ? '&' : '?') }`;
36
36
  const keys = Object.keys(opt.filter);
37
37
 
38
38
  keys.forEach((key) => {
@@ -42,9 +42,18 @@ export default {
42
42
  vals = [vals];
43
43
  }
44
44
 
45
- vals.forEach((val) => {
46
- url += `${ (url.includes('?') ? '&' : '?') + encodeURIComponent(key) }=${ encodeURIComponent(val) }`;
45
+ // Steve's filter options now support more complex filtering not yet implemented here #9341
46
+ if (isSteve) {
47
+ url += `${ (url.includes('filter=') ? '&' : 'filter=') }`;
48
+ }
49
+
50
+ const filterStrings = vals.map((val) => {
51
+ return `${ encodeURI(key) }=${ encodeURI(val) }`;
47
52
  });
53
+ const urlEnding = url.charAt(url.length - 1);
54
+ const nextStringConnector = ['&', '?', '='].includes(urlEnding) ? '' : '&';
55
+
56
+ url += `${ nextStringConnector }${ filterStrings.join('&') }`;
48
57
  });
49
58
  }
50
59
 
@@ -82,18 +91,21 @@ export default {
82
91
  // End: Limit
83
92
 
84
93
  // Sort
85
- // Steve's sort options work differently nowadays (https://github.com/rancher/steve#sort) #9341
94
+ // Steve's sort options supports multi-column sorting and column specific sort orders, not implemented yet #9341
86
95
  const sortBy = opt.sortBy;
96
+ const orderBy = opt.sortOrder;
87
97
 
88
98
  if ( sortBy ) {
89
- url += `${ url.includes('?') ? '&' : '?' }sort=${ encodeURIComponent(sortBy) }`;
99
+ if (isSteve) {
100
+ url += `${ url.includes('?') ? '&' : '?' }sort=${ (orderBy === 'desc' ? '-' : '') + encodeURI(sortBy) }`;
101
+ } else {
102
+ url += `${ url.includes('?') ? '&' : '?' }sort=${ encodeURI(sortBy) }`;
103
+ if ( orderBy ) {
104
+ url += `${ url.includes('?') ? '&' : '?' }order=${ encodeURI(orderBy) }`;
105
+ }
106
+ }
90
107
  }
91
108
 
92
- const orderBy = opt.sortOrder;
93
-
94
- if ( orderBy ) {
95
- url += `${ url.includes('?') ? '&' : '?' }order=${ encodeURIComponent(orderBy) }`;
96
- }
97
109
  // End: Sort
98
110
 
99
111
  return url;
package/public/index.html CHANGED
@@ -13,8 +13,10 @@
13
13
  <div id="app">
14
14
  <script>
15
15
  (() => {
16
- const isDark = document.cookie.includes('R_THEME=auto') ?
17
- // User selected automatic theme, so use pcs (set when ui previously loaded and is either os theme or time of day based
16
+ // Has the user chosen to auto detect the theme.... or if they haven't chosen anything.. --> check the auto-detected theme via R_PCS
17
+ // Otherwise check if they've specifically selected a theme --> R_THEME
18
+ const isDark = document.cookie.includes('R_THEME=auto') || !document.cookie.includes('R_THEME') ?
19
+ // User selected automatic theme, so use PCS (set when ui previously loaded and is either os theme or time of day based)
18
20
  document.cookie.includes('R_PCS=dark') :
19
21
  // Otherwise user selected light/dark theme directly
20
22
  document.cookie.includes('R_THEME=dark');
@@ -60,7 +60,11 @@ export default Vue.extend({
60
60
 
61
61
  <template>
62
62
  <span :class="{'badge-state': true, [bg]: true}">
63
- <i v-if="icon" class="icon" :class="{[icon]: true, 'mr-5': !!msg}" />{{ msg }}
63
+ <i
64
+ v-if="icon"
65
+ class="icon"
66
+ :class="{[icon]: true, 'mr-5': !!msg}"
67
+ />{{ msg }}
64
68
  </span>
65
69
  </template>
66
70
 
@@ -1,13 +1,63 @@
1
1
  import { mount } from '@vue/test-utils';
2
2
  import { Banner } from './index';
3
+ import { cleanHtmlDirective } from '@shell/plugins/clean-html-directive';
3
4
 
4
5
  describe('component: Banner', () => {
5
6
  it('should display text based on label', () => {
6
7
  const label = 'test';
7
- const wrapper = mount(Banner, { propsData: { label } });
8
+ const wrapper = mount(
9
+ Banner,
10
+ {
11
+ directives: { cleanHtmlDirective },
12
+ propsData: { label }
13
+ });
8
14
 
9
15
  const element = wrapper.find('span').element;
10
16
 
11
17
  expect(element.textContent).toBe(label);
12
18
  });
19
+
20
+ it('should display an icon', () => {
21
+ const icon = 'my-icon';
22
+ const wrapper = mount(Banner, { propsData: { icon } });
23
+
24
+ const element = wrapper.find(`.${ icon }`).element;
25
+
26
+ expect(element.classList).toContain(icon);
27
+ });
28
+
29
+ it('should not display an icon', () => {
30
+ const wrapper = mount(Banner);
31
+
32
+ const element = wrapper.find(`[data-testid="banner-icon"]`).element;
33
+
34
+ expect(element).not.toBeDefined();
35
+ });
36
+
37
+ it('should emit close event', () => {
38
+ const wrapper = mount(Banner, { propsData: { closable: true } });
39
+ const element = wrapper.find(`[data-testid="banner-close"]`).element;
40
+
41
+ element.click();
42
+
43
+ expect(wrapper.emitted('close')).toHaveLength(1);
44
+ });
45
+
46
+ it('should add the right color', () => {
47
+ const color = 'red';
48
+ const wrapper = mount(Banner, { propsData: { color } });
49
+
50
+ const element = wrapper.element;
51
+
52
+ expect(element.classList).toContain(color);
53
+ });
54
+
55
+ it('should stack the banner messages', () => {
56
+ const stacked = true;
57
+ const wrapper = mount(Banner, { propsData: { stacked } });
58
+
59
+ const element = wrapper.find(`[data-testid="banner-content"]`).element;
60
+
61
+ expect(element.classList).toContain('stacked');
62
+ });
13
63
  });
@@ -27,6 +27,13 @@ export default Vue.extend({
27
27
  type: String,
28
28
  default: null
29
29
  },
30
+ /**
31
+ * Add icon for the banner
32
+ */
33
+ icon: {
34
+ type: String,
35
+ default: null
36
+ },
30
37
  /**
31
38
  * Toggles the banner's close button.
32
39
  */
@@ -58,31 +65,137 @@ export default Vue.extend({
58
65
  class="banner"
59
66
  :class="{
60
67
  [color]: true,
61
- closable,
62
- stacked
63
68
  }"
64
69
  >
65
- <slot>
66
- <t v-if="labelKey" :k="labelKey" :raw="true" />
67
- <span v-else-if="messageLabel">{{ messageLabel }}</span>
68
- <span v-else v-html="nlToBr(label)" />
69
- </slot>
70
- <div v-if="closable" class="closer" @click="$emit('close')">
71
- <i class="icon icon-2x icon-close closer-icon" />
70
+ <div
71
+ v-if="icon"
72
+ class="banner__icon"
73
+ data-testid="banner-icon"
74
+ >
75
+ <i
76
+ class="icon icon-2x"
77
+ :class="icon"
78
+ />
79
+ </div>
80
+ <div
81
+ class="banner__content"
82
+ data-testid="banner-content"
83
+ :class="{
84
+ closable,
85
+ stacked,
86
+ icon
87
+ }"
88
+ >
89
+ <slot>
90
+ <t
91
+ v-if="labelKey"
92
+ :k="labelKey"
93
+ :raw="true"
94
+ />
95
+ <span v-else-if="messageLabel">{{ messageLabel }}</span>
96
+ <span
97
+ v-else
98
+ v-clean-html="nlToBr(label)"
99
+ />
100
+ </slot>
101
+ <div
102
+ v-if="closable"
103
+ class="banner__content__closer"
104
+ @click="$emit('close')"
105
+ >
106
+ <i
107
+ data-testid="banner-close"
108
+ class="icon icon-close closer-icon"
109
+ />
110
+ </div>
72
111
  </div>
73
112
  </div>
74
113
  </template>
75
114
 
76
115
  <style lang="scss" scoped>
77
- $left-border-size: 4px;
116
+ $left-border-size: 4px;
117
+ $icon-size: 24px;
118
+
119
+ .banner {
120
+ display: flex;
121
+ margin: 15px 0;
122
+ position: relative;
123
+ width: 100%;
124
+ color: var(--body-text);
125
+
126
+ &__icon {
127
+ width: $icon-size * 2;
128
+ flex-grow: 1;
129
+ display: flex;
130
+ justify-content: center;
131
+ align-items: center;
132
+ box-sizing: content-box;
133
+
134
+ .primary & {
135
+ background: var(--primary);
136
+ }
137
+
138
+ .secondary & {
139
+ background: var(--default);
140
+ }
141
+
142
+ .success & {
143
+ background: var(--success);
144
+ }
78
145
 
79
- .banner {
146
+ .info & {
147
+ background: var(--info);
148
+ }
149
+
150
+ .warning & {
151
+ background: var(--warning);
152
+ }
153
+
154
+ .error & {
155
+ background: var(--error);
156
+ color: var(--primary-text);
157
+ }
158
+ }
159
+
160
+ &__content {
80
161
  padding: 10px;
81
- margin: 15px 0;
82
- width: 100%;
83
162
  transition: all 0.2s ease;
84
- position: relative;
85
163
  line-height: 20px;
164
+ width: 100%;
165
+ border-left: solid $left-border-size transparent;
166
+ display: flex;
167
+ gap: 3px;
168
+
169
+ .primary & {
170
+ background: var(--primary);
171
+ border-color: var(--primary);
172
+ }
173
+
174
+ .secondary & {
175
+ background: var(--default-banner-bg);
176
+ border-color: var(--default);
177
+ }
178
+
179
+ .success & {
180
+ background: var(--success-banner-bg);
181
+ border-color: var(--success);
182
+ }
183
+
184
+ .info & {
185
+ background: var(--info-banner-bg);
186
+ border-color: var(--info);
187
+ }
188
+
189
+ .warning & {
190
+ background: var(--warning-banner-bg);
191
+ border-color: var(--warning);
192
+ }
193
+
194
+ .error & {
195
+ background: var(--error-banner-bg);
196
+ border-color: var(--error);
197
+ color: var(--error);
198
+ }
86
199
 
87
200
  &.stacked {
88
201
  padding: 0 10px;
@@ -97,10 +210,10 @@ export default Vue.extend({
97
210
  }
98
211
 
99
212
  &.closable {
100
- padding-right: 40px;
213
+ padding-right: $icon-size * 2;
101
214
  }
102
215
 
103
- .closer {
216
+ &__closer {
104
217
  display: flex;
105
218
  align-items: center;
106
219
 
@@ -109,12 +222,11 @@ export default Vue.extend({
109
222
  top: 0;
110
223
  right: 0;
111
224
  bottom: 0;
112
- width: 40px;
113
- line-height: 42px;
225
+ width: $icon-size;
226
+ line-height: $icon-size;
114
227
  text-align: center;
115
228
 
116
229
  .closer-icon {
117
- font-size: 22px;
118
230
  opacity: 0.7;
119
231
 
120
232
  &:hover {
@@ -124,40 +236,9 @@ export default Vue.extend({
124
236
  }
125
237
  }
126
238
 
127
- &.primary {
128
- background: var(--primary);
129
- border-left: solid $left-border-size var(--primary);
130
- color: var(--body-text);
131
- }
132
-
133
- &.secondary {
134
- background: var(--default-banner-bg);
135
- border-left: solid $left-border-size var(--default);
136
- color: var(--body-text);
137
- }
138
-
139
- &.success {
140
- background: var(--success-banner-bg);
141
- border-left: solid $left-border-size var(--success);
142
- color: var(--body-text);
143
- }
144
-
145
- &.info {
146
- background: var(--info-banner-bg);
147
- border-left: solid $left-border-size var(--info);
148
- color: var(--body-text);
149
- }
150
-
151
- &.warning {
152
- background: var(--warning-banner-bg);
153
- border-left: solid $left-border-size var(--warning);
154
- color: var(--body-text);
155
- }
156
-
157
- &.error {
158
- background: var(--error-banner-bg);
159
- border-left: solid $left-border-size var(--error);
160
- color: var(--error);
239
+ &.icon {
240
+ border-left: none;
161
241
  }
162
242
  }
243
+ }
163
244
  </style>
@@ -0,0 +1,37 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import { Card } from './index';
3
+
4
+ describe('component: Card', () => {
5
+ const title = 'Card title';
6
+ const body = 'Card body';
7
+
8
+ it('should have a card title', () => {
9
+ const wrapper = mount(Card, {
10
+ propsData: { title },
11
+ slots: { title: '<div>Card title</div>' }
12
+ });
13
+
14
+ const element = wrapper.find('[data-testid="card-title-slot"]');
15
+
16
+ expect(element.exists()).toBe(true);
17
+ expect(element.text()).toBe(title);
18
+ });
19
+
20
+ it('should have a card body', () => {
21
+ const wrapper = mount(Card, {
22
+ propsData: { body },
23
+ slots: { body: '<div>Card body</div>' }
24
+ });
25
+ const element = wrapper.find('[data-testid="card-body-slot"]');
26
+
27
+ expect(element.exists()).toBe(true);
28
+ expect(element.text()).toBe(body);
29
+ });
30
+
31
+ it('should display the default card actions', () => {
32
+ const wrapper = mount(Card);
33
+ const element = wrapper.find('[data-testid="card-actions-slot"]');
34
+
35
+ expect(element.exists()).toBe(true);
36
+ });
37
+ });
@@ -49,28 +49,45 @@ export default Vue.extend({
49
49
  sticky: {
50
50
  type: Boolean,
51
51
  default: false,
52
- },
52
+ },
53
53
  }
54
54
  });
55
55
  </script>
56
56
 
57
57
  <template>
58
- <div class="card-container" :class="{'highlight-border': showHighlightBorder, 'card-sticky': sticky}">
58
+ <div
59
+ class="card-container"
60
+ :class="{'highlight-border': showHighlightBorder, 'card-sticky': sticky}"
61
+ data-testid="card"
62
+ >
59
63
  <div class="card-wrap">
60
- <div class="card-title">
64
+ <div
65
+ class="card-title"
66
+ data-testid="card-title-slot"
67
+ >
61
68
  <slot name="title">
62
69
  {{ title }}
63
70
  </slot>
64
71
  </div>
65
- <hr />
66
- <div class="card-body">
72
+ <hr>
73
+ <div
74
+ class="card-body"
75
+ data-testid="card-body-slot"
76
+ >
67
77
  <slot name="body">
68
78
  {{ content }}
69
79
  </slot>
70
80
  </div>
71
- <div v-if="showActions" class="card-actions">
81
+ <div
82
+ v-if="showActions"
83
+ class="card-actions"
84
+ data-testid="card-actions-slot"
85
+ >
72
86
  <slot name="actions">
73
- <button class="btn role-primary" @click="buttonAction">
87
+ <button
88
+ class="btn role-primary"
89
+ @click="buttonAction"
90
+ >
74
91
  {{ buttonText }}
75
92
  </button>
76
93
  </slot>