@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.
- package/assets/styles/base/_variables.scss +1 -0
- package/assets/styles/themes/_dark.scss +1 -0
- package/assets/styles/themes/_light.scss +6 -5
- package/assets/translations/en-us.yaml +44 -17
- package/assets/translations/zh-hans.yaml +2 -2
- package/components/ClusterIconMenu.vue +143 -0
- package/components/CruResource.vue +7 -1
- package/components/ExplorerProjectsNamespaces.vue +11 -1
- package/components/FixedBanner.vue +17 -1
- package/components/Loading.vue +1 -1
- package/components/Markdown.vue +1 -1
- package/components/Questions/__tests__/Yaml.test.ts +3 -2
- package/components/SideNav.vue +1 -1
- package/components/SortableTable/index.vue +3 -2
- package/components/auth/RoleDetailEdit.vue +15 -2
- package/components/auth/login/saml.vue +12 -1
- package/components/form/LabeledSelect.vue +12 -5
- package/components/form/Members/ClusterPermissionsEditor.vue +1 -1
- package/components/form/Members/MembershipEditor.vue +6 -1
- package/components/form/SelectOrCreateAuthSecret.vue +7 -0
- package/components/form/__tests__/KeyValue.test.ts +6 -3
- package/components/form/__tests__/LabeledSelect.test.ts +18 -0
- package/components/formatter/PodsUsage.vue +11 -36
- package/components/formatter/PrincipalGroupBindings.vue +8 -5
- package/components/formatter/__tests__/PodsUsage.test.ts +36 -19
- package/components/nav/Group.vue +62 -34
- package/components/nav/Header.vue +13 -6
- package/components/nav/Pinned.vue +47 -0
- package/components/nav/TopLevelMenu.vue +673 -325
- package/components/nav/Type.vue +88 -8
- package/config/home-links.js +1 -1
- package/config/product/istio.js +15 -5
- package/config/router.js +3 -9
- package/config/table-headers.js +5 -6
- package/config/uiplugins.js +1 -0
- package/core/plugin-helpers.js +3 -0
- package/core/types.ts +6 -1
- package/creators/app/files/.vscode/settings.json +0 -1
- package/creators/pkg/init +2 -2
- package/detail/__tests__/autoscaling.horizontalpodautoscaler.test.ts +118 -0
- package/detail/autoscaling.horizontalpodautoscaler/index.vue +4 -4
- package/detail/provisioning.cattle.io.cluster.vue +7 -5
- package/edit/__tests__/management.cattle.io.clusterroletemplatebinding.test.ts +58 -0
- package/edit/__tests__/namespace.test.ts +5 -3
- package/edit/fleet.cattle.io.gitrepo.vue +43 -15
- package/edit/logging.banzaicloud.io.output/index.vue +7 -0
- package/edit/management.cattle.io.clusterroletemplatebinding.vue +3 -11
- package/edit/namespace.vue +8 -4
- package/edit/provisioning.cattle.io.cluster/Basics.vue +662 -0
- package/edit/provisioning.cattle.io.cluster/CustomCommand.vue +9 -8
- package/edit/provisioning.cattle.io.cluster/DrainOptions.vue +13 -8
- package/edit/provisioning.cattle.io.cluster/MachinePool.vue +11 -2
- package/edit/provisioning.cattle.io.cluster/MemberRoles.vue +40 -0
- package/edit/provisioning.cattle.io.cluster/__tests__/Basics.tests.ts +237 -0
- package/edit/provisioning.cattle.io.cluster/__tests__/CustomCommand.tests.ts +71 -23
- package/edit/provisioning.cattle.io.cluster/__tests__/DrainOptions.test.ts +52 -0
- package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +65 -142
- package/edit/provisioning.cattle.io.cluster/rke2.vue +253 -582
- package/edit/workload/storage/ContainerMountPaths.vue +7 -5
- package/edit/workload/storage/__tests__/Storage.test.ts +2 -2
- package/edit/workload/storage/persistentVolumeClaim/__tests__/persistentvolumeclaim.test.ts +36 -0
- package/edit/workload/storage/persistentVolumeClaim/persistentvolumeclaim.vue +15 -7
- package/initialize/App.js +2 -0
- package/initialize/client.js +63 -51
- package/initialize/index.js +7 -5
- package/layouts/default.vue +10 -2
- package/layouts/home.vue +6 -2
- package/layouts/plain.vue +9 -2
- package/list/fleet.cattle.io.cluster.vue +2 -2
- package/list/management.cattle.io.feature.vue +1 -1
- package/machine-config/amazonec2.vue +1 -0
- package/machine-config/vmwarevsphere.vue +48 -7
- package/mixins/brand.js +0 -8
- package/mixins/child-hook.js +2 -2
- package/mixins/create-edit-view/impl.js +3 -3
- package/mixins/fetch.client.js +3 -3
- package/models/__tests__/management.cattle.io.node.ts +96 -0
- package/models/__tests__/node.ts +74 -0
- package/models/cluster/node.js +6 -5
- package/models/cluster.x-k8s.io.machinedeployment.js +2 -2
- package/models/management.cattle.io.cluster.js +22 -1
- package/models/management.cattle.io.clusterroletemplatebinding.js +3 -3
- package/models/management.cattle.io.globalrole.js +17 -2
- package/models/management.cattle.io.node.js +6 -4
- package/models/management.cattle.io.projectroletemplatebinding.js +3 -3
- package/models/management.cattle.io.roletemplate.js +17 -2
- package/package.json +2 -6
- package/pages/__tests__/prefs.test.ts +1 -1
- package/pages/about.vue +2 -0
- package/pages/auth/setup.vue +5 -4
- package/pages/c/_cluster/explorer/ConfigBadge.vue +1 -0
- package/pages/c/_cluster/monitoring/index.vue +8 -3
- package/pages/c/_cluster/uiplugins/CatalogList/CatalogLoadDialog.vue +9 -66
- package/pages/c/_cluster/uiplugins/CatalogList/CatalogUninstallDialog.vue +182 -0
- package/pages/c/_cluster/uiplugins/CatalogList/index.vue +15 -32
- package/pages/c/_cluster/uiplugins/UninstallDialog.vue +8 -46
- package/pages/c/_cluster/uiplugins/index.vue +64 -64
- package/pages/diagnostic.vue +0 -39
- package/pages/home.vue +1 -1
- package/pages/prefs.vue +3 -13
- package/plugins/dashboard-store/normalize.js +4 -4
- package/plugins/dashboard-store/resource-class.js +1 -1
- package/plugins/int-number.js +5 -2
- package/plugins/positive-int-number.js +19 -0
- package/plugins/steve/__tests__/getters.spec.ts +15 -0
- package/plugins/steve/getters.js +22 -10
- package/public/index.html +4 -2
- package/rancher-components/BadgeState/BadgeState.vue +5 -1
- package/rancher-components/Banner/Banner.test.ts +51 -1
- package/rancher-components/Banner/Banner.vue +134 -53
- package/rancher-components/Card/Card.test.ts +37 -0
- package/rancher-components/Card/Card.vue +24 -7
- package/rancher-components/Form/Checkbox/Checkbox.test.ts +20 -29
- package/rancher-components/Form/Checkbox/Checkbox.vue +45 -20
- package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +2 -8
- package/rancher-components/Form/LabeledInput/LabeledInput.vue +22 -10
- package/rancher-components/Form/Radio/RadioButton.test.ts +31 -0
- package/rancher-components/Form/Radio/RadioButton.vue +30 -13
- package/rancher-components/Form/Radio/RadioGroup.vue +26 -7
- package/rancher-components/Form/TextArea/TextAreaAutoGrow.vue +7 -6
- package/rancher-components/Form/ToggleSwitch/ToggleSwitch.test.ts +25 -38
- package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +23 -11
- package/rancher-components/LabeledTooltip/LabeledTooltip.vue +19 -5
- package/rancher-components/StringList/StringList.test.ts +453 -49
- package/rancher-components/StringList/StringList.vue +92 -58
- package/scripts/extension/parse-tag-name +0 -0
- package/store/index.js +4 -0
- package/store/prefs.js +4 -4
- package/store/type-map.js +2 -16
- package/types/shell/index.d.ts +26 -14
- package/utils/__tests__/cluster.test.ts +55 -0
- package/utils/__tests__/object.test.ts +21 -2
- package/utils/__tests__/sort.test.ts +61 -0
- package/utils/cluster.js +47 -1
- package/utils/object.js +12 -5
- package/utils/string.js +12 -0
- package/utils/validators/formRules/__tests__/index.test.ts +13 -1
- package/utils/validators/formRules/index.ts +4 -0
- package/utils/validators/role-template.js +9 -1
- package/utils/version.js +1 -1
- package/vue.config.js +1 -4
- package/yarn-error.log +200 -0
- package/content/docs/en-us/getting-started.md +0 -224
- package/content/docs/en-us/whats-new.md +0 -29
- package/content/docs/zh-hans/getting-started.md +0 -224
- package/content/docs/zh-hans/whats-new.md +0 -28
- package/pages/docs/_doc.vue +0 -345
- package/pages/docs/toc.js +0 -27
- 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,
|
|
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(
|
|
25
|
-
const user = store.dispatch(
|
|
26
|
-
const cur = store.dispatch(
|
|
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);
|
package/plugins/int-number.js
CHANGED
|
@@ -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
|
|
8
|
+
const inputChar = String.fromCharCode(charcode);
|
|
9
9
|
|
|
10
|
-
|
|
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
|
});
|
package/plugins/steve/getters.js
CHANGED
|
@@ -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
|
-
|
|
46
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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
|
|
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(
|
|
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
|
-
<
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
213
|
+
padding-right: $icon-size * 2;
|
|
101
214
|
}
|
|
102
215
|
|
|
103
|
-
|
|
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:
|
|
113
|
-
line-height:
|
|
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
|
-
&.
|
|
128
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
81
|
+
<div
|
|
82
|
+
v-if="showActions"
|
|
83
|
+
class="card-actions"
|
|
84
|
+
data-testid="card-actions-slot"
|
|
85
|
+
>
|
|
72
86
|
<slot name="actions">
|
|
73
|
-
<button
|
|
87
|
+
<button
|
|
88
|
+
class="btn role-primary"
|
|
89
|
+
@click="buttonAction"
|
|
90
|
+
>
|
|
74
91
|
{{ buttonText }}
|
|
75
92
|
</button>
|
|
76
93
|
</slot>
|