@rancher/shell 0.3.17 → 0.3.19

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 (118) hide show
  1. package/assets/translations/en-us.yaml +8 -4
  2. package/assets/translations/zh-hans.yaml +64 -8
  3. package/components/AsyncButton.vue +1 -1
  4. package/components/Inactivity.vue +10 -0
  5. package/components/LazyImage.vue +2 -2
  6. package/components/PromptRestore.vue +8 -6
  7. package/components/ResourceDetail/Masthead.vue +1 -1
  8. package/components/ResourceDetail/index.vue +4 -2
  9. package/components/__tests__/PromptRestore.test.ts +142 -0
  10. package/components/auth/AzureWarning.vue +1 -1
  11. package/components/auth/RoleDetailEdit.vue +2 -0
  12. package/components/fleet/FleetResources.vue +3 -64
  13. package/components/form/FileImageSelector.vue +9 -0
  14. package/components/form/FileSelector.vue +2 -1
  15. package/components/form/MatchExpressions.vue +1 -3
  16. package/components/form/__tests__/FileImageSelector.test.ts +42 -0
  17. package/components/form/__tests__/FileSelector.test.ts +76 -0
  18. package/components/formatter/ClusterProvider.vue +3 -1
  19. package/components/formatter/__tests__/ClusterProvider.test.ts +24 -0
  20. package/components/nav/WindowManager/ContainerShell.vue +60 -36
  21. package/components/nav/WindowManager/__tests__/ContainerShell.test.ts +561 -0
  22. package/config/labels-annotations.js +2 -1
  23. package/config/persistentVolume.ts +108 -0
  24. package/config/product/manager.js +5 -1
  25. package/config/types.js +2 -0
  26. package/core/plugin-helpers.js +19 -3
  27. package/core/types.ts +4 -0
  28. package/detail/fleet.cattle.io.gitrepo.vue +10 -2
  29. package/detail/pod.vue +36 -3
  30. package/detail/workload/index.vue +40 -9
  31. package/dialog/DiagnosticTimingsDialog.vue +1 -0
  32. package/edit/__tests__/ui.cattle.io.navlink.test.ts +110 -0
  33. package/edit/fleet.cattle.io.clustergroup.vue +14 -3
  34. package/edit/persistentvolume/__tests__/persistentvolume.test.ts +82 -0
  35. package/edit/persistentvolume/index.vue +2 -1
  36. package/edit/persistentvolume/plugins/csi.vue +3 -1
  37. package/edit/persistentvolume/plugins/longhorn.vue +12 -12
  38. package/edit/provisioning.cattle.io.cluster/RegistryConfigs.vue +15 -11
  39. package/edit/provisioning.cattle.io.cluster/index.vue +1 -1
  40. package/edit/provisioning.cattle.io.cluster/rke2.vue +5 -1
  41. package/edit/storage.k8s.io.storageclass/index.vue +1 -2
  42. package/edit/ui.cattle.io.navlink.vue +213 -186
  43. package/layouts/default.vue +1 -1
  44. package/list/group.principal.vue +1 -1
  45. package/middleware/authenticated.js +12 -4
  46. package/mixins/create-edit-view/impl.js +2 -2
  47. package/models/chart.js +1 -1
  48. package/models/etcdbackup.js +2 -1
  49. package/models/fleet.cattle.io.cluster.js +33 -4
  50. package/models/fleet.cattle.io.gitrepo.js +112 -38
  51. package/models/management.cattle.io.cluster.js +13 -3
  52. package/models/management.cattle.io.kontainerdriver.js +14 -0
  53. package/models/persistentvolume.js +2 -111
  54. package/models/pod.js +30 -0
  55. package/models/rke.cattle.io.etcdsnapshot.js +10 -7
  56. package/package.json +1 -1
  57. package/pages/c/_cluster/apps/charts/install.vue +74 -25
  58. package/pages/c/_cluster/auth/group.principal/assign-edit.vue +1 -1
  59. package/pages/c/_cluster/auth/roles/index.vue +1 -1
  60. package/pages/c/_cluster/explorer/index.vue +1 -1
  61. package/pages/c/_cluster/manager/cloudCredential/_id.vue +0 -1
  62. package/pages/c/_cluster/manager/cloudCredential/create.vue +0 -1
  63. package/pages/c/_cluster/settings/brand.vue +11 -8
  64. package/pages/c/_cluster/uiplugins/index.vue +9 -4
  65. package/pages/diagnostic.vue +5 -3
  66. package/pages/home.vue +1 -1
  67. package/plugins/dashboard-store/__tests__/actions.spec.ts +165 -0
  68. package/plugins/dashboard-store/__tests__/getters.spec.ts +100 -0
  69. package/plugins/dashboard-store/__tests__/{mutations.spec.js → mutations.spec.ts} +2 -2
  70. package/plugins/dashboard-store/actions.js +1 -1
  71. package/plugins/dashboard-store/resource-class.js +4 -0
  72. package/plugins/steve/__tests__/getters.spec.ts +93 -0
  73. package/plugins/steve/getters.js +21 -1
  74. package/plugins/steve/subscribe.js +1 -3
  75. package/rancher-components/components/BadgeState/BadgeState.spec.ts +12 -0
  76. package/rancher-components/components/BadgeState/BadgeState.vue +111 -0
  77. package/rancher-components/components/BadgeState/index.ts +1 -0
  78. package/rancher-components/components/Banner/Banner.test.ts +63 -0
  79. package/rancher-components/components/Banner/Banner.vue +244 -0
  80. package/rancher-components/components/Banner/index.ts +1 -0
  81. package/rancher-components/components/Card/Card.test.ts +37 -0
  82. package/rancher-components/components/Card/Card.vue +167 -0
  83. package/rancher-components/components/Card/index.ts +1 -0
  84. package/rancher-components/components/Form/Checkbox/Checkbox.test.ts +68 -0
  85. package/rancher-components/components/Form/Checkbox/Checkbox.vue +420 -0
  86. package/rancher-components/components/Form/Checkbox/index.ts +1 -0
  87. package/rancher-components/components/Form/LabeledInput/LabeledInput.test.ts +23 -0
  88. package/rancher-components/components/Form/LabeledInput/LabeledInput.vue +355 -0
  89. package/rancher-components/components/Form/LabeledInput/index.ts +1 -0
  90. package/rancher-components/components/Form/Radio/RadioButton.test.ts +31 -0
  91. package/rancher-components/components/Form/Radio/RadioButton.vue +287 -0
  92. package/rancher-components/components/Form/Radio/RadioGroup.vue +254 -0
  93. package/rancher-components/components/Form/Radio/index.ts +2 -0
  94. package/rancher-components/components/Form/TextArea/TextAreaAutoGrow.vue +170 -0
  95. package/rancher-components/components/Form/TextArea/index.ts +1 -0
  96. package/rancher-components/components/Form/ToggleSwitch/ToggleSwitch.test.ts +94 -0
  97. package/rancher-components/components/Form/ToggleSwitch/ToggleSwitch.vue +149 -0
  98. package/rancher-components/components/Form/ToggleSwitch/index.ts +1 -0
  99. package/rancher-components/components/Form/index.ts +5 -0
  100. package/rancher-components/components/LabeledTooltip/LabeledTooltip.vue +151 -0
  101. package/rancher-components/components/LabeledTooltip/index.ts +1 -0
  102. package/rancher-components/components/StringList/StringList.test.ts +484 -0
  103. package/rancher-components/components/StringList/StringList.vue +611 -0
  104. package/rancher-components/components/StringList/index.ts +1 -0
  105. package/scripts/extension/publish +54 -14
  106. package/scripts/typegen.sh +10 -2
  107. package/store/index.js +1 -3
  108. package/store/store-types.js +2 -0
  109. package/types/api.d.ts +1 -0
  110. package/types/fleet.d.ts +1 -0
  111. package/types/shell/index.d.ts +696 -2
  112. package/types/userPreferences.d.ts +1 -1
  113. package/utils/__mocks__/socket.js +21 -0
  114. package/utils/grafana.js +23 -11
  115. package/utils/selector.js +2 -1
  116. package/utils/socket.js +1 -0
  117. package/utils/validators/formRules/index.ts +3 -3
  118. package/plugins/steve/urloptions.js +0 -47
@@ -0,0 +1,93 @@
1
+ import _getters from '@shell/plugins/steve/getters';
2
+
3
+ const { urlFor, urlOptions } = _getters;
4
+
5
+ describe('steve: getters', () => {
6
+ describe('steve > getters > urlFor', () => {
7
+ // we're not testing function output based off of state or getter inputs here since they are dependencies
8
+ const state = { config: { baseUrl: 'protocol' } };
9
+ const getters = {
10
+ normalizeType: (type) => type,
11
+ schemaFor: (type) => {
12
+ if (type === 'typeFoo') {
13
+ return { links: { collection: 'urlFoo' } };
14
+ }
15
+ },
16
+ // this has its own tests so it just returns the input string
17
+ urlOptions: (string) => string
18
+ };
19
+
20
+ const urlForGetter = urlFor(state, getters);
21
+
22
+ // most tests for this getter will go through the dashboard-store getters test spec, this only tests logic specific to the steve variant
23
+
24
+ it('expects urlFor to return a function', () => {
25
+ expect(typeof urlFor(state, getters)).toBe('function');
26
+ });
27
+
28
+ it('expects function returned by urlFor to return a string a type', () => {
29
+ expect(urlForGetter('typeFoo')).toBe('protocol/urlFoo');
30
+ });
31
+
32
+ it('expects function returned by urlFor to return a string containing a namespace when provided with a type and a single namespace string', () => {
33
+ expect(urlForGetter('typeFoo', undefined, { namespaced: 'nsBar' })).toBe('protocol/urlFoo/nsBar');
34
+ });
35
+
36
+ it('expects function returned by urlFor to return a string not containing a namespace when provided with a type and a multiple namespaces string', () => {
37
+ expect(urlForGetter('typeFoo', undefined, { namespaced: ['nsBar', 'nsBaz'] })).toBe('protocol/urlFoo');
38
+ });
39
+ });
40
+ describe('steve > getters > urlOptions', () => {
41
+ // we're not testing function output based off of state or getter inputs here since they are dependencies
42
+ const state = { config: { baseUrl: 'protocol' } };
43
+ const getters = {
44
+ normalizeType: (type) => type,
45
+ schemaFor: (type) => {
46
+ if (type === 'typeFoo') {
47
+ return { links: { collection: 'urlFoo' } };
48
+ }
49
+ },
50
+ // this has its own tests so it just returns the input string
51
+ urlOptions: (string) => string
52
+ };
53
+
54
+ const urlOptionsGetter = urlOptions();
55
+
56
+ it('expects urlOptions to return a function', () => {
57
+ expect(typeof urlOptions(state, getters)).toBe('function');
58
+ });
59
+ it('returns undefined when called without params', () => {
60
+ expect(urlOptionsGetter()).toBeUndefined();
61
+ });
62
+ it('returns an unmodified string when called without options', () => {
63
+ expect(urlOptionsGetter('foo')).toBe('foo');
64
+ });
65
+ it('returns an unmodified string when called with options that are not accounted for', () => {
66
+ expect(urlOptionsGetter('foo', { bar: 'baz' })).toBe('foo');
67
+ });
68
+ it('returns a string with a single filter statement applied if a single filter statement is applied', () => {
69
+ expect(urlOptionsGetter('foo', { filter: { bar: 'baz' } })).toBe('foo?bar=baz');
70
+ });
71
+ it('returns a string with a multiple filter statements applied if a single filter statement is applied', () => {
72
+ expect(urlOptionsGetter('foo', { filter: { bar: 'baz', far: 'faz' } })).toBe('foo?bar=baz&far=faz');
73
+ });
74
+ 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
+ expect(urlOptionsGetter('/v1/foo', { excludeFields: ['bar'] })).toBe('/v1/foo?exclude=bar&exclude=metadata.managedFields');
76
+ });
77
+ it('returns a string without an exclude statement if excludeFields is but the url does not start with "/v1/"', () => {
78
+ expect(urlOptionsGetter('foo', { excludeFields: ['bar'] })).toBe('foo');
79
+ });
80
+ it('returns a string without an exclude statement if excludeFields is an array but the URL doesnt include the "/v1/ string"', () => {
81
+ expect(urlOptionsGetter('foo', { excludeFields: ['bar'] })).toBe('foo');
82
+ });
83
+ it('returns a string with a limit applied if a limit is provided', () => {
84
+ expect(urlOptionsGetter('foo', { limit: 10 })).toBe('foo?limit=10');
85
+ });
86
+ it('returns a string with a sorting criteria if the sort option is provided', () => {
87
+ expect(urlOptionsGetter('foo', { sortBy: 'bar' })).toBe('foo?sort=bar');
88
+ });
89
+ it('returns a string with a sorting criteria if the sort option is provided and an order if sortOrder is provided', () => {
90
+ expect(urlOptionsGetter('foo', { sortBy: 'bar', sortOrder: 'baz' })).toBe('foo?sort=bar&order=baz');
91
+ });
92
+ });
93
+ });
@@ -9,6 +9,7 @@ import NormanModel from './norman-class';
9
9
  import { urlFor } from '@shell/plugins/dashboard-store/getters';
10
10
  import { normalizeType } from '@shell/plugins/dashboard-store/normalize';
11
11
  import pAndNFiltering from '@shell/utils/projectAndNamespaceFiltering.utils';
12
+ import { parse } from '@shell/utils/url';
12
13
 
13
14
  export const STEVE_MODEL_TYPES = {
14
15
  NORMAN: 'norman',
@@ -26,8 +27,11 @@ const GC_IGNORE_TYPES = {
26
27
  export default {
27
28
  urlOptions: () => (url, opt) => {
28
29
  opt = opt || {};
30
+ const parsedUrl = parse(url);
31
+ const isSteve = parsedUrl.path.startsWith('/v1');
29
32
 
30
33
  // Filter
34
+ // Steve's filter options work differently nowadays (https://github.com/rancher/steve#filter) #9341
31
35
  if ( opt.filter ) {
32
36
  const keys = Object.keys(opt.filter);
33
37
 
@@ -54,6 +58,21 @@ export default {
54
58
  }
55
59
  // End: Filter
56
60
 
61
+ // Exclude
62
+ // excludeFields should be an array of strings representing the paths of the fields to exclude
63
+ // only works on Steve but is ignored without error by Norman
64
+ if (isSteve) {
65
+ if (Array.isArray(opt?.excludeFields)) {
66
+ opt.excludeFields = [...opt.excludeFields, 'metadata.managedFields'];
67
+ } else {
68
+ opt.excludeFields = ['metadata.managedFields'];
69
+ }
70
+ const excludeParamsString = opt.excludeFields.map((field) => `exclude=${ field }`).join('&');
71
+
72
+ url += `${ url.includes('?') ? '&' : '?' }${ excludeParamsString }`;
73
+ }
74
+ // End: Exclude
75
+
57
76
  // Limit
58
77
  const limit = opt.limit;
59
78
 
@@ -63,6 +82,7 @@ export default {
63
82
  // End: Limit
64
83
 
65
84
  // Sort
85
+ // Steve's sort options work differently nowadays (https://github.com/rancher/steve#sort) #9341
66
86
  const sortBy = opt.sortBy;
67
87
 
68
88
  if ( sortBy ) {
@@ -85,7 +105,7 @@ export default {
85
105
  // `namespaced` is either
86
106
  // - a string representing a single namespace - add restriction to the url
87
107
  // - an array of namespaces or projects - add restriction as a param
88
- if (opt.namespaced && !pAndNFiltering.isApplicable(opt)) {
108
+ if (opt?.namespaced && !pAndNFiltering.isApplicable(opt)) {
89
109
  const parts = url.split('/');
90
110
 
91
111
  url = `${ parts.join('/') }/${ opt.namespaced }`;
@@ -32,9 +32,7 @@ import { keyForSubscribe } from '@shell/plugins/steve/resourceWatcher';
32
32
  import { waitFor } from '@shell/utils/async';
33
33
  import { WORKER_MODES } from './worker';
34
34
  import pAndNFiltering from '@shell/utils/projectAndNamespaceFiltering.utils';
35
-
36
- import { BLANK_CLUSTER } from '@shell/store/index.js';
37
- import { STORE } from '@shell/store/store-types';
35
+ import { BLANK_CLUSTER, STORE } from '@shell/store/store-types.js';
38
36
 
39
37
  // minimum length of time a disconnect notification is shown
40
38
  const MINIMUM_TIME_NOTIFIED = 3000;
@@ -0,0 +1,12 @@
1
+ import { shallowMount } from '@vue/test-utils';
2
+ import { BadgeState } from './index';
3
+
4
+ describe('BadgeState.vue', () => {
5
+ it('renders props.msg when passed', () => {
6
+ const label = 'Hello, World!';
7
+
8
+ const wrapper = shallowMount(BadgeState, { propsData: { label } });
9
+
10
+ expect(wrapper.find('span').text()).toMatch(label);
11
+ });
12
+ });
@@ -0,0 +1,111 @@
1
+ <script lang="ts">
2
+ import Vue, { PropType } from 'vue';
3
+
4
+ interface Badge {
5
+ stateBackground: string;
6
+ stateDisplay: string;
7
+ }
8
+
9
+ /**
10
+ * Badge state component.
11
+ * <p>Represents a badge whose label and color is either taken from the value property or
12
+ * from the label and color properties. The state property takes precedence.</p>
13
+ */
14
+ export default Vue.extend({
15
+ props: {
16
+ /**
17
+ * A value having the properties `stateBackground` and `stateDisplay`
18
+ */
19
+ value: {
20
+ type: Object as PropType<Badge>,
21
+ default: null
22
+ },
23
+
24
+ /**
25
+ * Badge color. `stateBackground` of the value property takes precedence if supplied
26
+ */
27
+ color: {
28
+ type: String,
29
+ default: null
30
+ },
31
+
32
+ /**
33
+ * Optional icon to be shown before the label
34
+ */
35
+ icon: {
36
+ type: String,
37
+ default: null
38
+ },
39
+
40
+ /**
41
+ * Label to display in the badge. `stateDisplay` of the value property takes precedence if supplied
42
+ */
43
+ label: {
44
+ type: String,
45
+ default: null
46
+ }
47
+ },
48
+
49
+ computed: {
50
+ bg(): string | null {
51
+ return this.value?.stateBackground || this.color;
52
+ },
53
+
54
+ msg(): string | null {
55
+ return this.value?.stateDisplay || this.label;
56
+ }
57
+ }
58
+ });
59
+ </script>
60
+
61
+ <template>
62
+ <span :class="{'badge-state': true, [bg]: true}">
63
+ <i
64
+ v-if="icon"
65
+ class="icon"
66
+ :class="{[icon]: true, 'mr-5': !!msg}"
67
+ />{{ msg }}
68
+ </span>
69
+ </template>
70
+
71
+ <style lang="scss" scoped>
72
+ .badge-state {
73
+ align-items: center;
74
+ display: inline-flex;
75
+ padding: 2px 10px;
76
+ border: 1px solid transparent;
77
+ border-radius: 20px;
78
+
79
+ &.bg-info {
80
+ border-color: var(--primary);
81
+ }
82
+
83
+ &.bg-error {
84
+ border-color: var(--error);
85
+ }
86
+
87
+ &.bg-warning {
88
+ border-color: var(--warning);
89
+ }
90
+
91
+ // Successful states are de-emphasized by using [text-]color instead of background-color
92
+ &.bg-success {
93
+ color: var(--success);
94
+ background: transparent;
95
+ border-color: var(--success);
96
+ }
97
+ }
98
+ </style>
99
+ <style lang="scss">
100
+ // TODO: #6005
101
+ // Investigate why this is here.. I don't think that styles for sortable table should belong here
102
+ .sortable-table TD .badge-state {
103
+ @include clip;
104
+ display: inline-block;
105
+ max-width: 100%;
106
+ position: relative;
107
+ max-width: 110px;
108
+ font-size: .85em;
109
+ vertical-align: middle;
110
+ }
111
+ </style>
@@ -0,0 +1 @@
1
+ export { default as BadgeState } from './BadgeState.vue';
@@ -0,0 +1,63 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import { Banner } from './index';
3
+ import { cleanHtmlDirective } from '@shell/plugins/clean-html-directive';
4
+
5
+ describe('component: Banner', () => {
6
+ it('should display text based on label', () => {
7
+ const label = 'test';
8
+ const wrapper = mount(
9
+ Banner,
10
+ {
11
+ directives: { cleanHtmlDirective },
12
+ propsData: { label }
13
+ });
14
+
15
+ const element = wrapper.find('span').element;
16
+
17
+ expect(element.textContent).toBe(label);
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
+ });
63
+ });
@@ -0,0 +1,244 @@
1
+ <script lang="ts">
2
+ import Vue from 'vue';
3
+ import { nlToBr } from '@shell/utils/string';
4
+ import { stringify } from '@shell/utils/error';
5
+
6
+ export default Vue.extend({
7
+ props: {
8
+ /**
9
+ * A color class that represents the color of the banner.
10
+ * @values primary, secondary, success, warning, error, info
11
+ */
12
+ color: {
13
+ type: String,
14
+ default: 'secondary'
15
+ },
16
+ /**
17
+ * The label to display as the banner's default content.
18
+ */
19
+ label: {
20
+ type: [String, Error, Object],
21
+ default: null
22
+ },
23
+ /**
24
+ * The i18n key for the label to display as the banner's default content.
25
+ */
26
+ labelKey: {
27
+ type: String,
28
+ default: null
29
+ },
30
+ /**
31
+ * Add icon for the banner
32
+ */
33
+ icon: {
34
+ type: String,
35
+ default: null
36
+ },
37
+ /**
38
+ * Toggles the banner's close button.
39
+ */
40
+ closable: {
41
+ type: Boolean,
42
+ default: false
43
+ },
44
+ /**
45
+ * Toggles the stacked class for the banner.
46
+ */
47
+ stacked: {
48
+ type: Boolean,
49
+ default: false
50
+ }
51
+ },
52
+ computed: {
53
+ /**
54
+ * Return message text as label.
55
+ */
56
+ messageLabel(): string | void {
57
+ return !(typeof this.label === 'string') ? stringify(this.label) : undefined;
58
+ }
59
+ },
60
+ methods: { nlToBr }
61
+ });
62
+ </script>
63
+ <template>
64
+ <div
65
+ class="banner"
66
+ :class="{
67
+ [color]: true,
68
+ }"
69
+ >
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>
111
+ </div>
112
+ </div>
113
+ </template>
114
+
115
+ <style lang="scss" scoped>
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
+ }
145
+
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 {
161
+ padding: 10px;
162
+ transition: all 0.2s ease;
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
+ }
199
+
200
+ &.stacked {
201
+ padding: 0 10px;
202
+ margin: 0;
203
+ transition: none;
204
+ &:first-child {
205
+ padding-top: 10px;
206
+ }
207
+ &:last-child {
208
+ padding-bottom: 10px;
209
+ }
210
+ }
211
+
212
+ &.closable {
213
+ padding-right: $icon-size * 2;
214
+ }
215
+
216
+ &__closer {
217
+ display: flex;
218
+ align-items: center;
219
+
220
+ cursor: pointer;
221
+ position: absolute;
222
+ top: 0;
223
+ right: 0;
224
+ bottom: 0;
225
+ width: $icon-size;
226
+ line-height: $icon-size;
227
+ text-align: center;
228
+
229
+ .closer-icon {
230
+ opacity: 0.7;
231
+
232
+ &:hover {
233
+ opacity: 1;
234
+ color: var(--link);
235
+ }
236
+ }
237
+ }
238
+
239
+ &.icon {
240
+ border-left: none;
241
+ }
242
+ }
243
+ }
244
+ </style>
@@ -0,0 +1 @@
1
+ export { default as Banner } from './Banner.vue';
@@ -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
+ });