@rancher/shell 3.0.9-rc.5 → 3.0.9-rc.6
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/images/providers/oci-open-containers.svg +22 -0
- package/assets/images/providers/traefik.png +0 -0
- package/assets/styles/themes/_dark.scss +2 -0
- package/assets/styles/themes/_light.scss +2 -0
- package/assets/styles/themes/_modern.scss +6 -0
- package/assets/translations/en-us.yaml +129 -25
- package/components/CruResource.vue +3 -1
- package/components/ExplorerProjectsNamespaces.vue +12 -12
- package/components/Resource/Detail/Card/StatusCard/__tests__/StatusCard.test.ts +109 -0
- package/components/Resource/Detail/Card/StatusCard/index.vue +21 -4
- package/components/Resource/Detail/Metadata/IdentifyingInformation/__tests__/identifying-fields.test.ts +19 -2
- package/components/Resource/Detail/Metadata/IdentifyingInformation/identifying-fields.ts +19 -11
- package/components/Resource/Detail/ResourcePopover/__tests__/index.test.ts +12 -0
- package/components/Resource/Detail/ResourcePopover/index.vue +2 -0
- package/components/Resource/Detail/ResourceRow.vue +2 -2
- package/components/ResourceList/index.vue +7 -4
- package/components/Window/ContainerLogs.vue +48 -37
- package/components/fleet/FleetClusterTargets/TargetsList.vue +2 -2
- package/components/fleet/FleetClusterTargets/index.vue +6 -1
- package/components/fleet/GitRepoAdvancedTab.vue +333 -0
- package/components/fleet/GitRepoMetadataTab.vue +43 -0
- package/components/fleet/GitRepoRepositoryTab.vue +101 -0
- package/components/fleet/GitRepoTargetTab.vue +77 -0
- package/components/fleet/HelmOpAdvancedTab.vue +247 -0
- package/components/fleet/HelmOpChartTab.vue +158 -0
- package/components/fleet/HelmOpMetadataTab.vue +46 -0
- package/components/fleet/HelmOpTargetTab.vue +84 -0
- package/components/fleet/HelmOpValuesTab.vue +147 -0
- package/components/fleet/__tests__/FleetClusterTargets.test.ts +119 -70
- package/components/form/NodeScheduling.vue +81 -7
- package/components/form/PodAffinity.vue +1 -36
- package/components/form/ResourceLabeledSelect.vue +8 -4
- package/components/form/ResourceQuota/Namespace.vue +30 -9
- package/components/form/ResourceQuota/NamespaceRow.vue +25 -7
- package/components/form/ResourceQuota/Project.vue +140 -82
- package/components/form/ResourceQuota/ResourceQuotaEntry.vue +145 -0
- package/components/form/ResourceQuota/__tests__/Namespace.test.ts +307 -0
- package/components/form/ResourceQuota/__tests__/NamespaceRow.test.ts +281 -0
- package/components/form/ResourceQuota/__tests__/Project.test.ts +274 -27
- package/components/form/ResourceQuota/__tests__/ResourceQuotaEntry.test.ts +215 -0
- package/components/form/SchedulingCustomization.vue +14 -6
- package/components/form/SelectOrCreateAuthSecret.vue +107 -18
- package/components/form/__tests__/NodeScheduling.test.ts +12 -9
- package/components/form/__tests__/PodAffinity.test.ts +21 -2
- package/components/form/__tests__/SchedulingCustomization.test.ts +240 -0
- package/components/formatter/ClusterLink.vue +8 -0
- package/components/formatter/SecretOrigin.vue +79 -0
- package/config/labels-annotations.js +7 -6
- package/config/pagination-table-headers.js +6 -4
- package/config/product/explorer.js +1 -11
- package/config/query-params.js +3 -0
- package/config/settings.ts +15 -2
- package/config/table-headers.js +21 -17
- package/config/types.js +23 -8
- package/detail/workload/index.vue +11 -16
- package/dialog/DeactivateDriverDialog.vue +1 -1
- package/dialog/Ipv6NetworkingDialog.vue +156 -0
- package/dialog/ScalePoolDownDialog.vue +2 -2
- package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +1 -1
- package/edit/__tests__/fleet.cattle.io.helmop.test.ts +1 -0
- package/edit/__tests__/management.cattle.io.project.test.js +56 -128
- package/edit/auth/oidc.vue +1 -1
- package/edit/catalog.cattle.io.clusterrepo.vue +155 -25
- package/edit/fleet.cattle.io.gitrepo.vue +153 -283
- package/edit/fleet.cattle.io.helmop.vue +190 -332
- package/edit/management.cattle.io.project.vue +5 -42
- package/edit/management.cattle.io.setting.vue +6 -0
- package/edit/provisioning.cattle.io.cluster/__tests__/Basics.test.ts +55 -24
- package/edit/provisioning.cattle.io.cluster/__tests__/Networking.test.ts +1 -103
- package/edit/provisioning.cattle.io.cluster/__tests__/index.test.ts +13 -1
- package/edit/provisioning.cattle.io.cluster/__tests__/rke2-fleet-cluster-agent.test.ts +283 -0
- package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +65 -49
- package/edit/provisioning.cattle.io.cluster/ingress/IngressCards.vue +112 -0
- package/edit/provisioning.cattle.io.cluster/ingress/IngressConfiguration.vue +158 -0
- package/edit/provisioning.cattle.io.cluster/rke2.vue +171 -72
- package/edit/provisioning.cattle.io.cluster/shared.ts +36 -1
- package/edit/provisioning.cattle.io.cluster/tabs/AgentConfiguration.vue +2 -1
- package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +55 -7
- package/edit/provisioning.cattle.io.cluster/tabs/Ingress.vue +319 -0
- package/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue +2 -1
- package/edit/provisioning.cattle.io.cluster/tabs/etcd/__tests__/S3Config.test.ts +13 -1
- package/edit/provisioning.cattle.io.cluster/tabs/networking/index.vue +10 -44
- package/edit/secret/index.vue +1 -1
- package/edit/token.vue +68 -29
- package/edit/workload/__tests__/index.test.ts +2 -37
- package/edit/workload/index.vue +6 -2
- package/edit/workload/mixins/workload.js +0 -32
- package/list/__tests__/management.cattle.io.setting.test.ts +198 -0
- package/list/management.cattle.io.setting.vue +13 -0
- package/list/provisioning.cattle.io.cluster.vue +50 -1
- package/list/secret.vue +4 -9
- package/list/service.vue +6 -8
- package/machine-config/amazonec2.vue +11 -4
- package/machine-config/components/EC2Networking.vue +46 -30
- package/machine-config/components/__tests__/EC2Networking.test.ts +7 -7
- package/machine-config/components/__tests__/utils/vpcSubnetMockData.js +0 -9
- package/machine-config/digitalocean.vue +3 -3
- package/models/__tests__/namespace.test.ts +11 -0
- package/models/__tests__/provisioning.cattle.io.cluster.test.ts +96 -0
- package/models/__tests__/workload.test.ts +42 -1
- package/models/catalog.cattle.io.clusterrepo.js +30 -4
- package/models/ext.cattle.io.token.js +48 -0
- package/models/kontainerdriver.js +2 -2
- package/models/namespace.js +7 -1
- package/models/nodedriver.js +2 -2
- package/models/provisioning.cattle.io.cluster.js +28 -7
- package/models/secret.js +0 -17
- package/models/service.js +44 -1
- package/models/token.js +4 -0
- package/models/workload.js +12 -6
- package/package.json +1 -1
- package/pages/account/index.vue +96 -67
- package/pages/auth/setup.vue +5 -14
- package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +4 -1
- package/pages/c/_cluster/apps/charts/index.vue +93 -4
- package/pages/c/_cluster/apps/charts/install.vue +317 -42
- package/pages/c/_cluster/manager/drivers/kontainerDriver/index.vue +5 -4
- package/pages/c/_cluster/settings/index.vue +3 -1
- package/plugins/dashboard-store/__tests__/getters.test.ts +108 -0
- package/plugins/dashboard-store/__tests__/resource-class.test.ts +27 -0
- package/plugins/dashboard-store/actions.js +3 -8
- package/plugins/dashboard-store/getters.js +7 -5
- package/plugins/dashboard-store/mutations.js +4 -1
- package/plugins/dashboard-store/resource-class.js +3 -3
- package/plugins/steve/__tests__/steve-class.test.ts +102 -141
- package/plugins/steve/steve-class.js +12 -3
- package/plugins/steve/steve-pagination-utils.ts +6 -2
- package/rancher-components/RcIcon/types.ts +2 -0
- package/rancher-components/RcItemCard/RcItemCard.vue +64 -19
- package/store/prefs.js +3 -0
- package/types/aws-sdk.d.ts +121 -0
- package/types/resources/node.ts +15 -0
- package/types/shell/index.d.ts +536 -506
- package/types/store/pagination.types.ts +5 -5
- package/utils/__tests__/array.test.ts +1 -29
- package/utils/__tests__/cluster-agent-configuration.test.ts +203 -0
- package/utils/array.ts +0 -11
- package/utils/aws.ts +21 -0
- package/utils/cluster.js +22 -2
- package/utils/selector-typed.ts +1 -1
- package/components/__tests__/ProjectRow.test.ts +0 -206
- package/components/form/ResourceQuota/ProjectRow.vue +0 -277
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils';
|
|
2
|
+
import { nextTick } from 'vue';
|
|
3
|
+
import SchedulingCustomization from '@shell/components/form/SchedulingCustomization.vue';
|
|
4
|
+
import { AGENT_CONFIGURATION_TYPES } from '@shell/config/settings';
|
|
5
|
+
import { _CREATE, _EDIT } from '@shell/config/query-params';
|
|
6
|
+
|
|
7
|
+
const mockStore = { getters: { 'i18n/t': jest.fn().mockImplementation((key: string) => key) } };
|
|
8
|
+
|
|
9
|
+
const createWrapper = (propsData: any = {}) => {
|
|
10
|
+
return mount(SchedulingCustomization, {
|
|
11
|
+
propsData: {
|
|
12
|
+
type: AGENT_CONFIGURATION_TYPES.CLUSTER,
|
|
13
|
+
feature: true,
|
|
14
|
+
mode: _CREATE,
|
|
15
|
+
defaultPC: { value: 100, preemptionPolicy: 'PreemptLowerPriority' },
|
|
16
|
+
defaultPDB: { maxUnavailable: 1 },
|
|
17
|
+
...propsData
|
|
18
|
+
},
|
|
19
|
+
global: {
|
|
20
|
+
mocks: {
|
|
21
|
+
$store: mockStore,
|
|
22
|
+
t: jest.fn().mockImplementation((key: string) => key)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
describe('component: SchedulingCustomization - Agent Type Support', () => {
|
|
29
|
+
let wrapper: any;
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
jest.clearAllMocks();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
if (wrapper) {
|
|
37
|
+
wrapper.unmount();
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('agent Type Prop', () => {
|
|
42
|
+
it('should accept and use CLUSTER agent type', () => {
|
|
43
|
+
wrapper = createWrapper({ type: AGENT_CONFIGURATION_TYPES.CLUSTER });
|
|
44
|
+
|
|
45
|
+
expect(wrapper.vm.type).toBe(AGENT_CONFIGURATION_TYPES.CLUSTER);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should accept and use FLEET agent type', () => {
|
|
49
|
+
wrapper = createWrapper({ type: AGENT_CONFIGURATION_TYPES.FLEET });
|
|
50
|
+
|
|
51
|
+
expect(wrapper.vm.type).toBe(AGENT_CONFIGURATION_TYPES.FLEET);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should emit scheduling-customization-changed event with correct agent type for CLUSTER', async() => {
|
|
55
|
+
wrapper = createWrapper({ type: AGENT_CONFIGURATION_TYPES.CLUSTER });
|
|
56
|
+
|
|
57
|
+
const checkbox = wrapper.findComponent('[data-testid="scheduling-customization-checkbox"]');
|
|
58
|
+
|
|
59
|
+
await checkbox.vm.$emit('update:value', true);
|
|
60
|
+
|
|
61
|
+
expect(wrapper.emitted('scheduling-customization-changed')).toBeTruthy();
|
|
62
|
+
expect(wrapper.emitted('scheduling-customization-changed')[0][0]).toStrictEqual({
|
|
63
|
+
event: true,
|
|
64
|
+
agentType: AGENT_CONFIGURATION_TYPES.CLUSTER
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should emit scheduling-customization-changed event with correct agent type for FLEET', async() => {
|
|
69
|
+
wrapper = createWrapper({ type: AGENT_CONFIGURATION_TYPES.FLEET });
|
|
70
|
+
|
|
71
|
+
const checkbox = wrapper.findComponent('[data-testid="scheduling-customization-checkbox"]');
|
|
72
|
+
|
|
73
|
+
await checkbox.vm.$emit('update:value', false);
|
|
74
|
+
|
|
75
|
+
expect(wrapper.emitted('scheduling-customization-changed')).toBeTruthy();
|
|
76
|
+
expect(wrapper.emitted('scheduling-customization-changed')[0][0]).toStrictEqual({
|
|
77
|
+
event: false,
|
|
78
|
+
agentType: AGENT_CONFIGURATION_TYPES.FLEET
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('settings Mismatch Detection', () => {
|
|
84
|
+
it('should detect priority class value mismatch', () => {
|
|
85
|
+
const value = {
|
|
86
|
+
priorityClass: { value: 200, preemptionPolicy: 'PreemptLowerPriority' },
|
|
87
|
+
podDisruptionBudget: { maxUnavailable: 1 }
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
wrapper = createWrapper({
|
|
91
|
+
value,
|
|
92
|
+
defaultPC: { value: 100, preemptionPolicy: 'PreemptLowerPriority' },
|
|
93
|
+
mode: _EDIT
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(wrapper.vm.settingMissmatch).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should detect priority class preemption policy mismatch', () => {
|
|
100
|
+
const value = {
|
|
101
|
+
priorityClass: { value: 100, preemptionPolicy: 'Never' },
|
|
102
|
+
podDisruptionBudget: { maxUnavailable: 1 }
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
wrapper = createWrapper({
|
|
106
|
+
value,
|
|
107
|
+
defaultPC: { value: 100, preemptionPolicy: 'PreemptLowerPriority' },
|
|
108
|
+
mode: _EDIT
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
expect(wrapper.vm.settingMissmatch).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should detect pod disruption budget maxUnavailable mismatch', () => {
|
|
115
|
+
const value = {
|
|
116
|
+
priorityClass: { value: 100, preemptionPolicy: 'PreemptLowerPriority' },
|
|
117
|
+
podDisruptionBudget: { maxUnavailable: 2 }
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
wrapper = createWrapper({
|
|
121
|
+
value,
|
|
122
|
+
defaultPDB: { maxUnavailable: 1 },
|
|
123
|
+
mode: _EDIT
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
expect(wrapper.vm.settingMissmatch).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should detect pod disruption budget minAvailable mismatch', () => {
|
|
130
|
+
const value = {
|
|
131
|
+
priorityClass: { value: 100, preemptionPolicy: 'PreemptLowerPriority' },
|
|
132
|
+
podDisruptionBudget: { minAvailable: 2 }
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
wrapper = createWrapper({
|
|
136
|
+
value,
|
|
137
|
+
defaultPDB: { minAvailable: 1 },
|
|
138
|
+
mode: _EDIT
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(wrapper.vm.settingMissmatch).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should not detect mismatch when values match defaults', () => {
|
|
145
|
+
const value = {
|
|
146
|
+
priorityClass: { value: 100, preemptionPolicy: 'PreemptLowerPriority' },
|
|
147
|
+
podDisruptionBudget: { maxUnavailable: 1 }
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
wrapper = createWrapper({
|
|
151
|
+
value,
|
|
152
|
+
defaultPC: { value: 100, preemptionPolicy: 'PreemptLowerPriority' },
|
|
153
|
+
defaultPDB: { maxUnavailable: 1 },
|
|
154
|
+
mode: _EDIT
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
expect(wrapper.vm.settingMissmatch).toBe(false);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should not detect mismatch when no value is provided', () => {
|
|
161
|
+
wrapper = createWrapper({
|
|
162
|
+
value: undefined,
|
|
163
|
+
mode: _EDIT
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
expect(wrapper.vm.settingMissmatch).toBe(false);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('component State', () => {
|
|
171
|
+
it('should be enabled when value is provided', () => {
|
|
172
|
+
const value = {
|
|
173
|
+
priorityClass: { value: 100 },
|
|
174
|
+
podDisruptionBudget: { maxUnavailable: 1 }
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
wrapper = createWrapper({ value });
|
|
178
|
+
|
|
179
|
+
expect(wrapper.vm.enabled).toBe(true);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should be disabled when no value is provided', () => {
|
|
183
|
+
wrapper = createWrapper({ value: undefined });
|
|
184
|
+
|
|
185
|
+
expect(wrapper.vm.enabled).toBe(false);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should detect edit mode correctly', () => {
|
|
189
|
+
wrapper = createWrapper({ mode: _EDIT });
|
|
190
|
+
|
|
191
|
+
expect(wrapper.vm.isEdit).toBe(true);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should detect create mode correctly', () => {
|
|
195
|
+
wrapper = createWrapper({ mode: _CREATE });
|
|
196
|
+
|
|
197
|
+
expect(wrapper.vm.isEdit).toBe(false);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('banner Display in Edit Mode', () => {
|
|
202
|
+
it('should show banner when in edit mode and settings mismatch', async() => {
|
|
203
|
+
const value = {
|
|
204
|
+
priorityClass: { value: 200, preemptionPolicy: 'PreemptLowerPriority' },
|
|
205
|
+
podDisruptionBudget: { maxUnavailable: 1 }
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
wrapper = createWrapper({
|
|
209
|
+
value,
|
|
210
|
+
defaultPC: { value: 100, preemptionPolicy: 'PreemptLowerPriority' },
|
|
211
|
+
mode: _EDIT,
|
|
212
|
+
feature: true
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
await nextTick();
|
|
216
|
+
|
|
217
|
+
// The banner should be available in the template when conditions are met
|
|
218
|
+
expect(wrapper.vm.settingMissmatch).toBe(true);
|
|
219
|
+
expect(wrapper.vm.isEdit).toBe(true);
|
|
220
|
+
expect(wrapper.vm.feature).toBe(true);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should not show banner when not in edit mode', () => {
|
|
224
|
+
const value = {
|
|
225
|
+
priorityClass: { value: 200, preemptionPolicy: 'PreemptLowerPriority' },
|
|
226
|
+
podDisruptionBudget: { maxUnavailable: 1 }
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
wrapper = createWrapper({
|
|
230
|
+
value,
|
|
231
|
+
defaultPC: { value: 100, preemptionPolicy: 'PreemptLowerPriority' },
|
|
232
|
+
mode: _CREATE,
|
|
233
|
+
feature: true
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
expect(wrapper.vm.isEdit).toBe(false);
|
|
237
|
+
// Banner should not show because not in edit mode
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
});
|
|
@@ -15,6 +15,10 @@ export default {
|
|
|
15
15
|
reference: {
|
|
16
16
|
type: String,
|
|
17
17
|
default: null,
|
|
18
|
+
},
|
|
19
|
+
getCustomDetailLink: {
|
|
20
|
+
type: Function,
|
|
21
|
+
default: null
|
|
18
22
|
}
|
|
19
23
|
},
|
|
20
24
|
computed: {
|
|
@@ -23,6 +27,10 @@ export default {
|
|
|
23
27
|
return get(this.row, this.reference);
|
|
24
28
|
}
|
|
25
29
|
|
|
30
|
+
if (this.getCustomDetailLink) {
|
|
31
|
+
return this.getCustomDetailLink(this.row);
|
|
32
|
+
}
|
|
33
|
+
|
|
26
34
|
return this.row?.detailLocation;
|
|
27
35
|
},
|
|
28
36
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { MANAGEMENT } from '@shell/config/types';
|
|
3
|
+
import { STORE } from '@shell/store/store-types';
|
|
4
|
+
import { mapGetters } from 'vuex';
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
props: {
|
|
8
|
+
row: {
|
|
9
|
+
type: Object,
|
|
10
|
+
required: true
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
async fetch() {
|
|
14
|
+
if (this.row.isProjectScoped && this.currentCluster?.isLocal) {
|
|
15
|
+
const id = this.row.projectScopedClusterId;
|
|
16
|
+
|
|
17
|
+
if (id && !this.$store.getters[`${ STORE.MANAGEMENT }/byId`](MANAGEMENT.CLUSTER, id)) {
|
|
18
|
+
try {
|
|
19
|
+
await this.$store.dispatch(`${ STORE.MANAGEMENT }/find`, { type: MANAGEMENT.CLUSTER, id });
|
|
20
|
+
} catch (e) {
|
|
21
|
+
// Ignore error
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
computed: {
|
|
27
|
+
...mapGetters(['currentCluster']),
|
|
28
|
+
originText() {
|
|
29
|
+
if (this.row.isProjectScoped) {
|
|
30
|
+
return this.t('secret.projectScoped.origin.source');
|
|
31
|
+
} else if (this.row.isProjectSecretCopy) {
|
|
32
|
+
return this.t('secret.projectScoped.origin.copy');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return '';
|
|
36
|
+
},
|
|
37
|
+
tooltip() {
|
|
38
|
+
if (this.row.isProjectScoped) {
|
|
39
|
+
const projectName = this.row.project?.nameDisplay || this.row.projectScopedProjectId;
|
|
40
|
+
const clusterName = this.row.projectCluster?.nameDisplay || this.row.projectScopedClusterId;
|
|
41
|
+
|
|
42
|
+
return this.t('secret.projectScoped.tooltip.source', { project: projectName, cluster: clusterName });
|
|
43
|
+
} else if (this.row.isProjectSecretCopy) {
|
|
44
|
+
const projectID = this.row.projectScopedProjectId;
|
|
45
|
+
const clusterId = this.currentCluster?.id;
|
|
46
|
+
|
|
47
|
+
// Try to fetch the project.
|
|
48
|
+
// Note: The management store might not have the project loaded if we haven't visited the cluster list or project list.
|
|
49
|
+
// However, if we are in the dashboard, we usually have projects loaded.
|
|
50
|
+
const project = this.$store.getters[`${ STORE.MANAGEMENT }/byId`](MANAGEMENT.PROJECT, `${ clusterId }/${ projectID }`);
|
|
51
|
+
const projectName = project?.nameDisplay || projectID;
|
|
52
|
+
|
|
53
|
+
return this.t('secret.projectScoped.tooltip.copy', { secret: this.row.nameDisplay, project: projectName });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
</script>
|
|
61
|
+
|
|
62
|
+
<template>
|
|
63
|
+
<div
|
|
64
|
+
v-if="originText"
|
|
65
|
+
v-clean-tooltip="tooltip"
|
|
66
|
+
class="secret-origin"
|
|
67
|
+
>
|
|
68
|
+
{{ originText }}
|
|
69
|
+
</div>
|
|
70
|
+
<div v-else>
|
|
71
|
+
—
|
|
72
|
+
</div>
|
|
73
|
+
</template>
|
|
74
|
+
|
|
75
|
+
<style lang="scss" scoped>
|
|
76
|
+
.secret-origin {
|
|
77
|
+
white-space: nowrap;
|
|
78
|
+
}
|
|
79
|
+
</style>
|
|
@@ -108,12 +108,13 @@ export const CATALOG = {
|
|
|
108
108
|
_CLUSTER_TPL: 'cluster-template',
|
|
109
109
|
_CLUSTER_TOOL: 'cluster-tool',
|
|
110
110
|
|
|
111
|
-
COMPONENT:
|
|
112
|
-
SOURCE_REPO_TYPE:
|
|
113
|
-
SOURCE_REPO_NAME:
|
|
114
|
-
COLOR:
|
|
115
|
-
DISPLAY_NAME:
|
|
116
|
-
CLUSTER_REPO_NAME:
|
|
111
|
+
COMPONENT: 'catalog.cattle.io/ui-component',
|
|
112
|
+
SOURCE_REPO_TYPE: 'catalog.cattle.io/ui-source-repo-type',
|
|
113
|
+
SOURCE_REPO_NAME: 'catalog.cattle.io/ui-source-repo',
|
|
114
|
+
COLOR: 'catalog.cattle.io/ui-color',
|
|
115
|
+
DISPLAY_NAME: 'catalog.cattle.io/display-name',
|
|
116
|
+
CLUSTER_REPO_NAME: 'catalog.cattle.io/cluster-repo-name',
|
|
117
|
+
SUSE_APP_COLLECTION: 'catalog.cattle.io/suse-application-collection',
|
|
117
118
|
|
|
118
119
|
SUPPORTED_OS: 'catalog.cattle.io/os',
|
|
119
120
|
PERMITTED_OS: 'catalog.cattle.io/permits-os',
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
STATE, NAME as NAME_COL, NAMESPACE as NAMESPACE_COL, AGE, OBJECT,
|
|
4
4
|
EVENT_LAST_SEEN_TIME,
|
|
5
5
|
EVENT_TYPE,
|
|
6
|
-
|
|
6
|
+
SECRET_ORIGIN,
|
|
7
7
|
EVENT_FIRST_SEEN_TIME
|
|
8
8
|
} from '@shell/config/table-headers';
|
|
9
9
|
|
|
@@ -89,7 +89,9 @@ export const STEVE_LIST_GROUPS = [{
|
|
|
89
89
|
groupLabelKey: 'groupByLabel',
|
|
90
90
|
}];
|
|
91
91
|
|
|
92
|
-
export const
|
|
93
|
-
...
|
|
94
|
-
sort
|
|
92
|
+
export const STEVE_SECRET_ORIGIN = {
|
|
93
|
+
...SECRET_ORIGIN,
|
|
94
|
+
// We can't sort by the 'UI_PROJECT_SECRET' label (management.cattle.io/project-scoped-secret) due to backend limitations.
|
|
95
|
+
// So we sort by the 'UI_PROJECT_SECRET_COPY' annotation (management.cattle.io/project-scoped-secret-copy) which at least groups the copies.
|
|
96
|
+
sort: `metadata.annotations[${ UI_PROJECT_SECRET_COPY }]:desc`,
|
|
95
97
|
};
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
USER_ID, USERNAME, USER_DISPLAY_NAME, USER_PROVIDER, USER_LAST_LOGIN, USER_DISABLED_IN, USER_DELETED_IN, WORKLOAD_ENDPOINTS, STORAGE_CLASS_DEFAULT,
|
|
20
20
|
STORAGE_CLASS_PROVISIONER, PERSISTENT_VOLUME_SOURCE,
|
|
21
21
|
HPA_REFERENCE, MIN_REPLICA, MAX_REPLICA, CURRENT_REPLICA,
|
|
22
|
-
|
|
22
|
+
DESCRIPTION, SUB_TYPE, PERSISTENT_VOLUME_CLAIM, RECLAIM_POLICY, PV_REASON, WORKLOAD_HEALTH_SCALE, POD_RESTARTS,
|
|
23
23
|
DURATION, MESSAGE, REASON, EVENT_TYPE, OBJECT, ROLE, ROLES, VERSION, INTERNAL_EXTERNAL_IP, KUBE_NODE_OS, CPU, RAM, SECRET_DATA,
|
|
24
24
|
EVENT_LAST_SEEN_TIME,
|
|
25
25
|
EVENT_FIRST_SEEN_TIME,
|
|
@@ -546,16 +546,6 @@ export function init(store) {
|
|
|
546
546
|
AGE
|
|
547
547
|
]);
|
|
548
548
|
|
|
549
|
-
headers(NORMAN.TOKEN, [
|
|
550
|
-
EXPIRY_STATE,
|
|
551
|
-
ACCESS_KEY,
|
|
552
|
-
DESCRIPTION,
|
|
553
|
-
SCOPE_NORMAN,
|
|
554
|
-
LAST_USED,
|
|
555
|
-
EXPIRES,
|
|
556
|
-
AGE_NORMAN
|
|
557
|
-
]);
|
|
558
|
-
|
|
559
549
|
virtualType({
|
|
560
550
|
label: store.getters['i18n/t']('clusterIndexPage.header'),
|
|
561
551
|
group: 'Root',
|
package/config/query-params.js
CHANGED
|
@@ -94,3 +94,6 @@ export const SECRET_QUERY_PARAMS = {
|
|
|
94
94
|
export const SECRET_SCOPE = 'scope';
|
|
95
95
|
// RANCHER OIDC CLIENT
|
|
96
96
|
export const RANCHER_AS_OIDC_QUERY_PARAMS = ['scope', 'client_id', 'redirect_uri', 'response_type'];
|
|
97
|
+
|
|
98
|
+
// For REPOSITORIES, to determine which type of repo to create, used on clusterrepo create page
|
|
99
|
+
export const TARGET = 'target';
|
package/config/settings.ts
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
import { GC_DEFAULTS, GC_PREFERENCES } from '@shell/utils/gc/gc-types';
|
|
3
3
|
import { PaginationSettings } from '@shell/types/resources/settings';
|
|
4
4
|
|
|
5
|
+
export const AGENT_CONFIGURATION_TYPES = {
|
|
6
|
+
CLUSTER: 'cluster',
|
|
7
|
+
FLEET: 'fleet'
|
|
8
|
+
} as const;
|
|
9
|
+
|
|
5
10
|
interface GlobalSettingRuleset {
|
|
6
11
|
name: string,
|
|
7
12
|
key?: string | number,
|
|
@@ -17,6 +22,7 @@ interface GlobalSetting {
|
|
|
17
22
|
kind?: string,
|
|
18
23
|
options?: string[]
|
|
19
24
|
readOnly?: boolean,
|
|
25
|
+
agent?: typeof AGENT_CONFIGURATION_TYPES.CLUSTER | typeof AGENT_CONFIGURATION_TYPES.FLEET,
|
|
20
26
|
/**
|
|
21
27
|
* Function used from the form validation
|
|
22
28
|
*/
|
|
@@ -111,6 +117,8 @@ export const SETTING = {
|
|
|
111
117
|
IMPORTED_CLUSTER_VERSION_MANAGEMENT: 'imported-cluster-version-management',
|
|
112
118
|
CLUSTER_AGENT_DEFAULT_PRIORITY_CLASS: 'cluster-agent-default-priority-class',
|
|
113
119
|
CLUSTER_AGENT_DEFAULT_POD_DISTRIBUTION_BUDGET: 'cluster-agent-default-pod-disruption-budget',
|
|
120
|
+
FLEET_AGENT_DEFAULT_PRIORITY_CLASS: 'fleet-agent-default-priority-class',
|
|
121
|
+
FLEET_AGENT_DEFAULT_POD_DISTRIBUTION_BUDGET: 'fleet-agent-default-pod-disruption-budget',
|
|
114
122
|
KEV2_OPERATORS: 'kev2-operators',
|
|
115
123
|
/**
|
|
116
124
|
* Dynamic Content settings
|
|
@@ -178,8 +186,11 @@ export const ALLOWED_SETTINGS: GlobalSetting = {
|
|
|
178
186
|
ruleSet: [{ name: 'minValue', factoryArg: 1 }]
|
|
179
187
|
},
|
|
180
188
|
[SETTING.IMPORTED_CLUSTER_VERSION_MANAGEMENT]: { kind: 'boolean' },
|
|
181
|
-
|
|
182
|
-
[SETTING.
|
|
189
|
+
// Configuration setup for agent configuration. Setting this up will activate the specific banner configuration.
|
|
190
|
+
[SETTING.CLUSTER_AGENT_DEFAULT_PRIORITY_CLASS]: { kind: 'json', agent: AGENT_CONFIGURATION_TYPES.CLUSTER },
|
|
191
|
+
[SETTING.CLUSTER_AGENT_DEFAULT_POD_DISTRIBUTION_BUDGET]: { kind: 'json', agent: AGENT_CONFIGURATION_TYPES.CLUSTER },
|
|
192
|
+
[SETTING.FLEET_AGENT_DEFAULT_PRIORITY_CLASS]: { kind: 'json', agent: AGENT_CONFIGURATION_TYPES.FLEET },
|
|
193
|
+
[SETTING.FLEET_AGENT_DEFAULT_POD_DISTRIBUTION_BUDGET]: { kind: 'json', agent: AGENT_CONFIGURATION_TYPES.FLEET }
|
|
183
194
|
|
|
184
195
|
};
|
|
185
196
|
|
|
@@ -195,6 +206,8 @@ export const PROVISIONING_SETTINGS = [
|
|
|
195
206
|
SETTING.IMPORTED_CLUSTER_VERSION_MANAGEMENT,
|
|
196
207
|
SETTING.CLUSTER_AGENT_DEFAULT_PRIORITY_CLASS,
|
|
197
208
|
SETTING.CLUSTER_AGENT_DEFAULT_POD_DISTRIBUTION_BUDGET,
|
|
209
|
+
SETTING.FLEET_AGENT_DEFAULT_PRIORITY_CLASS,
|
|
210
|
+
SETTING.FLEET_AGENT_DEFAULT_POD_DISTRIBUTION_BUDGET,
|
|
198
211
|
];
|
|
199
212
|
|
|
200
213
|
/**
|
package/config/table-headers.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CATTLE_PUBLIC_ENDPOINTS
|
|
1
|
+
import { CATTLE_PUBLIC_ENDPOINTS } from '@shell/config/labels-annotations';
|
|
2
2
|
import { NODE as NODE_TYPE, NAMESPACE as NAMESPACE_TYPE } from '@shell/config/types';
|
|
3
3
|
import { COLUMN_BREAKPOINTS } from '@shell/types/store/type-map';
|
|
4
4
|
|
|
@@ -55,6 +55,11 @@ export const NAME = {
|
|
|
55
55
|
canBeVariable: true,
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
+
export const PROJECT_NAMESPACES_NAME = {
|
|
59
|
+
...NAME,
|
|
60
|
+
search: ['nameDisplay', 'projectNameDisplay'],
|
|
61
|
+
};
|
|
62
|
+
|
|
58
63
|
export const LOGGING_OUTPUT_PROVIDERS = {
|
|
59
64
|
name: 'logging-output-providers',
|
|
60
65
|
labelKey: 'tableHeaders.loggingOutputProviders',
|
|
@@ -385,27 +390,17 @@ export const SECRET_DATA = {
|
|
|
385
390
|
formatter: 'SecretData'
|
|
386
391
|
};
|
|
387
392
|
|
|
388
|
-
export const
|
|
389
|
-
name: 'secret-
|
|
390
|
-
labelKey: 'tableHeaders.secret.
|
|
391
|
-
tooltip: 'tableHeaders.secret.
|
|
392
|
-
|
|
393
|
-
sort: `metadata.annotations."${ UI_PROJECT_SECRET_COPY }"`,
|
|
394
|
-
search: false,
|
|
395
|
-
formatter: 'Checked',
|
|
396
|
-
};
|
|
397
|
-
|
|
398
|
-
export const SECRET_PROJECT_SCOPED = {
|
|
399
|
-
name: 'secret-project-scoped',
|
|
400
|
-
labelKey: 'tableHeaders.secret.project-scoped',
|
|
401
|
-
tooltip: 'tableHeaders.secret.project-scoped-tooltip',
|
|
402
|
-
value: 'clusterAndProjectLabel',
|
|
393
|
+
export const SECRET_ORIGIN = {
|
|
394
|
+
name: 'secret-origin',
|
|
395
|
+
labelKey: 'tableHeaders.secret.origin',
|
|
396
|
+
tooltip: 'tableHeaders.secret.originTooltip',
|
|
397
|
+
formatter: 'SecretOrigin',
|
|
403
398
|
// Cannot _sort_ upstream secrets by if they are cluster scoped
|
|
404
399
|
// https://github.com/rancher/rancher/issues/51001
|
|
405
400
|
// metadata.labels[management.cattle.io/project-scoped-secret] - covers both cluster scoped AND clones
|
|
406
401
|
// metadata.annotations[management.cattle.io/project-scoped-secret-copy]
|
|
407
402
|
// sort: [`metadata.labels[${ UI_PROJECT_SECRET }]`, `metadata.annotations[${ UI_PROJECT_SECRET_COPY }]`],
|
|
408
|
-
search:
|
|
403
|
+
search: false,
|
|
409
404
|
};
|
|
410
405
|
|
|
411
406
|
export const TARGET_KIND = {
|
|
@@ -1038,6 +1033,15 @@ export const SCOPE_NORMAN = {
|
|
|
1038
1033
|
sort: ['clusterId'],
|
|
1039
1034
|
};
|
|
1040
1035
|
|
|
1036
|
+
export const NORMAN_KEY_DEPRECATION = {
|
|
1037
|
+
name: 'isNormanKeyDeprecated',
|
|
1038
|
+
labelKey: 'tableHeaders.isLegacy',
|
|
1039
|
+
value: (row) => row.isDeprecated ? 'True' : undefined,
|
|
1040
|
+
sort: 'isDeprecated',
|
|
1041
|
+
align: 'left',
|
|
1042
|
+
dashIfEmpty: true,
|
|
1043
|
+
};
|
|
1044
|
+
|
|
1041
1045
|
export const EXPIRES = {
|
|
1042
1046
|
name: 'expires',
|
|
1043
1047
|
value: 'expiresAt',
|
package/config/types.js
CHANGED
|
@@ -269,6 +269,7 @@ export const EXT = {
|
|
|
269
269
|
GROUP_MEMBERSHIP_REFRESH_REQUESTS: 'ext.cattle.io.groupmembershiprefreshrequest',
|
|
270
270
|
PASSWORD_CHANGE_REQUESTS: 'ext.cattle.io.passwordchangerequest',
|
|
271
271
|
KUBECONFIG: 'ext.cattle.io.kubeconfig',
|
|
272
|
+
TOKEN: 'ext.cattle.io.token',
|
|
272
273
|
};
|
|
273
274
|
|
|
274
275
|
export const CAPI = {
|
|
@@ -390,21 +391,35 @@ export const ADDRESSES = {
|
|
|
390
391
|
export const DEFAULT_WORKSPACE = 'fleet-default';
|
|
391
392
|
|
|
392
393
|
export const AUTH_TYPE = {
|
|
393
|
-
_NONE:
|
|
394
|
-
_BASIC:
|
|
395
|
-
_SSH:
|
|
396
|
-
_S3:
|
|
397
|
-
_RKE:
|
|
394
|
+
_NONE: '_none',
|
|
395
|
+
_BASIC: '_basic',
|
|
396
|
+
_SSH: '_ssh',
|
|
397
|
+
_S3: '_S3',
|
|
398
|
+
_RKE: '_RKE',
|
|
399
|
+
_IMAGE_PULL_SECRET: '_IPS',
|
|
398
400
|
};
|
|
399
401
|
|
|
400
402
|
export const LOCAL_CLUSTER = 'local';
|
|
401
403
|
|
|
402
404
|
export const CLUSTER_REPO_TYPES = {
|
|
403
|
-
HELM_URL:
|
|
404
|
-
GIT_REPO:
|
|
405
|
-
OCI_URL:
|
|
405
|
+
HELM_URL: 'helm-url',
|
|
406
|
+
GIT_REPO: 'git-repo',
|
|
407
|
+
OCI_URL: 'oci-url',
|
|
408
|
+
SUSE_APP_COLLECTION: 'suse-application-collection'
|
|
406
409
|
};
|
|
407
410
|
|
|
411
|
+
/**
|
|
412
|
+
* The `generateName` prefix used when creating authentication secrets
|
|
413
|
+
* for SUSE App Collection repositories.
|
|
414
|
+
*/
|
|
415
|
+
export const CLUSTER_REPO_APPCO_AUTH_GENERATE_NAME = 'clusterrepo-appco-auth-';
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* The `generateName` prefix used when creating authentication secrets
|
|
419
|
+
* for standard repositories.
|
|
420
|
+
*/
|
|
421
|
+
export const CLUSTER_REPO_AUTH_GENERATE_NAME = 'clusterrepo-auth-';
|
|
422
|
+
|
|
408
423
|
export const ZERO_TIME = '0001-01-01T00:00:00Z';
|
|
409
424
|
|
|
410
425
|
export const DEFAULT_GRAFANA_STORAGE_SIZE = '10Gi';
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
|
3
3
|
import { NAMESPACE as NAMESPACE_COL } from '@shell/config/table-headers';
|
|
4
4
|
import {
|
|
5
|
-
POD, WORKLOAD_TYPES, SERVICE, INGRESS,
|
|
5
|
+
POD, WORKLOAD_TYPES, SERVICE, INGRESS, NAMESPACE, WORKLOAD_TYPE_TO_KIND_MAPPING, METRICS_SUPPORTED_KINDS
|
|
6
6
|
} from '@shell/config/types';
|
|
7
7
|
import ResourceTable from '@shell/components/ResourceTable';
|
|
8
8
|
import Tab from '@shell/components/Tabbed/Tab';
|
|
@@ -13,6 +13,7 @@ import DashboardMetrics from '@shell/components/DashboardMetrics';
|
|
|
13
13
|
import { mapGetters } from 'vuex';
|
|
14
14
|
import { allDashboardsExist } from '@shell/utils/grafana';
|
|
15
15
|
import { PROJECT } from '@shell/config/labels-annotations';
|
|
16
|
+
import { fetchNodesForServiceTargets } from '@shell/models/service';
|
|
16
17
|
|
|
17
18
|
const WORKLOAD_METRICS_DETAIL_URL = '/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/d/rancher-workload-pods-1/rancher-workload-pods?orgId=1';
|
|
18
19
|
const WORKLOAD_METRICS_SUMMARY_URL = '/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/d/rancher-workload-1/rancher-workload?orgId=1';
|
|
@@ -29,28 +30,23 @@ export default {
|
|
|
29
30
|
mixins: [CreateEditView],
|
|
30
31
|
|
|
31
32
|
async fetch() {
|
|
32
|
-
let hasNodes = false;
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
const inStore = this.$store.getters['currentStore']();
|
|
36
|
-
const schema = this.$store.getters[`${ inStore }/schemaFor`](NODE);
|
|
37
|
-
|
|
38
|
-
if (schema) {
|
|
39
|
-
hasNodes = true;
|
|
40
|
-
}
|
|
41
|
-
} catch {}
|
|
42
|
-
|
|
43
33
|
const hash = {
|
|
44
34
|
allIngresses: this.$store.dispatch('cluster/findAll', { type: INGRESS }),
|
|
45
|
-
// Nodes should be fetched because they may be referenced in the target
|
|
46
|
-
|
|
47
|
-
|
|
35
|
+
// Nodes should be fetched because they may be referenced in the target column of a service list item.
|
|
36
|
+
nodes: fetchNodesForServiceTargets({
|
|
37
|
+
$store: this.$store,
|
|
38
|
+
inStore: this.$store.getters['currentStore']()
|
|
39
|
+
})
|
|
48
40
|
};
|
|
49
41
|
|
|
50
42
|
if (this.podSchema) {
|
|
51
43
|
hash.pods = this.value.fetchPods();
|
|
52
44
|
}
|
|
53
45
|
|
|
46
|
+
if (this.serviceSchema) {
|
|
47
|
+
hash.servicesInNamespace = this.$store.dispatch('cluster/findAll', { type: SERVICE, opt: { namespaced: this.value.metadata.namespace } });
|
|
48
|
+
}
|
|
49
|
+
|
|
54
50
|
if (this.value.type === WORKLOAD_TYPES.CRON_JOB) {
|
|
55
51
|
hash.jobs = this.value.matchingJobs();
|
|
56
52
|
}
|
|
@@ -88,7 +84,6 @@ export default {
|
|
|
88
84
|
return {
|
|
89
85
|
allIngresses: [],
|
|
90
86
|
matchingIngresses: [],
|
|
91
|
-
allNodes: [],
|
|
92
87
|
WORKLOAD_METRICS_DETAIL_URL,
|
|
93
88
|
WORKLOAD_METRICS_SUMMARY_URL,
|
|
94
89
|
POD_PROJECT_METRICS_DETAIL_URL: '',
|
|
@@ -55,7 +55,7 @@ export default {
|
|
|
55
55
|
try {
|
|
56
56
|
await Promise.all(this.drivers.map(
|
|
57
57
|
(driver) => this.$store.dispatch('rancher/request', {
|
|
58
|
-
url: `v3/${ this.driverType }/${
|
|
58
|
+
url: `v3/${ this.driverType }/${ encodeURIComponent(driver.id) }?action=deactivate`,
|
|
59
59
|
method: 'POST'
|
|
60
60
|
})
|
|
61
61
|
));
|