@rancher/shell 3.0.8 → 3.0.9-rc.2
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/apis/intf/modal.ts +38 -0
- package/apis/intf/slide-in.ts +3 -1
- package/apis/shell/__tests__/slide-in.test.ts +36 -0
- package/apis/shell/slide-in.ts +5 -1
- package/assets/styles/base/_color.scss +1 -0
- package/assets/styles/base/_typography.scss +14 -5
- package/assets/styles/themes/_light.scss +1 -1
- package/assets/styles/themes/_modern.scss +1 -1
- package/assets/translations/en-us.yaml +94 -33
- package/assets/translations/zh-hans.yaml +0 -2
- package/components/ActionMenuShell.vue +4 -4
- package/components/CodeMirror.vue +4 -3
- package/components/DetailText.vue +54 -7
- package/components/Drawer/Chrome.vue +11 -4
- package/components/Drawer/DrawerCard.vue +19 -0
- package/components/Drawer/ResourceDetailDrawer/ConfigTab.vue +3 -11
- package/components/Drawer/ResourceDetailDrawer/__tests__/ConfigTab.test.ts +2 -2
- package/components/Drawer/ResourceDetailDrawer/index.vue +3 -20
- package/components/Drawer/types.ts +1 -0
- package/components/DynamicContent/DynamicContentCloseButton.vue +2 -2
- package/components/LocaleSelector.vue +1 -1
- package/components/Markdown.vue +1 -1
- package/components/PopoverCard.vue +3 -3
- package/components/Resource/Detail/Card/ExtrasCard.vue +39 -0
- package/components/Resource/Detail/Card/StateCard/__tests__/composables.test.ts +142 -0
- package/components/Resource/Detail/Card/StateCard/composables.ts +41 -11
- package/components/Resource/Detail/Card/StateCard/index.vue +3 -9
- package/components/Resource/Detail/Card/StateCard/types.ts +6 -0
- package/components/Resource/Detail/Card/{PodsCard → StatusCard}/index.vue +11 -10
- package/components/Resource/Detail/Card/__tests__/PodsCard.test.ts +24 -25
- package/components/Resource/Detail/Cards.vue +27 -0
- package/components/Resource/Detail/Masthead/__tests__/index.test.ts +70 -0
- package/components/Resource/Detail/Masthead/index.vue +5 -0
- package/components/Resource/Detail/Metadata/KeyValueRow.vue +4 -2
- package/components/Resource/Detail/ResourcePopover/ResourcePopoverCard.vue +2 -2
- package/components/Resource/Detail/ResourceRow.types.ts +14 -0
- package/components/Resource/Detail/ResourceRow.vue +23 -35
- package/components/Resource/Detail/StatusRow.vue +5 -2
- package/components/Resource/Detail/TitleBar/__tests__/composables.test.ts +38 -7
- package/components/Resource/Detail/TitleBar/__tests__/index.test.ts +106 -2
- package/components/Resource/Detail/TitleBar/composables.ts +2 -1
- package/components/Resource/Detail/TitleBar/index.vue +41 -6
- package/components/ResourceDetail/Masthead/__tests__/index.test.ts +49 -1
- package/components/ResourceDetail/Masthead/__tests__/latest.test.ts +85 -0
- package/components/ResourceDetail/Masthead/index.vue +1 -0
- package/components/ResourceDetail/Masthead/latest.vue +8 -1
- package/components/ResourceDetail/Masthead/legacy.vue +1 -1
- package/components/Setting.vue +1 -1
- package/components/SortableTable/index.vue +25 -0
- package/components/SortableTable/selection.js +25 -12
- package/components/SortableTable/sorting.js +1 -1
- package/components/Tabbed/Tab.vue +1 -0
- package/components/Tabbed/index.vue +29 -6
- package/components/Window/ContainerShell.vue +10 -13
- package/components/fleet/FleetClusterTargets/TargetsList.vue +47 -29
- package/components/fleet/FleetClusterTargets/index.vue +82 -29
- package/components/fleet/FleetClusters.vue +26 -12
- package/components/fleet/FleetGitRepoPaths.vue +2 -2
- package/components/fleet/FleetResources.vue +14 -0
- package/components/fleet/FleetValuesFrom.vue +2 -2
- package/components/fleet/__tests__/FleetClusterTargets.test.ts +531 -0
- package/components/fleet/__tests__/FleetClusters.test.ts +576 -0
- package/components/fleet/dashboard/ResourceDetails.vue +96 -123
- package/components/form/Conditions.vue +1 -15
- package/components/form/HookOption.vue +5 -0
- package/components/form/LabeledSelect.vue +1 -1
- package/components/form/LifecycleHooks.vue +2 -6
- package/components/form/ResourceLabeledSelect.vue +12 -1
- package/components/form/SeccompProfile.vue +113 -0
- package/components/form/Security.vue +244 -133
- package/components/form/__tests__/LabeledSelect.test.ts +1 -1
- package/components/form/__tests__/SeccompProfile.test.js +124 -0
- package/components/form/__tests__/Security.test.ts +125 -37
- package/components/formatter/Autoscaler.vue +2 -2
- package/components/formatter/FleetSummaryGraph.vue +4 -1
- package/components/nav/Group.vue +5 -0
- package/components/nav/Header.vue +3 -3
- package/components/nav/HeaderPageActionMenu.vue +1 -1
- package/components/nav/NamespaceFilter.vue +6 -6
- package/components/nav/NotificationCenter/index.vue +1 -1
- package/components/nav/TopLevelMenu.helper.ts +41 -16
- package/components/nav/TopLevelMenu.vue +45 -25
- package/components/nav/WorkspaceSwitcher.vue +1 -1
- package/components/nav/__tests__/TopLevelMenu.helper.test.ts +277 -0
- package/components/nav/__tests__/TopLevelMenu.test.ts +160 -4
- package/components/templates/default.vue +0 -3
- package/components/templates/home.vue +0 -3
- package/components/templates/plain.vue +0 -3
- package/composables/useClickOutside.ts +1 -1
- package/config/product/explorer.js +1 -2
- package/config/types.js +41 -8
- package/detail/__tests__/workload.test.ts +8 -16
- package/detail/catalog.cattle.io.app.vue +6 -0
- package/detail/fleet.cattle.io.cluster.vue +6 -0
- package/detail/workload/index.vue +7 -109
- package/edit/__tests__/projectsecret.test.ts +42 -0
- package/edit/auth/__tests__/oidc.test.ts +50 -0
- package/edit/auth/oidc.vue +68 -44
- package/edit/autoscaling.horizontalpodautoscaler/index.vue +140 -59
- package/edit/autoscaling.horizontalpodautoscaler/metrics-row.vue +41 -5
- package/edit/projectsecret.vue +29 -0
- package/edit/provisioning.cattle.io.cluster/__tests__/Basics.test.ts +89 -200
- package/edit/provisioning.cattle.io.cluster/__tests__/Networking.test.ts +58 -17
- package/edit/provisioning.cattle.io.cluster/rke2.vue +11 -0
- package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +3 -63
- package/edit/provisioning.cattle.io.cluster/tabs/networking/index.vue +82 -14
- package/edit/workload/__tests__/index.test.ts +122 -85
- package/edit/workload/index.vue +48 -29
- package/edit/workload/mixins/workload.js +85 -32
- package/list/catalog.cattle.io.clusterrepo.vue +1 -1
- package/list/projectsecret.vue +2 -2
- package/machine-config/__tests__/vmwarevsphere.test.ts +64 -0
- package/machine-config/amazonec2.vue +2 -2
- package/machine-config/vmwarevsphere.vue +58 -4
- package/mixins/__tests__/brand.spec.ts +18 -13
- package/mixins/__tests__/chart.test.ts +63 -0
- package/mixins/chart.js +56 -51
- package/models/__tests__/catalog.cattle.io.app.test.ts +33 -0
- package/models/__tests__/workload.test.ts +333 -0
- package/models/catalog.cattle.io.app.js +8 -0
- package/models/pod.js +14 -0
- package/models/secret.js +1 -1
- package/models/workload.js +93 -27
- package/package.json +4 -4
- package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +91 -0
- package/pages/c/_cluster/apps/charts/install.vue +4 -4
- package/pages/c/_cluster/explorer/EventsTable.vue +2 -2
- package/pages/c/_cluster/fleet/index.vue +18 -12
- package/pages/c/_cluster/manager/hostedprovider/index.vue +1 -19
- package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +1 -1
- package/pages/c/_cluster/uiplugins/index.vue +1 -1
- package/plugins/dashboard-store/__tests__/resource-class.test.ts +234 -0
- package/plugins/dashboard-store/actions.js +9 -8
- package/plugins/dashboard-store/resource-class.js +97 -1
- package/plugins/steve/__tests__/revision.test.ts +84 -0
- package/plugins/steve/__tests__/steve-pagination-utils.test.ts +30 -0
- package/plugins/steve/__tests__/subscribe.spec.ts +134 -0
- package/plugins/steve/mutations.js +9 -0
- package/plugins/steve/revision.ts +26 -0
- package/plugins/steve/steve-pagination-utils.ts +6 -5
- package/plugins/steve/subscribe.js +211 -51
- package/plugins/subscribe-events.ts +2 -2
- package/rancher-components/Form/Checkbox/Checkbox.vue +13 -0
- package/rancher-components/LabeledTooltip/LabeledTooltip.vue +1 -1
- package/rancher-components/Pill/RcCounterBadge/RcCounterBadge.vue +1 -1
- package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +3 -1
- package/rancher-components/Pill/RcStatusIndicator/RcStatusIndicator.vue +3 -1
- package/rancher-components/Pill/RcTag/RcTag.vue +1 -1
- package/rancher-components/Pill/index.ts +4 -0
- package/rancher-components/RcButton/RcButton.test.ts +53 -9
- package/rancher-components/RcButton/RcButton.vue +217 -25
- package/rancher-components/RcButton/types.ts +27 -1
- package/rancher-components/RcDropdown/RcDropdownMenu.vue +4 -4
- package/rancher-components/RcDropdown/types.ts +3 -3
- package/rancher-components/RcIcon/RcIcon.test.ts +42 -0
- package/rancher-components/RcIcon/RcIcon.vue +9 -6
- package/rancher-components/RcIcon/types.ts +13 -9
- package/rancher-components/utils/status.test.ts +10 -15
- package/rancher-components/utils/status.ts +5 -6
- package/store/aws.js +18 -12
- package/store/index.js +4 -8
- package/store/type-map.utils.ts +1 -1
- package/types/kube/kube-api.ts +29 -3
- package/types/rancher/steve.api.ts +40 -0
- package/types/shell/index.d.ts +99 -0
- package/types/store/dashboard-store.types.ts +29 -7
- package/types/store/pagination.types.ts +1 -0
- package/types/store/subscribe-events.types.ts +1 -0
- package/utils/__tests__/azure.test.ts +56 -0
- package/utils/__tests__/back-off.test.ts +364 -245
- package/utils/__tests__/error.test.ts +44 -0
- package/utils/__tests__/fleet.test.ts +8 -1
- package/utils/__tests__/pagination-wrapper.test.ts +167 -0
- package/utils/__tests__/version.test.ts +55 -1
- package/utils/azure.js +12 -0
- package/utils/back-off.ts +302 -69
- package/utils/cspAdaptor.ts +32 -14
- package/utils/dynamic-content/__tests__/index.test.ts +1 -1
- package/utils/dynamic-content/__tests__/new-release.test.ts +48 -7
- package/utils/dynamic-content/__tests__/support-notice.test.ts +1 -4
- package/utils/dynamic-content/index.ts +1 -6
- package/utils/dynamic-content/new-release.ts +5 -3
- package/utils/dynamic-content/types.d.ts +0 -1
- package/utils/error.js +9 -0
- package/utils/fleet.ts +2 -2
- package/utils/inactivity.ts +2 -3
- package/utils/pagination-wrapper.ts +101 -17
- package/utils/validators/formRules/index.ts +3 -0
- package/utils/version.js +38 -0
- package/components/auth/AzureWarning.vue +0 -77
- /package/components/Resource/Detail/{Card/PodsCard/Bubble.vue → Bubble.vue} +0 -0
- /package/components/Resource/Detail/Card/{PodsCard → StatusCard}/composable.ts +0 -0
|
@@ -20,40 +20,67 @@
|
|
|
20
20
|
*
|
|
21
21
|
* Below are some VERY brief steps for common flows. Some will link together
|
|
22
22
|
*
|
|
23
|
-
* Successfully flow
|
|
23
|
+
* # Successfully flow
|
|
24
|
+
* ## watch - standard mode
|
|
24
25
|
* 1. UI --> Rancher: _watch_ request
|
|
25
26
|
* 2. Rancher --> UI: `resource.start`. UI sets watch as started
|
|
26
27
|
* ...
|
|
27
28
|
* 3. Rancher --> UI: `resource.change` (contains data). UI caches data
|
|
28
29
|
*
|
|
29
|
-
*
|
|
30
|
+
* ## watch - new resource.changes mode
|
|
30
31
|
* 1. UI --> Rancher: _watch_ request
|
|
31
32
|
* 2. Rancher --> UI: `resource.start`. UI sets watch as started
|
|
32
33
|
* ...
|
|
33
34
|
* 3. Rancher --> UI: `resource.changes` (contains no data). UI makes a HTTP request to fetch data
|
|
34
35
|
*
|
|
35
|
-
*
|
|
36
|
+
* ## watch - unwatch
|
|
36
37
|
* 1. UI --> Rancher: _unwatch_ request
|
|
37
38
|
* 2. Rancher --> UI: `resource.stop`. UI sets watch as stopped
|
|
38
39
|
*
|
|
39
|
-
*
|
|
40
|
+
* ## watch - resource.stop received
|
|
40
41
|
* 1. Rancher --> UI: `resource.stop`. UI sets watch as stopped
|
|
41
42
|
* 2. UI --> Rancher: _watch_ request
|
|
42
43
|
*
|
|
43
|
-
*
|
|
44
|
+
* ## watch - socket disconnected
|
|
44
45
|
* 1. Socket closes|disconnects (not sure which)
|
|
45
46
|
* 2. UI: reopens socket
|
|
46
47
|
* 3. UI --> Rancher: _watch_ request (for every started watch)
|
|
47
48
|
*
|
|
48
|
-
* Error Flow
|
|
49
|
+
* # Error Flow
|
|
50
|
+
* ## resource.error
|
|
49
51
|
* 1. UI --> Rancher: _watch_ request
|
|
50
52
|
* 2. Rancher --> UI: `resource.start`. UI sets watch as started
|
|
51
53
|
* 3. Rancher --> UI: `resource.error`. UI sets watch as errored.
|
|
52
54
|
* a) UI: in the event of 'too old' the UI will make a http request to fetch a new revision and re-watch with it. This process is delayed on each call
|
|
53
55
|
* 4. Rancher --> UI: `resource.stop`. UI sets watch as stop (note the resource.stop flow above is avoided given error state)
|
|
54
56
|
*
|
|
57
|
+
* # HA Support for Stale Replicates - https://github.com/rancher/dashboard/issues/14974
|
|
58
|
+
*
|
|
59
|
+
* ## Scenario 1 - handle case where watch request is handled by a stale replica
|
|
60
|
+
* 1. UI --> Rancher: _watch_ request (contains latest revision)
|
|
61
|
+
* 2. Rancher --> UI: `resource.error` (stale replica does not know new revision)
|
|
62
|
+
* 3. Rancher --> UI: `resource.stop` (stale replica cannot provide updates for unknown revision)
|
|
63
|
+
* 4. UI --> Rancher : UI makes a HTTP request to fetch data
|
|
64
|
+
* 5. Loop back to step 1 (if stale again, backoff retry)
|
|
65
|
+
*
|
|
66
|
+
* ## Scenario 2 - handle case where http request is handled by a stale replica (don't fetch stale data)
|
|
67
|
+
* 1. UI --> Rancher: _watch_ request
|
|
68
|
+
* 2. Rancher --> UI: `resource.start`. UI sets watch as started
|
|
69
|
+
* ...
|
|
70
|
+
* 3. Rancher --> UI: `resource.changes` (sent by good replica containing good revision)
|
|
71
|
+
* 4. UI --> Rancher : UI makes a HTTP request to fetch data. Stale Replica handles request, does not know revision, returns error
|
|
72
|
+
* 5. Loop back to step 4 (if errors with stale again, backoff retry)
|
|
73
|
+
*
|
|
74
|
+
* ## Scenario 3 - handle case where update request was sent by stale replica (don't overwrite good data with stale)
|
|
75
|
+
* 1. UI --> Rancher: _watch_ request
|
|
76
|
+
* 2. Rancher --> UI: `resource.start`. UI sets watch as started
|
|
77
|
+
* ...
|
|
78
|
+
* 3. Rancher --> UI: `resource.changes` (sent by stale replica containing stale revision)
|
|
79
|
+
* 4. UI compares stale revision with newer store revision
|
|
80
|
+
* 5. UI does not make new http request, which could be handled by stale replica --> overwrites newer local values
|
|
81
|
+
*
|
|
55
82
|
* Additionally
|
|
56
|
-
* - if we receive resource.stop, unless the watch is in error, we immediately send back a watch
|
|
83
|
+
* - if we receive resource.stop, unless the watch is in error, we immediately send back a watch request to re-start the watch
|
|
57
84
|
* - if the web socket is disconnected (for steve based sockets it happens every 30 mins, or when there are permission changes)
|
|
58
85
|
* the ui will re-connect it and re-watch all previous watches using a best effort revision
|
|
59
86
|
*/
|
|
@@ -68,7 +95,6 @@ import Socket, {
|
|
|
68
95
|
EVENT_CONNECTED,
|
|
69
96
|
EVENT_DISCONNECTED,
|
|
70
97
|
EVENT_MESSAGE,
|
|
71
|
-
// EVENT_FRAME_TIMEOUT,
|
|
72
98
|
EVENT_CONNECT_ERROR,
|
|
73
99
|
EVENT_DISCONNECT_ERROR,
|
|
74
100
|
NO_WATCH,
|
|
@@ -90,6 +116,8 @@ import { STEVE_WATCH_EVENT_TYPES, STEVE_WATCH_MODE } from '@shell/types/store/su
|
|
|
90
116
|
import paginationUtils from '@shell/utils/pagination-utils';
|
|
91
117
|
import backOff from '@shell/utils/back-off';
|
|
92
118
|
import { SteveWatchEventListenerManager } from '@shell/plugins/subscribe-events';
|
|
119
|
+
import { SteveRevision } from '@shell/plugins/steve/revision';
|
|
120
|
+
import { STEVE_RESPONSE_CODE } from '@shell/types/rancher/steve.api';
|
|
93
121
|
|
|
94
122
|
// minimum length of time a disconnect notification is shown
|
|
95
123
|
const MINIMUM_TIME_NOTIFIED = 3000;
|
|
@@ -683,7 +711,7 @@ const sharedActions = {
|
|
|
683
711
|
/**
|
|
684
712
|
* Ensure there's no back-off process waiting to run for
|
|
685
713
|
* - resource.changes fetchResources
|
|
686
|
-
* - resource.error
|
|
714
|
+
* - resource.error resyncWatch
|
|
687
715
|
*/
|
|
688
716
|
resetWatchBackOff({ state, getters, commit }, {
|
|
689
717
|
type, compareWatches, resetInError = true, resetStarted = true
|
|
@@ -820,17 +848,117 @@ const defaultActions = {
|
|
|
820
848
|
async resyncWatch({ getters, dispatch }, params) {
|
|
821
849
|
console.info(`Resync [${ getters.storeName }]`, params); // eslint-disable-line no-console
|
|
822
850
|
|
|
851
|
+
const { backOffId, ...others } = params;
|
|
852
|
+
|
|
823
853
|
await dispatch('fetchResources', {
|
|
824
|
-
|
|
825
|
-
|
|
854
|
+
params: others,
|
|
855
|
+
backOffId,
|
|
856
|
+
opt: { force: true, forceWatch: true }
|
|
826
857
|
});
|
|
827
858
|
},
|
|
828
859
|
|
|
860
|
+
/**
|
|
861
|
+
* Helper function used by fetchResources
|
|
862
|
+
*
|
|
863
|
+
* Integrates the concept of 'back-off' to reduce spam, overwrite stale old requests, etc
|
|
864
|
+
*/
|
|
865
|
+
async fetchPageResources({ getters, dispatch }, {
|
|
866
|
+
opt, storePagination, params, backOffId
|
|
867
|
+
}) {
|
|
868
|
+
const { resourceType, namespace, revision } = params;
|
|
869
|
+
const type = resourceType || params.type;
|
|
870
|
+
|
|
871
|
+
const safeBackOffId = backOffId || getters.backOffId(params, `fetchPageResources`);
|
|
872
|
+
|
|
873
|
+
const activeRevisionSt = backOff.getBackOff(safeBackOffId)?.metadata?.revision;
|
|
874
|
+
const cachedRevisionSt = getters['typeEntry'](resourceType || type)?.revision;
|
|
875
|
+
|
|
876
|
+
const targetRevision = new SteveRevision(revision);
|
|
877
|
+
const activeRevision = new SteveRevision(activeRevisionSt);
|
|
878
|
+
const cachedRevision = new SteveRevision(cachedRevisionSt);
|
|
879
|
+
const currentRevision = new SteveRevision(activeRevisionSt || cachedRevisionSt);
|
|
880
|
+
|
|
881
|
+
// Three cases to support HA scenarios 2 + 3
|
|
882
|
+
// 1. current version is newer than target revision - abort/ignore (don't overwrite new with old)
|
|
883
|
+
// 2. current version is older than target revision - reset previous (drop older requests with older revision, use new revision)
|
|
884
|
+
// 3. current version is same as target revision - we're retrying
|
|
885
|
+
|
|
886
|
+
// There are two places we do this to cover the two cases we make http request following socket changes
|
|
887
|
+
// shell/utils/pagination-wrapper.ts - request
|
|
888
|
+
// shell/plugins/steve/subscribe.js - fetchPageResources
|
|
889
|
+
|
|
890
|
+
if (currentRevision.isNewerThan(targetRevision)) {
|
|
891
|
+
// Case 1 - abort/ignore (don't overwrite new with old)
|
|
892
|
+
|
|
893
|
+
// eslint-disable-next-line no-console
|
|
894
|
+
console.warn(`Ignoring subscribe request to update '${ type }' with revision '${ targetRevision.revision }' (active revision '${ currentRevision.revision } & cached revision '${ cachedRevision.revision }''). ` +
|
|
895
|
+
`This probably means the replica that provided the web socket message has not yet correctly synced it's cache with other fresher replicas.`);
|
|
896
|
+
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
if (targetRevision.isNewerThan(activeRevision)) {
|
|
901
|
+
// Case 2 - reset previous (drop older requests with older revision, use new revision)
|
|
902
|
+
|
|
903
|
+
console.info(`Dropping previous subscribe request to update '${ type }' with revision '${ currentRevision.revision }' (new target revision '${ targetRevision.revision }'). `); // eslint-disable-line no-console
|
|
904
|
+
|
|
905
|
+
backOff.reset(safeBackOffId);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
try {
|
|
909
|
+
// Keep making requests until we make one that succeeds, fails with unknown revision or we run out of retries
|
|
910
|
+
await backOff.recurse({
|
|
911
|
+
id: safeBackOffId,
|
|
912
|
+
metadata: { revision },
|
|
913
|
+
description: `Fetching resources for ${ type }. Triggered by web socket`,
|
|
914
|
+
canFn: () => {
|
|
915
|
+
if (!getters.canBackoff(this.$socket)) {
|
|
916
|
+
console.info(`Aborting subscribe request to update '${ type }' with revision '${ currentRevision.revision }' (socket closed). `); // eslint-disable-line no-console
|
|
917
|
+
|
|
918
|
+
return false;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
if (!getters['watchStarted'](params)) {
|
|
922
|
+
// No watch has started... but are we in initial state where the watch failed due to a bad revision?
|
|
923
|
+
const inError = getters.inError(params);
|
|
924
|
+
|
|
925
|
+
if (inError !== REVISION_TOO_OLD) {
|
|
926
|
+
console.info(`Aborting subscribe request to update '${ type }' with revision '${ currentRevision.revision }' (resource not watched). `); // eslint-disable-line no-console
|
|
927
|
+
|
|
928
|
+
return false;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
return true;
|
|
933
|
+
},
|
|
934
|
+
continueOnError: async(err) => {
|
|
935
|
+
// Have we made a request to a stale replica that does not know about the required revision? If so continue to try until we hit a ripe replica
|
|
936
|
+
return err?.status === 400 && err?.code === STEVE_RESPONSE_CODE.UNKNOWN_REVISION;
|
|
937
|
+
},
|
|
938
|
+
delayedFn: async() => {
|
|
939
|
+
return await dispatch('findPage', {
|
|
940
|
+
type,
|
|
941
|
+
opt: {
|
|
942
|
+
...opt,
|
|
943
|
+
namespaced: namespace,
|
|
944
|
+
revision,
|
|
945
|
+
// This brings in page, page size, filter, etc
|
|
946
|
+
...storePagination.request,
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
},
|
|
950
|
+
});
|
|
951
|
+
} catch (err) {
|
|
952
|
+
// Nothing depends on the error higher in the call stack, so prevent dev full screen errors by catching it
|
|
953
|
+
console.info(`Failed subscribe request to update '${ type }' with revision '${ currentRevision.revision }' (error). `, err); // eslint-disable-line no-console
|
|
954
|
+
}
|
|
955
|
+
},
|
|
956
|
+
|
|
829
957
|
async fetchResources({
|
|
830
958
|
state, getters, dispatch, commit
|
|
831
|
-
}, { opt,
|
|
959
|
+
}, { opt, params, backOffId }) {
|
|
832
960
|
const {
|
|
833
|
-
resourceType, namespace, id, selector, mode
|
|
961
|
+
resourceType, namespace, id, selector, mode, revision
|
|
834
962
|
} = params;
|
|
835
963
|
|
|
836
964
|
if (!resourceType) {
|
|
@@ -840,6 +968,7 @@ const defaultActions = {
|
|
|
840
968
|
}
|
|
841
969
|
|
|
842
970
|
if ( id ) {
|
|
971
|
+
// Fetch an individual resource
|
|
843
972
|
await dispatch('find', {
|
|
844
973
|
type: resourceType,
|
|
845
974
|
id,
|
|
@@ -857,6 +986,7 @@ const defaultActions = {
|
|
|
857
986
|
let have = []; let want = [];
|
|
858
987
|
|
|
859
988
|
if ( selector ) {
|
|
989
|
+
// Fetch a selection of resources
|
|
860
990
|
have = getters['matching'](resourceType, selector).slice();
|
|
861
991
|
want = await dispatch('findMatching', {
|
|
862
992
|
type: resourceType,
|
|
@@ -864,39 +994,40 @@ const defaultActions = {
|
|
|
864
994
|
opt,
|
|
865
995
|
});
|
|
866
996
|
} else {
|
|
997
|
+
// Fetch all or a page of resources
|
|
867
998
|
if (mode === STEVE_WATCH_MODE.RESOURCE_CHANGES) {
|
|
999
|
+
// Fetch a page of resources
|
|
1000
|
+
|
|
868
1001
|
// Other findX use options (id/ns/selector) from the messages received over socket.
|
|
869
|
-
// However paginated requests have more complex params so grab them from
|
|
1002
|
+
// However paginated requests have more complex params so grab them from the store.
|
|
1003
|
+
|
|
870
1004
|
// of type @StorePagination
|
|
871
1005
|
const storePagination = getters['havePage'](resourceType);
|
|
872
1006
|
|
|
873
1007
|
if (!!storePagination) {
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
// (see deferred process - `waiting.push(later);` - in request action).
|
|
880
|
-
// If this becomes an issue we need to debounce and work around the deferred issue within request
|
|
881
|
-
want = await dispatch('findPage', {
|
|
882
|
-
type: resourceType,
|
|
883
|
-
opt: {
|
|
884
|
-
...opt,
|
|
885
|
-
namespaced: namespace,
|
|
886
|
-
// This brings in page, page size, filter, etc
|
|
887
|
-
...storePagination.request
|
|
888
|
-
}
|
|
1008
|
+
await dispatch('fetchPageResources', {
|
|
1009
|
+
params,
|
|
1010
|
+
storePagination,
|
|
1011
|
+
opt,
|
|
1012
|
+
backOffId
|
|
889
1013
|
});
|
|
1014
|
+
|
|
1015
|
+
// findPage removes stale entries, so we don't need to rely on below process to remove them
|
|
1016
|
+
have = [];
|
|
1017
|
+
want = [];
|
|
890
1018
|
}
|
|
1019
|
+
|
|
891
1020
|
// Should any listeners be notified of this request for them to kick off their own event handling?
|
|
892
1021
|
getters.listenerManager.triggerEventListener({
|
|
893
1022
|
event: STEVE_WATCH_MODE.RESOURCE_CHANGES,
|
|
894
1023
|
params: {
|
|
895
1024
|
...params,
|
|
896
|
-
|
|
1025
|
+
revision,
|
|
1026
|
+
forceWatch: opt.forceWatch,
|
|
897
1027
|
}
|
|
898
1028
|
});
|
|
899
1029
|
} else {
|
|
1030
|
+
// Fetch all of a resource
|
|
900
1031
|
have = getters['all'](resourceType).slice();
|
|
901
1032
|
|
|
902
1033
|
if ( namespace ) {
|
|
@@ -1110,7 +1241,7 @@ const defaultActions = {
|
|
|
1110
1241
|
// 2) will be cleared when resyncWatch --> watch (with force) --> resource.start completes
|
|
1111
1242
|
commit('setInError', { msg, reason: REVISION_TOO_OLD });
|
|
1112
1243
|
|
|
1113
|
-
//
|
|
1244
|
+
// HA scenario 1 - handle case where stale replica processes watch request
|
|
1114
1245
|
// The watch that results from resyncWatch will fail and end up here if the revision isn't (yet) known
|
|
1115
1246
|
// So re-retry resyncWatch until it does OR
|
|
1116
1247
|
// - we're already re-retrying
|
|
@@ -1120,11 +1251,17 @@ const defaultActions = {
|
|
|
1120
1251
|
// - we need to stop (socket is disconnected or closed, type is 'forgotten', watch is unwatched)
|
|
1121
1252
|
// - `reset` called asynchronously
|
|
1122
1253
|
// - Note - we won't need to clear the id outside of the above scenarios because `too old` only occurs on fresh watches (covered by above scenarios)
|
|
1254
|
+
|
|
1255
|
+
const backOffId = getters.backOffId(msg, REVISION_TOO_OLD);
|
|
1256
|
+
|
|
1123
1257
|
backOff.execute({
|
|
1124
|
-
id:
|
|
1258
|
+
id: backOffId,
|
|
1125
1259
|
description: `Invalid watch revision, re-syncing`,
|
|
1126
1260
|
canFn: () => getters.canBackoff(this.$socket),
|
|
1127
|
-
delayedFn: () => dispatch('resyncWatch',
|
|
1261
|
+
delayedFn: () => dispatch('resyncWatch', {
|
|
1262
|
+
...msg,
|
|
1263
|
+
backOffId: undefined,
|
|
1264
|
+
}),
|
|
1128
1265
|
});
|
|
1129
1266
|
} else if ( err.includes('the server does not allow this method on the requested resource')) {
|
|
1130
1267
|
commit('setInError', { msg, reason: NO_PERMS });
|
|
@@ -1200,6 +1337,18 @@ const defaultActions = {
|
|
|
1200
1337
|
},
|
|
1201
1338
|
|
|
1202
1339
|
'ws.resource.create'(ctx, msg) {
|
|
1340
|
+
const data = msg.data;
|
|
1341
|
+
const type = data?.type;
|
|
1342
|
+
|
|
1343
|
+
const havePage = ctx.getters['havePage'](type);
|
|
1344
|
+
|
|
1345
|
+
if (havePage) {
|
|
1346
|
+
console.warn(`Prevented watch \`resource.create\` data from polluting the cache for type "${ type }" (currently represents a page). To prevent any further issues the watch has been stopped.`, msg); // eslint-disable-line no-console
|
|
1347
|
+
ctx.dispatch('unwatch', { ...msg, type });
|
|
1348
|
+
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1203
1352
|
ctx.state.debugSocket && console.info(`Resource Create [${ ctx.getters.storeName }]`, msg.resourceType, msg); // eslint-disable-line no-console
|
|
1204
1353
|
queueChange(ctx, msg, true, 'Create');
|
|
1205
1354
|
},
|
|
@@ -1230,8 +1379,8 @@ const defaultActions = {
|
|
|
1230
1379
|
const havePage = ctx.getters['havePage'](type);
|
|
1231
1380
|
|
|
1232
1381
|
if (havePage) {
|
|
1233
|
-
console.warn(`Prevented watch \`resource.change\` data from polluting the cache for type "${ type }" (currently represents a page). To prevent any further issues the watch has been stopped.`,
|
|
1234
|
-
ctx.dispatch('unwatch',
|
|
1382
|
+
console.warn(`Prevented watch \`resource.change\` data from polluting the cache for type "${ type }" (currently represents a page). To prevent any further issues the watch has been stopped.`, msg); // eslint-disable-line no-console
|
|
1383
|
+
ctx.dispatch('unwatch', { ...msg, type });
|
|
1235
1384
|
|
|
1236
1385
|
return;
|
|
1237
1386
|
}
|
|
@@ -1256,10 +1405,10 @@ const defaultActions = {
|
|
|
1256
1405
|
}
|
|
1257
1406
|
},
|
|
1258
1407
|
|
|
1259
|
-
'ws.resource.changes'({ dispatch }, msg) {
|
|
1260
|
-
dispatch('fetchResources', {
|
|
1261
|
-
|
|
1262
|
-
opt:
|
|
1408
|
+
async 'ws.resource.changes'({ dispatch }, msg) {
|
|
1409
|
+
await dispatch('fetchResources', {
|
|
1410
|
+
params: msg,
|
|
1411
|
+
opt: { force: true, load: _MERGE }
|
|
1263
1412
|
} );
|
|
1264
1413
|
},
|
|
1265
1414
|
|
|
@@ -1277,6 +1426,15 @@ const defaultActions = {
|
|
|
1277
1426
|
}
|
|
1278
1427
|
}
|
|
1279
1428
|
|
|
1429
|
+
const havePage = ctx.getters['havePage'](type);
|
|
1430
|
+
|
|
1431
|
+
if (havePage) {
|
|
1432
|
+
console.warn(`Prevented watch \`resource.remove\` data from polluting the cache for type "${ type }" (currently represents a page). To prevent any further issues the watch has been stopped.`, msg); // eslint-disable-line no-console
|
|
1433
|
+
ctx.dispatch('unwatch', { ...msg, type });
|
|
1434
|
+
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1280
1438
|
queueChange(ctx, msg, false, 'Remove');
|
|
1281
1439
|
|
|
1282
1440
|
const typeOption = ctx.rootGetters['type-map/optionsFor'](type);
|
|
@@ -1392,7 +1550,7 @@ const defaultGetters = {
|
|
|
1392
1550
|
* @param postFix - something else to uniquely id this back-off
|
|
1393
1551
|
*/
|
|
1394
1552
|
backOffId: () => (obj, postFix) => {
|
|
1395
|
-
return `${ keyForSubscribe(obj) }${ postFix ?
|
|
1553
|
+
return `${ keyForSubscribe(obj) }${ postFix ? `:detail=${ postFix }` : '' }`;
|
|
1396
1554
|
},
|
|
1397
1555
|
|
|
1398
1556
|
/**
|
|
@@ -1433,15 +1591,15 @@ const defaultGetters = {
|
|
|
1433
1591
|
*/
|
|
1434
1592
|
nextResourceVersion: (state, getters) => (type, id) => {
|
|
1435
1593
|
type = normalizeType(type);
|
|
1436
|
-
let
|
|
1594
|
+
let nextRevision = 0;
|
|
1437
1595
|
|
|
1438
1596
|
if ( id ) {
|
|
1439
1597
|
const existing = getters['byId'](type, id);
|
|
1440
1598
|
|
|
1441
|
-
|
|
1599
|
+
nextRevision = existing?.metadata?.resourceVersion;
|
|
1442
1600
|
}
|
|
1443
1601
|
|
|
1444
|
-
if ( !
|
|
1602
|
+
if ( !nextRevision ) {
|
|
1445
1603
|
const cache = state.types[type];
|
|
1446
1604
|
|
|
1447
1605
|
// No Cache, nothing to compare to, return early
|
|
@@ -1449,27 +1607,29 @@ const defaultGetters = {
|
|
|
1449
1607
|
return null;
|
|
1450
1608
|
}
|
|
1451
1609
|
|
|
1452
|
-
|
|
1610
|
+
const cacheRevision = new SteveRevision(cache.revision);
|
|
1453
1611
|
|
|
1454
1612
|
// Cached LIST revision isn't a number, cannot compare to, return early
|
|
1455
|
-
if (
|
|
1613
|
+
if (!cacheRevision.isNumber) {
|
|
1456
1614
|
return cache.revision || null;
|
|
1457
1615
|
}
|
|
1458
1616
|
|
|
1617
|
+
nextRevision = cacheRevision;
|
|
1618
|
+
|
|
1459
1619
|
for ( const obj of cache.list || [] ) {
|
|
1460
1620
|
if ( obj && obj.metadata ) {
|
|
1461
|
-
const
|
|
1621
|
+
const candidateRevision = new SteveRevision(obj.metadata.resourceVersion);
|
|
1462
1622
|
|
|
1463
|
-
if (
|
|
1464
|
-
|
|
1623
|
+
if (candidateRevision.isNewerThan(nextRevision)) {
|
|
1624
|
+
nextRevision = candidateRevision;
|
|
1465
1625
|
}
|
|
1466
|
-
|
|
1467
|
-
revision = Math.max(revision, neu);
|
|
1468
1626
|
}
|
|
1469
1627
|
}
|
|
1628
|
+
|
|
1629
|
+
nextRevision = nextRevision.asNumber;
|
|
1470
1630
|
}
|
|
1471
1631
|
|
|
1472
|
-
return
|
|
1632
|
+
return nextRevision || null;
|
|
1473
1633
|
},
|
|
1474
1634
|
|
|
1475
1635
|
/**
|
|
@@ -167,7 +167,7 @@ export class SteveWatchEventListenerManager {
|
|
|
167
167
|
|
|
168
168
|
if (eventWatcher) {
|
|
169
169
|
Object.values(eventWatcher.callbacks).forEach((cb) => {
|
|
170
|
-
cb({ forceWatch: params.forceWatch }); // eslint-disable-line node/no-callback-literal
|
|
170
|
+
cb({ forceWatch: params.forceWatch, revision: params.revision }); // eslint-disable-line node/no-callback-literal
|
|
171
171
|
});
|
|
172
172
|
}
|
|
173
173
|
}
|
|
@@ -177,7 +177,7 @@ export class SteveWatchEventListenerManager {
|
|
|
177
177
|
|
|
178
178
|
watch.listeners.forEach((l) => {
|
|
179
179
|
Object.values(l.callbacks || {}).forEach((cb) => {
|
|
180
|
-
cb({ forceWatch: params.forceWatch });// eslint-disable-line node/no-callback-literal
|
|
180
|
+
cb({ forceWatch: params.forceWatch, revision: params.revision });// eslint-disable-line node/no-callback-literal
|
|
181
181
|
});
|
|
182
182
|
});
|
|
183
183
|
}
|
|
@@ -259,6 +259,12 @@ export default defineComponent({
|
|
|
259
259
|
*/
|
|
260
260
|
findTrueValues(value: boolean[]): boolean {
|
|
261
261
|
return value.find((v) => v === this.valueWhenTrue) || false;
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
focus() {
|
|
265
|
+
if (!this.isDisabled) {
|
|
266
|
+
(this.$refs.checkbox as HTMLElement)?.focus();
|
|
267
|
+
}
|
|
262
268
|
}
|
|
263
269
|
}
|
|
264
270
|
});
|
|
@@ -285,10 +291,12 @@ export default defineComponent({
|
|
|
285
291
|
:value="valueWhenTrue"
|
|
286
292
|
type="checkbox"
|
|
287
293
|
tabindex="-1"
|
|
294
|
+
aria-hidden="true"
|
|
288
295
|
@click.stop.prevent
|
|
289
296
|
@keyup.enter.stop.prevent
|
|
290
297
|
>
|
|
291
298
|
<span
|
|
299
|
+
ref="checkbox"
|
|
292
300
|
class="checkbox-custom"
|
|
293
301
|
:class="{indeterminate: indeterminate}"
|
|
294
302
|
:tabindex="isDisabled ? -1 : 0"
|
|
@@ -424,6 +432,11 @@ $fontColor: var(--input-label);
|
|
|
424
432
|
outline-offset: 2px;
|
|
425
433
|
border-radius: 0;
|
|
426
434
|
}
|
|
435
|
+
&:focus {
|
|
436
|
+
@include focus-outline;
|
|
437
|
+
outline-offset: 2px;
|
|
438
|
+
border-radius: 0;
|
|
439
|
+
}
|
|
427
440
|
}
|
|
428
441
|
|
|
429
442
|
input {
|
|
@@ -68,7 +68,7 @@ export default defineComponent({
|
|
|
68
68
|
<template v-if="hover">
|
|
69
69
|
<i
|
|
70
70
|
v-clean-tooltip="tooltipContent"
|
|
71
|
-
v-stripped-aria-label="isObject(value) ? value.content : value"
|
|
71
|
+
v-stripped-aria-label="`${t('generic.tooltip')} - ${(isObject(value) ? value.content : value)}`"
|
|
72
72
|
:class="{'hover':!value, [iconClass]: true}"
|
|
73
73
|
class="icon status-icon"
|
|
74
74
|
tabindex="0"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { RcCounterBadgeProps } from '
|
|
2
|
+
import { RcCounterBadgeProps } from './types';
|
|
3
3
|
import { computed } from 'vue';
|
|
4
4
|
const props = withDefaults(defineProps<RcCounterBadgeProps>(), { disabled: false });
|
|
5
5
|
const displayCount = computed(() => props.count < 1000 ? props.count : '999+');
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { toRef } from 'vue';
|
|
2
3
|
import { RcStatusBadgeProps } from './types';
|
|
3
4
|
import { useStatusColors } from '@components/utils/status';
|
|
4
5
|
|
|
5
6
|
const props = defineProps<RcStatusBadgeProps>();
|
|
6
7
|
|
|
7
|
-
const
|
|
8
|
+
const status = toRef(props, 'status');
|
|
9
|
+
const { backgroundColor, borderColor, textColor } = useStatusColors(status, 'outlined');
|
|
8
10
|
</script>
|
|
9
11
|
|
|
10
12
|
<template>
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { toRef } from 'vue';
|
|
2
3
|
import { RcStatusIndicatorProps } from './types';
|
|
3
4
|
import { useStatusColors } from '@components/utils/status';
|
|
4
5
|
|
|
5
6
|
const props = defineProps<RcStatusIndicatorProps>();
|
|
6
7
|
|
|
7
|
-
const
|
|
8
|
+
const status = toRef(props, 'status');
|
|
9
|
+
const { backgroundColor, borderColor } = useStatusColors(status, 'solid');
|
|
8
10
|
</script>
|
|
9
11
|
|
|
10
12
|
<template>
|
|
@@ -2,22 +2,22 @@ import { mount } from '@vue/test-utils';
|
|
|
2
2
|
import RcButton from './RcButton.vue';
|
|
3
3
|
|
|
4
4
|
describe('rcButton.vue', () => {
|
|
5
|
-
it('renders with default
|
|
5
|
+
it('renders with default variant', () => {
|
|
6
6
|
const wrapper = mount(RcButton);
|
|
7
7
|
const button = wrapper.find('button');
|
|
8
8
|
|
|
9
9
|
expect(button.classes()).toContain('btn');
|
|
10
|
-
expect(button.classes()).toContain('
|
|
10
|
+
expect(button.classes()).toContain('variant-primary');
|
|
11
11
|
});
|
|
12
12
|
|
|
13
|
-
it('applies correct
|
|
13
|
+
it('applies correct variant', () => {
|
|
14
14
|
const wrapper = mount(RcButton, { props: { primary: true } });
|
|
15
15
|
const button = wrapper.find('button');
|
|
16
16
|
|
|
17
|
-
expect(button.classes()).toContain('
|
|
17
|
+
expect(button.classes()).toContain('variant-primary');
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
-
it('defaults to primary
|
|
20
|
+
it('defaults to primary variant if multiple variants are provided', () => {
|
|
21
21
|
const wrapper = mount(
|
|
22
22
|
RcButton,
|
|
23
23
|
{
|
|
@@ -30,10 +30,10 @@ describe('rcButton.vue', () => {
|
|
|
30
30
|
);
|
|
31
31
|
const button = wrapper.find('button');
|
|
32
32
|
|
|
33
|
-
expect(button.classes()).toContain('
|
|
33
|
+
expect(button.classes()).toContain('variant-primary');
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
it('defaults to secondary
|
|
36
|
+
it('defaults to secondary variant if both secondary and tertiary variants are provided', () => {
|
|
37
37
|
const wrapper = mount(
|
|
38
38
|
RcButton,
|
|
39
39
|
{
|
|
@@ -45,7 +45,7 @@ describe('rcButton.vue', () => {
|
|
|
45
45
|
);
|
|
46
46
|
const button = wrapper.find('button');
|
|
47
47
|
|
|
48
|
-
expect(button.classes()).toContain('
|
|
48
|
+
expect(button.classes()).toContain('variant-secondary');
|
|
49
49
|
});
|
|
50
50
|
|
|
51
51
|
it('applies correct size class', () => {
|
|
@@ -92,6 +92,50 @@ describe('rcButton.vue', () => {
|
|
|
92
92
|
const wrapper = mount(RcButton, { props: { ghost: true } });
|
|
93
93
|
const button = wrapper.find('button');
|
|
94
94
|
|
|
95
|
-
expect(button.classes()).toContain('
|
|
95
|
+
expect(button.classes()).toContain('variant-ghost');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('variant prop', () => {
|
|
99
|
+
it('applies variant-primary class when variant="primary"', () => {
|
|
100
|
+
const wrapper = mount(RcButton, { props: { variant: 'primary' } });
|
|
101
|
+
const button = wrapper.find('button');
|
|
102
|
+
|
|
103
|
+
expect(button.classes()).toContain('variant-primary');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('applies variant-secondary class when variant="secondary"', () => {
|
|
107
|
+
const wrapper = mount(RcButton, { props: { variant: 'secondary' } });
|
|
108
|
+
const button = wrapper.find('button');
|
|
109
|
+
|
|
110
|
+
expect(button.classes()).toContain('variant-secondary');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('applies variant-tertiary class when variant="tertiary"', () => {
|
|
114
|
+
const wrapper = mount(RcButton, { props: { variant: 'tertiary' } });
|
|
115
|
+
const button = wrapper.find('button');
|
|
116
|
+
|
|
117
|
+
expect(button.classes()).toContain('variant-tertiary');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('applies variant-link class when variant="link"', () => {
|
|
121
|
+
const wrapper = mount(RcButton, { props: { variant: 'link' } });
|
|
122
|
+
const button = wrapper.find('button');
|
|
123
|
+
|
|
124
|
+
expect(button.classes()).toContain('variant-link');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('applies variant-multi-action class when variant="multiAction"', () => {
|
|
128
|
+
const wrapper = mount(RcButton, { props: { variant: 'multiAction' } });
|
|
129
|
+
const button = wrapper.find('button');
|
|
130
|
+
|
|
131
|
+
expect(button.classes()).toContain('variant-multi-action');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('applies variant-ghost class when variant="ghost"', () => {
|
|
135
|
+
const wrapper = mount(RcButton, { props: { variant: 'ghost' } });
|
|
136
|
+
const button = wrapper.find('button');
|
|
137
|
+
|
|
138
|
+
expect(button.classes()).toContain('variant-ghost');
|
|
139
|
+
});
|
|
96
140
|
});
|
|
97
141
|
});
|