@rancher/shell 0.3.17 → 0.3.18
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/translations/en-us.yaml +8 -4
- package/assets/translations/zh-hans.yaml +64 -8
- package/components/AsyncButton.vue +1 -1
- package/components/Inactivity.vue +10 -0
- package/components/LazyImage.vue +2 -2
- package/components/PromptRestore.vue +7 -5
- package/components/ResourceDetail/Masthead.vue +1 -1
- package/components/ResourceDetail/index.vue +4 -2
- package/components/__tests__/PromptRestore.test.ts +72 -0
- package/components/auth/AzureWarning.vue +1 -1
- package/components/fleet/FleetResources.vue +3 -64
- package/components/form/FileImageSelector.vue +9 -0
- package/components/form/FileSelector.vue +2 -1
- package/components/form/MatchExpressions.vue +1 -3
- package/components/form/__tests__/FileImageSelector.test.ts +42 -0
- package/components/form/__tests__/FileSelector.test.ts +76 -0
- package/components/formatter/ClusterProvider.vue +3 -1
- package/components/formatter/__tests__/ClusterProvider.test.ts +24 -0
- package/components/nav/WindowManager/ContainerShell.vue +60 -36
- package/components/nav/WindowManager/__tests__/ContainerShell.test.ts +561 -0
- package/config/labels-annotations.js +2 -1
- package/config/persistentVolume.ts +108 -0
- package/config/product/manager.js +5 -1
- package/config/types.js +2 -0
- package/core/plugin-helpers.js +19 -3
- package/core/types.ts +4 -0
- package/detail/fleet.cattle.io.gitrepo.vue +10 -2
- package/detail/pod.vue +36 -3
- package/detail/workload/index.vue +40 -9
- package/edit/__tests__/ui.cattle.io.navlink.test.ts +110 -0
- package/edit/fleet.cattle.io.clustergroup.vue +14 -3
- package/edit/persistentvolume/__tests__/persistentvolume.test.ts +82 -0
- package/edit/persistentvolume/index.vue +2 -1
- package/edit/persistentvolume/plugins/csi.vue +3 -1
- package/edit/persistentvolume/plugins/longhorn.vue +12 -12
- package/edit/provisioning.cattle.io.cluster/RegistryConfigs.vue +15 -11
- package/edit/provisioning.cattle.io.cluster/index.vue +1 -1
- package/edit/provisioning.cattle.io.cluster/rke2.vue +5 -1
- package/edit/storage.k8s.io.storageclass/index.vue +1 -2
- package/edit/ui.cattle.io.navlink.vue +213 -187
- package/layouts/default.vue +1 -1
- package/list/group.principal.vue +1 -1
- package/middleware/authenticated.js +12 -4
- package/mixins/create-edit-view/impl.js +2 -2
- package/models/chart.js +1 -1
- package/models/fleet.cattle.io.cluster.js +33 -4
- package/models/fleet.cattle.io.gitrepo.js +112 -38
- package/models/management.cattle.io.kontainerdriver.js +14 -0
- package/models/persistentvolume.js +2 -111
- package/models/pod.js +30 -0
- package/models/rke.cattle.io.etcdsnapshot.js +10 -7
- package/package.json +1 -1
- package/pages/c/_cluster/auth/group.principal/assign-edit.vue +1 -1
- package/pages/c/_cluster/auth/roles/index.vue +1 -1
- package/pages/c/_cluster/explorer/index.vue +1 -1
- package/pages/c/_cluster/manager/cloudCredential/_id.vue +0 -1
- package/pages/c/_cluster/manager/cloudCredential/create.vue +0 -1
- package/pages/c/_cluster/settings/brand.vue +11 -8
- package/pages/c/_cluster/uiplugins/index.vue +9 -4
- package/pages/home.vue +1 -1
- package/plugins/dashboard-store/__tests__/actions.spec.ts +165 -0
- package/plugins/dashboard-store/__tests__/getters.spec.ts +100 -0
- package/plugins/dashboard-store/__tests__/{mutations.spec.js → mutations.spec.ts} +2 -2
- package/plugins/dashboard-store/actions.js +1 -1
- package/plugins/dashboard-store/resource-class.js +4 -0
- package/plugins/steve/__tests__/getters.spec.ts +93 -0
- package/plugins/steve/getters.js +21 -1
- package/plugins/steve/subscribe.js +1 -3
- package/rancher-components/components/BadgeState/BadgeState.spec.ts +12 -0
- package/rancher-components/components/BadgeState/BadgeState.vue +111 -0
- package/rancher-components/components/BadgeState/index.ts +1 -0
- package/rancher-components/components/Banner/Banner.test.ts +63 -0
- package/rancher-components/components/Banner/Banner.vue +244 -0
- package/rancher-components/components/Banner/index.ts +1 -0
- package/rancher-components/components/Card/Card.test.ts +37 -0
- package/rancher-components/components/Card/Card.vue +167 -0
- package/rancher-components/components/Card/index.ts +1 -0
- package/rancher-components/components/Form/Checkbox/Checkbox.test.ts +68 -0
- package/rancher-components/components/Form/Checkbox/Checkbox.vue +420 -0
- package/rancher-components/components/Form/Checkbox/index.ts +1 -0
- package/rancher-components/components/Form/LabeledInput/LabeledInput.test.ts +23 -0
- package/rancher-components/components/Form/LabeledInput/LabeledInput.vue +355 -0
- package/rancher-components/components/Form/LabeledInput/index.ts +1 -0
- package/rancher-components/components/Form/Radio/RadioButton.test.ts +31 -0
- package/rancher-components/components/Form/Radio/RadioButton.vue +287 -0
- package/rancher-components/components/Form/Radio/RadioGroup.vue +254 -0
- package/rancher-components/components/Form/Radio/index.ts +2 -0
- package/rancher-components/components/Form/TextArea/TextAreaAutoGrow.vue +170 -0
- package/rancher-components/components/Form/TextArea/index.ts +1 -0
- package/rancher-components/components/Form/ToggleSwitch/ToggleSwitch.test.ts +94 -0
- package/rancher-components/components/Form/ToggleSwitch/ToggleSwitch.vue +149 -0
- package/rancher-components/components/Form/ToggleSwitch/index.ts +1 -0
- package/rancher-components/components/Form/index.ts +5 -0
- package/rancher-components/components/LabeledTooltip/LabeledTooltip.vue +151 -0
- package/rancher-components/components/LabeledTooltip/index.ts +1 -0
- package/rancher-components/components/StringList/StringList.test.ts +484 -0
- package/rancher-components/components/StringList/StringList.vue +611 -0
- package/rancher-components/components/StringList/index.ts +1 -0
- package/scripts/typegen.sh +10 -2
- package/store/index.js +1 -3
- package/store/store-types.js +2 -0
- package/types/api.d.ts +1 -0
- package/types/fleet.d.ts +1 -0
- package/types/shell/index.d.ts +695 -2
- package/types/userPreferences.d.ts +1 -1
- package/utils/__mocks__/socket.js +21 -0
- package/utils/grafana.js +23 -11
- package/utils/selector.js +2 -1
- package/utils/validators/formRules/index.ts +3 -3
- package/plugins/steve/urloptions.js +0 -47
|
@@ -541,6 +541,7 @@ export default {
|
|
|
541
541
|
},
|
|
542
542
|
|
|
543
543
|
showAddExtensionReposDialog() {
|
|
544
|
+
this.updateAddReposSetting();
|
|
544
545
|
this.refreshCharts(true);
|
|
545
546
|
this.$refs.addExtensionReposDialog.showDialog();
|
|
546
547
|
},
|
|
@@ -745,14 +746,13 @@ export default {
|
|
|
745
746
|
<Banner
|
|
746
747
|
v-if="showAddReposBanner"
|
|
747
748
|
color="warning"
|
|
748
|
-
class="mb-20"
|
|
749
|
-
:closable="true"
|
|
749
|
+
class="add-repos-banner mb-20"
|
|
750
750
|
data-testid="extensions-new-repos-banner"
|
|
751
|
-
@close="updateAddReposSetting"
|
|
752
751
|
>
|
|
753
752
|
<span>{{ t('plugins.addRepos.banner', {}, true) }}</span>
|
|
754
753
|
<button
|
|
755
754
|
class="ml-10 btn btn-sm role-primary"
|
|
755
|
+
data-testid="extensions-new-repos-banner-action-btn"
|
|
756
756
|
@click="showAddExtensionReposDialog()"
|
|
757
757
|
>
|
|
758
758
|
{{ t('plugins.addRepos.bannerBtn') }}
|
|
@@ -1237,11 +1237,16 @@ export default {
|
|
|
1237
1237
|
}
|
|
1238
1238
|
}
|
|
1239
1239
|
}
|
|
1240
|
-
|
|
1241
1240
|
::v-deep .checkbox-label {
|
|
1242
1241
|
font-weight: normal !important;
|
|
1243
1242
|
}
|
|
1244
1243
|
|
|
1244
|
+
::v-deep .add-repos-banner .banner__content {
|
|
1245
|
+
display: flex;
|
|
1246
|
+
justify-content: space-between;
|
|
1247
|
+
align-items: center;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1245
1250
|
@media screen and (max-width: 1200px) {
|
|
1246
1251
|
.plugin-list {
|
|
1247
1252
|
.plugin {
|
package/pages/home.vue
CHANGED
|
@@ -17,7 +17,7 @@ import { getVersionInfo, readReleaseNotes, markReadReleaseNotes, markSeenRelease
|
|
|
17
17
|
import PageHeaderActions from '@shell/mixins/page-actions';
|
|
18
18
|
import { getVendor } from '@shell/config/private-label';
|
|
19
19
|
import { mapFeature, MULTI_CLUSTER } from '@shell/store/features';
|
|
20
|
-
import { BLANK_CLUSTER } from '@shell/store';
|
|
20
|
+
import { BLANK_CLUSTER } from '@shell/store/store-types.js';
|
|
21
21
|
import { filterOnlyKubernetesClusters, filterHiddenLocalCluster } from '@shell/utils/cluster';
|
|
22
22
|
|
|
23
23
|
import { RESET_CARDS_ACTION, SET_LOGIN_ACTION } from '@shell/config/page-actions';
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import _actions from '@shell/plugins/dashboard-store/actions';
|
|
2
|
+
|
|
3
|
+
const { findAll } = _actions;
|
|
4
|
+
|
|
5
|
+
describe('dashboard-store: actions', () => {
|
|
6
|
+
const setupContext = () => {
|
|
7
|
+
const commit = jest.fn();
|
|
8
|
+
const dispatch = jest.fn((...args) => {
|
|
9
|
+
switch (args[0]) {
|
|
10
|
+
case 'request':
|
|
11
|
+
return { data: ['requestData'] };
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
const state = { config: { namespace: 'unitTest' } };
|
|
15
|
+
const getters = {
|
|
16
|
+
normalizeType: jest.fn(() => 'getters.normalizeType'),
|
|
17
|
+
typeRegistered: jest.fn(() => false),
|
|
18
|
+
haveAll: jest.fn(() => false),
|
|
19
|
+
haveAllNamespace: jest.fn(() => false),
|
|
20
|
+
all: jest.fn(() => 'getters.all'),
|
|
21
|
+
urlFor: jest.fn(() => 'getters.urlFor'), // we're not testing the urlFor getter so we don't need to do anything with opt here
|
|
22
|
+
};
|
|
23
|
+
const rootGetters = {
|
|
24
|
+
'type-map/optionsFor': jest.fn(),
|
|
25
|
+
'auth/fromHeader': 'foo'
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// we're not testing function output based off of state or getter inputs here since they are dependencies and should be tested independently
|
|
29
|
+
return {
|
|
30
|
+
state,
|
|
31
|
+
getters,
|
|
32
|
+
rootGetters,
|
|
33
|
+
commit,
|
|
34
|
+
dispatch
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const standardAssertions = {
|
|
39
|
+
returnsPromise:
|
|
40
|
+
{
|
|
41
|
+
assertionLabel: 'returns a promise',
|
|
42
|
+
valueGetter: ({ findAllPromise }) => typeof findAllPromise.then,
|
|
43
|
+
valueExpected: 'function'
|
|
44
|
+
},
|
|
45
|
+
callsAll: {
|
|
46
|
+
assertionLabel: 'calls the "all" getter with the normalizedType',
|
|
47
|
+
valueGetter: ({ getters }) => getters.all.mock.calls[0][0],
|
|
48
|
+
valueExpected: 'getters.normalizeType'
|
|
49
|
+
},
|
|
50
|
+
returnsFromAll: {
|
|
51
|
+
assertionLabel: 'returns the value expected from the "all" getter',
|
|
52
|
+
valueGetter: ({ findAllReturnValue }) => findAllReturnValue,
|
|
53
|
+
valueExpected: 'getters.all'
|
|
54
|
+
},
|
|
55
|
+
firstDispatchAction: {
|
|
56
|
+
assertionLabel: 'first dispatch should be the "request" action',
|
|
57
|
+
valueGetter: ({ dispatch }) => dispatch.mock.calls[0][0],
|
|
58
|
+
valueExpected: 'request'
|
|
59
|
+
},
|
|
60
|
+
firstDispatchParams: {
|
|
61
|
+
assertionLabel: 'first dispatch parameters should be provided a normalized type and a url, streaming, and "metadata.managedFields" excluded under opt',
|
|
62
|
+
valueGetter: ({ dispatch }) => dispatch.mock.calls[0][1],
|
|
63
|
+
valueExpected: {
|
|
64
|
+
type: 'getters.normalizeType',
|
|
65
|
+
opt: {
|
|
66
|
+
url: 'getters.urlFor',
|
|
67
|
+
stream: true
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
assertionMethod: 'toMatchObject'
|
|
71
|
+
},
|
|
72
|
+
secondDispatchAction: {
|
|
73
|
+
assertionLabel: 'second dispatch should be the "watch" action',
|
|
74
|
+
valueGetter: ({ dispatch }) => dispatch.mock.calls[1][0],
|
|
75
|
+
valueExpected: 'watch'
|
|
76
|
+
},
|
|
77
|
+
secondDispatchParams: {
|
|
78
|
+
assertionLabel: 'second dispatch parameters should have a normalized type and force set to false',
|
|
79
|
+
valueGetter: ({ dispatch }) => dispatch.mock.calls[1][1],
|
|
80
|
+
valueExpected: { type: 'getters.normalizeType', force: false },
|
|
81
|
+
assertionMethod: 'toMatchObject'
|
|
82
|
+
},
|
|
83
|
+
countDispatches: {
|
|
84
|
+
assertionLabel: 'should only make two dispatches',
|
|
85
|
+
valueGetter: ({ dispatch }) => dispatch.mock.calls,
|
|
86
|
+
valueExpected: 2,
|
|
87
|
+
assertionMethod: 'toHaveLength'
|
|
88
|
+
},
|
|
89
|
+
firstCommitMutation: {
|
|
90
|
+
assertionLabel: 'first commit should be the "registerType" mutation',
|
|
91
|
+
valueGetter: ({ commit }) => commit.mock.calls[0][0],
|
|
92
|
+
valueExpected: 'registerType'
|
|
93
|
+
},
|
|
94
|
+
firstCommitParams: {
|
|
95
|
+
assertionLabel: 'first commit parameter should be a normalized type',
|
|
96
|
+
valueGetter: ({ commit }) => commit.mock.calls[0][1],
|
|
97
|
+
valueExpected: 'getters.normalizeType'
|
|
98
|
+
},
|
|
99
|
+
secondCommitMutation: {
|
|
100
|
+
assertionLabel: 'second commit should be the "loadAll" mutation',
|
|
101
|
+
valueGetter: ({ commit }) => commit.mock.calls[1][0],
|
|
102
|
+
valueExpected: 'loadAll'
|
|
103
|
+
},
|
|
104
|
+
secondCommitParams: {
|
|
105
|
+
assertionLabel: 'second commit parameters should have a normalized type, ctx.state.config.namespace, data returned by request, and skipHaveAll set to false',
|
|
106
|
+
valueGetter: ({ commit }) => commit.mock.calls[1][1],
|
|
107
|
+
valueExpected: {
|
|
108
|
+
type: 'getters.normalizeType',
|
|
109
|
+
ctx: { state: { config: { namespace: 'unitTest' } } },
|
|
110
|
+
data: ['requestData'],
|
|
111
|
+
skipHaveAll: false,
|
|
112
|
+
},
|
|
113
|
+
assertionMethod: 'toMatchObject'
|
|
114
|
+
},
|
|
115
|
+
countCommits: {
|
|
116
|
+
assertionLabel: 'should only make two commits',
|
|
117
|
+
valueGetter: ({ commit }) => commit.mock.calls,
|
|
118
|
+
valueExpected: 2,
|
|
119
|
+
assertionMethod: 'toHaveLength'
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
describe('dashboard-store > actions > findAll', () => {
|
|
125
|
+
describe('called without a cache for the type in the second param', () => {
|
|
126
|
+
const {
|
|
127
|
+
dispatch, commit, getters, rootGetters, state
|
|
128
|
+
} = setupContext();
|
|
129
|
+
|
|
130
|
+
const findAllPromise = findAll(
|
|
131
|
+
{
|
|
132
|
+
dispatch, commit, getters, rootGetters, state
|
|
133
|
+
},
|
|
134
|
+
{ type: 'type' }
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const assertionChain = [
|
|
138
|
+
standardAssertions.returnsPromise,
|
|
139
|
+
standardAssertions.callsAll,
|
|
140
|
+
standardAssertions.returnsFromAll,
|
|
141
|
+
standardAssertions.firstDispatchAction,
|
|
142
|
+
standardAssertions.firstDispatchParams,
|
|
143
|
+
standardAssertions.secondDispatchAction,
|
|
144
|
+
standardAssertions.secondDispatchParams,
|
|
145
|
+
standardAssertions.countDispatches,
|
|
146
|
+
standardAssertions.firstCommitMutation,
|
|
147
|
+
standardAssertions.firstCommitParams,
|
|
148
|
+
standardAssertions.secondCommitMutation,
|
|
149
|
+
standardAssertions.secondCommitParams,
|
|
150
|
+
standardAssertions.countCommits
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
it.each(assertionChain)(
|
|
154
|
+
'$assertionLabel',
|
|
155
|
+
async({ valueGetter, valueExpected, assertionMethod = 'toBe' }) => {
|
|
156
|
+
const findAllReturnValue = await findAllPromise;
|
|
157
|
+
|
|
158
|
+
expect(valueGetter({
|
|
159
|
+
findAllPromise, findAllReturnValue, getters, dispatch, commit
|
|
160
|
+
}))[assertionMethod](valueExpected);
|
|
161
|
+
}
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import getters, { urlFor } from '@shell/plugins/dashboard-store/getters';
|
|
2
|
+
|
|
3
|
+
const { urlOptions } = getters;
|
|
4
|
+
|
|
5
|
+
describe('dashboard-store: getters', () => {
|
|
6
|
+
describe('dashboard-store > getters > exported function: 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
|
+
it('expects urlFor to return a function', () => {
|
|
23
|
+
expect(typeof urlFor(state, getters)).toBe('function');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('expects function returned by urlFor to return a string', () => {
|
|
27
|
+
expect(urlForGetter('typeFoo')).toBe('protocol/urlFoo');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('expects function returned by urlFor to return a the url supplied as opt.url directly', () => {
|
|
31
|
+
expect(urlForGetter('typeFoo', null, { url: 'urlBar' })).toBe('protocol/urlBar');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('expects function returned by urlFor to return a the url to not receive an additional protocol if relative url', () => {
|
|
35
|
+
expect(urlForGetter('typeFoo', null, { url: '/urlBar' })).toBe('/urlBar');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('expects function returned by urlFor to return a the url to to not receive an additional protocol if the url starts with "http"', () => {
|
|
39
|
+
expect(urlForGetter('typeFoo', null, { url: 'http urlBar' })).toBe('http urlBar');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('expects function returned by urlFor to throw an error if passed an invalid type', () => {
|
|
43
|
+
expect(() => urlForGetter('typeBaz')).toThrow('Unknown schema for type: typeBaz');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('expects function returned by urlFor to append an id to the url if one is supplied', () => {
|
|
47
|
+
expect(urlForGetter('typeFoo', 'idBar')).toBe('protocol/urlFoo/idBar');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
describe('dashboard-store > getters > urlOptions', () => {
|
|
51
|
+
// we're not testing function output based off of state or getter inputs here since they are dependencies
|
|
52
|
+
const state = { config: { baseUrl: 'protocol' } };
|
|
53
|
+
const getters = {
|
|
54
|
+
normalizeType: (type) => type,
|
|
55
|
+
schemaFor: (type) => {
|
|
56
|
+
if (type === 'typeFoo') {
|
|
57
|
+
return { links: { collection: 'urlFoo' } };
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
// this has its own tests so it just returns the input string
|
|
61
|
+
urlOptions: (string) => string
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const urlOptionsGetter = urlOptions();
|
|
65
|
+
|
|
66
|
+
it('expects urlOptions to return a function', () => {
|
|
67
|
+
expect(typeof urlOptions(state, getters)).toBe('function');
|
|
68
|
+
});
|
|
69
|
+
it('returns undefined when called without params', () => {
|
|
70
|
+
expect(urlOptionsGetter()).toBeUndefined();
|
|
71
|
+
});
|
|
72
|
+
it('returns an unmodified string when called without options', () => {
|
|
73
|
+
expect(urlOptionsGetter('foo')).toBe('foo');
|
|
74
|
+
});
|
|
75
|
+
it('returns an unmodified string when called with options that are not accounted for', () => {
|
|
76
|
+
expect(urlOptionsGetter('foo', { bar: 'baz' })).toBe('foo');
|
|
77
|
+
});
|
|
78
|
+
it('returns an unmodified stringif a single filter statement is applied', () => {
|
|
79
|
+
expect(urlOptionsGetter('foo', { filter: { bar: 'baz' } })).toBe('foo');
|
|
80
|
+
});
|
|
81
|
+
it('returns an unmodified string if a single filter statement is applied', () => {
|
|
82
|
+
expect(urlOptionsGetter('foo', { filter: { bar: 'baz', far: 'faz' } })).toBe('foo');
|
|
83
|
+
});
|
|
84
|
+
it('returns an unmodified string if excludeFields is a single element array with the string "bar"', () => {
|
|
85
|
+
expect(urlOptionsGetter('/v1/foo', { excludeFields: ['bar'] })).toBe('/v1/foo');
|
|
86
|
+
});
|
|
87
|
+
it('returns an unmodified string if excludeFields is an array but the URL doesnt include the "/v1/ string"', () => {
|
|
88
|
+
expect(urlOptionsGetter('foo', { excludeFields: ['bar'] })).toBe('foo');
|
|
89
|
+
});
|
|
90
|
+
it('returns an unmodified string if a limit is provided', () => {
|
|
91
|
+
expect(urlOptionsGetter('foo', { limit: 10 })).toBe('foo');
|
|
92
|
+
});
|
|
93
|
+
it('returns an unmodified string if the sort option is provided', () => {
|
|
94
|
+
expect(urlOptionsGetter('foo', { sortBy: 'bar' })).toBe('foo');
|
|
95
|
+
});
|
|
96
|
+
it('returns an unmodified string if the sort option is provided and an order if sortOrder is provided', () => {
|
|
97
|
+
expect(urlOptionsGetter('foo', { sortBy: 'bar', sortOrder: 'baz' })).toBe('foo');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -256,8 +256,8 @@ describe('dashboard-store: mutations', () => {
|
|
|
256
256
|
{
|
|
257
257
|
ctx,
|
|
258
258
|
batch: {
|
|
259
|
-
[POD]:
|
|
260
|
-
[
|
|
259
|
+
[POD]: { [pod.id]: pod },
|
|
260
|
+
[WORKLOAD_TYPES.DEPLOYMENT]: { [deployment.id]: deployment }
|
|
261
261
|
}
|
|
262
262
|
}
|
|
263
263
|
],
|
|
@@ -1464,6 +1464,10 @@ export default class Resource {
|
|
|
1464
1464
|
}
|
|
1465
1465
|
|
|
1466
1466
|
async saveYaml(yaml) {
|
|
1467
|
+
this._saveYaml(yaml);
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
async _saveYaml(yaml) {
|
|
1467
1471
|
/* Multipart support, but need to know the right cluster and work for management store
|
|
1468
1472
|
and "apply" seems to only work for create, not update.
|
|
1469
1473
|
|
|
@@ -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
|
+
});
|
package/plugins/steve/getters.js
CHANGED
|
@@ -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
|
|
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';
|