@rancher/shell 3.0.9-rc.4 → 3.0.9-rc.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/assets/brand/suse/metadata.json +2 -1
  2. package/assets/translations/en-us.yaml +91 -3
  3. package/components/ActionMenuShell.vue +1 -1
  4. package/components/Inactivity.vue +2 -2
  5. package/components/Resource/Detail/Card/ExtrasCard.vue +49 -15
  6. package/components/Resource/Detail/Card/__tests__/ExtrasCard.test.ts +111 -0
  7. package/components/Resource/Detail/Masthead/__tests__/index.test.ts +0 -17
  8. package/components/Resource/Detail/Masthead/index.vue +11 -4
  9. package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +3 -1
  10. package/components/Resource/Detail/Metadata/index.vue +1 -1
  11. package/components/Resource/Detail/ResourceRow.vue +1 -1
  12. package/components/ResourceDetail/Masthead/latest.vue +12 -2
  13. package/components/ResourceList/index.vue +9 -0
  14. package/components/ResourceTable.vue +38 -4
  15. package/components/Tabbed/Tab.vue +4 -0
  16. package/components/Tabbed/index.vue +4 -1
  17. package/components/__tests__/ProjectRow.test.ts +60 -0
  18. package/components/form/ChangePassword.vue +41 -35
  19. package/components/form/ResourceQuota/Project.vue +42 -1
  20. package/components/form/ResourceQuota/ProjectRow.vue +71 -4
  21. package/components/form/ResourceQuota/__tests__/Project.test.ts +63 -0
  22. package/components/form/SelectOrCreateAuthSecret.vue +6 -1
  23. package/components/form/__tests__/SelectOrCreateAuthSecret.test.ts +35 -0
  24. package/components/formatter/MachineSummaryGraph.vue +10 -2
  25. package/components/nav/TopLevelMenu.helper.ts +50 -2
  26. package/components/nav/TopLevelMenu.vue +14 -0
  27. package/components/nav/Type.vue +5 -0
  28. package/components/nav/__tests__/TopLevelMenu.test.ts +3 -3
  29. package/components/nav/__tests__/Type.test.ts +6 -4
  30. package/config/product/explorer.js +4 -3
  31. package/config/product/manager.js +18 -1
  32. package/config/router/navigation-guards/authentication.js +8 -9
  33. package/config/types.js +10 -2
  34. package/detail/auditlog.cattle.io.auditpolicy.vue +19 -0
  35. package/detail/management.cattle.io.user.vue +1 -2
  36. package/detail/node.vue +0 -1
  37. package/detail/provisioning.cattle.io.cluster.vue +2 -1
  38. package/dialog/ChangePasswordDialog.vue +8 -0
  39. package/dialog/GenericPrompt.vue +20 -3
  40. package/dialog/ScaleMachineDownDialog.vue +65 -15
  41. package/dialog/SearchDialog.vue +10 -2
  42. package/dialog/__tests__/ScaleMachineDownDialog.test.ts +184 -0
  43. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +89 -0
  44. package/edit/__tests__/management.cattle.io.project.test.js +56 -1
  45. package/edit/auditlog.cattle.io.auditpolicy/AdditionalRedactions.vue +114 -0
  46. package/edit/auditlog.cattle.io.auditpolicy/Filters.vue +119 -0
  47. package/edit/auditlog.cattle.io.auditpolicy/General.vue +180 -0
  48. package/edit/auditlog.cattle.io.auditpolicy/__tests__/AdditionalRedactions.test.ts +327 -0
  49. package/edit/auditlog.cattle.io.auditpolicy/__tests__/Filters.test.ts +449 -0
  50. package/edit/auditlog.cattle.io.auditpolicy/__tests__/General.test.ts +472 -0
  51. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/AdditionalRedactions.test.ts.snap +27 -0
  52. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/Filters.test.ts.snap +39 -0
  53. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/General.test.ts.snap +174 -0
  54. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/index.test.ts.snap +29 -0
  55. package/edit/auditlog.cattle.io.auditpolicy/__tests__/index.test.ts +215 -0
  56. package/edit/auditlog.cattle.io.auditpolicy/index.vue +104 -0
  57. package/edit/auditlog.cattle.io.auditpolicy/types.ts +28 -0
  58. package/edit/fleet.cattle.io.gitrepo.vue +16 -1
  59. package/edit/management.cattle.io.project.vue +8 -2
  60. package/edit/management.cattle.io.user.vue +29 -34
  61. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +178 -0
  62. package/edit/provisioning.cattle.io.cluster/rke2.vue +21 -2
  63. package/edit/provisioning.cattle.io.cluster/shared.ts +4 -0
  64. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +1 -0
  65. package/edit/provisioning.cattle.io.cluster/tabs/etcd/S3Config.vue +1 -1
  66. package/list/auditlog.cattle.io.auditpolicy.vue +63 -0
  67. package/list/group.principal.vue +11 -15
  68. package/list/management.cattle.io.user.vue +11 -21
  69. package/machine-config/azure.vue +14 -0
  70. package/mixins/browser-tab-visibility.js +5 -4
  71. package/mixins/fetch.client.js +6 -0
  72. package/models/__tests__/auditlog.cattle.io.auditpolicy.test.ts +117 -0
  73. package/models/__tests__/workload.test.ts +49 -6
  74. package/models/auditlog.cattle.io.auditpolicy.js +46 -0
  75. package/models/cluster.x-k8s.io.machine.js +1 -1
  76. package/models/cluster.x-k8s.io.machinedeployment.js +5 -5
  77. package/models/event.js +5 -0
  78. package/models/ext.cattle.io.groupmembershiprefreshrequest.js +15 -0
  79. package/models/ext.cattle.io.passwordchangerequest.js +15 -0
  80. package/models/ext.cattle.io.selfuser.js +15 -0
  81. package/models/fleet-application.js +17 -7
  82. package/models/management.cattle.io.user.js +28 -31
  83. package/models/schema.js +18 -0
  84. package/models/secret.js +27 -24
  85. package/models/steve-schema.ts +39 -2
  86. package/models/workload.js +3 -2
  87. package/package.json +1 -1
  88. package/pages/account/index.vue +23 -16
  89. package/pages/auth/login.vue +15 -8
  90. package/pages/auth/setup.vue +52 -15
  91. package/pages/home.vue +9 -3
  92. package/plugins/dashboard-store/__tests__/resource-class.test.ts +1 -3
  93. package/plugins/dashboard-store/actions.js +7 -0
  94. package/plugins/dashboard-store/getters.js +23 -1
  95. package/plugins/dashboard-store/index.js +3 -2
  96. package/plugins/dashboard-store/mutations.js +4 -0
  97. package/plugins/dashboard-store/resource-class.js +12 -5
  98. package/plugins/steve/__tests__/steve-class.test.ts +167 -0
  99. package/plugins/steve/schema.d.ts +5 -0
  100. package/plugins/steve/steve-class.js +19 -0
  101. package/plugins/steve/steve-pagination-utils.ts +2 -1
  102. package/store/auth.js +57 -19
  103. package/store/notifications.ts +1 -1
  104. package/store/type-map.js +12 -1
  105. package/types/shell/index.d.ts +10 -14
  106. package/types/store/dashboard-store.types.ts +7 -0
  107. package/utils/pagination-wrapper.ts +11 -3
  108. package/vue.config.js +26 -13
  109. package/edit/provisioning.cattle.io.cluster/defaults.ts +0 -1
@@ -0,0 +1,184 @@
1
+ import { shallowMount } from '@vue/test-utils';
2
+ import ScaleMachineDownDialog from '@shell/dialog/ScaleMachineDownDialog.vue';
3
+ import { CAPI as CAPI_LABELS } from '@shell/config/labels-annotations';
4
+
5
+ const defaultStubs = { GenericPrompt: true };
6
+
7
+ const defaultGetters = { 'type-map/labelFor': jest.fn(() => 'Node') };
8
+
9
+ const defaultMocks = {
10
+ $store: {
11
+ dispatch: jest.fn(),
12
+ getters: defaultGetters
13
+ },
14
+ t: jest.fn((key) => key),
15
+ };
16
+
17
+ const defaultCluster = {
18
+ isRke2: true,
19
+ machines: [],
20
+ save: jest.fn()
21
+ };
22
+
23
+ const defaultResource = {
24
+ cluster: defaultCluster,
25
+ isWorker: true,
26
+ poolName: 'pool1',
27
+ pool: { scalePool: jest.fn() },
28
+ save: jest.fn(),
29
+ setAnnotation: jest.fn(),
30
+ nameDisplay: 'machine-1',
31
+ namespace: 'default',
32
+ schema: 'machine'
33
+ };
34
+
35
+ describe('component: ScaleMachineDownDialog', () => {
36
+ const createWrapper = (propsData = {}, mocks = {}) => {
37
+ return shallowMount(ScaleMachineDownDialog, {
38
+ propsData: {
39
+ resources: [defaultResource],
40
+ ...propsData
41
+ },
42
+ global: {
43
+ mocks: {
44
+ ...defaultMocks,
45
+ ...mocks
46
+ },
47
+ stubs: defaultStubs,
48
+ directives: { 'clean-html': true }
49
+ }
50
+ });
51
+ };
52
+
53
+ beforeEach(() => {
54
+ jest.clearAllMocks();
55
+ });
56
+
57
+ it('should render correctly', () => {
58
+ const wrapper = createWrapper();
59
+
60
+ expect(wrapper.exists()).toBe(true);
61
+ });
62
+
63
+ describe('data initialization', () => {
64
+ it('should identify safe machines to delete (Worker)', () => {
65
+ const wrapper = createWrapper();
66
+
67
+ expect(wrapper.vm.safeMachinesToDelete).toHaveLength(1);
68
+ expect(wrapper.vm.ignored).toHaveLength(0);
69
+ });
70
+
71
+ it('should ignore deletion if it is the last control plane', () => {
72
+ const cluster = {
73
+ isRke2: true,
74
+ machines: [{ isControlPlane: true }]
75
+ };
76
+ const resource = {
77
+ ...defaultResource,
78
+ cluster,
79
+ isControlPlane: true,
80
+ isWorker: false
81
+ };
82
+
83
+ const wrapper = createWrapper({ resources: [resource] });
84
+
85
+ expect(wrapper.vm.safeMachinesToDelete).toHaveLength(0);
86
+ expect(wrapper.vm.ignored).toHaveLength(1);
87
+ });
88
+
89
+ it('should allow deletion if multiple control planes exist', () => {
90
+ const cluster = {
91
+ isRke2: true,
92
+ machines: [{ isControlPlane: true }, { isControlPlane: true }]
93
+ };
94
+ const resource = {
95
+ ...defaultResource,
96
+ cluster,
97
+ isControlPlane: true,
98
+ isWorker: false
99
+ };
100
+
101
+ const wrapper = createWrapper({ resources: [resource] });
102
+
103
+ expect(wrapper.vm.safeMachinesToDelete).toHaveLength(1);
104
+ expect(wrapper.vm.ignored).toHaveLength(0);
105
+ });
106
+ });
107
+
108
+ describe('fetch', () => {
109
+ it('should fetch machine sets and update loading state', async() => {
110
+ const wrapper = createWrapper();
111
+
112
+ // Mock dispatch to return empty array for findAll
113
+ (wrapper.vm as any).$store.dispatch.mockResolvedValue([]);
114
+
115
+ await ScaleMachineDownDialog.fetch.call(wrapper.vm);
116
+
117
+ expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith('management/findAll', expect.anything());
118
+ expect(wrapper.vm.loading).toBe(false);
119
+ });
120
+ });
121
+
122
+ describe('remove', () => {
123
+ it('should perform RKE2 removal steps', async() => {
124
+ const wrapper = createWrapper();
125
+ const resource = (wrapper.vm as any).resources[0];
126
+
127
+ await (wrapper.vm as any).remove();
128
+
129
+ expect(resource.setAnnotation).toHaveBeenCalledWith(CAPI_LABELS.DELETE_MACHINE, 'true');
130
+ expect(resource.save).toHaveBeenCalledWith();
131
+ expect(resource.pool.scalePool).toHaveBeenCalledWith(-1, false);
132
+ expect(resource.cluster.save).toHaveBeenCalledWith();
133
+ });
134
+
135
+ it('should perform non-RKE2 removal steps', async() => {
136
+ const normanAction = jest.fn();
137
+ const resource = {
138
+ cluster: { isRke2: false },
139
+ provisioningCluster: { nodes: [] },
140
+ norman: { doAction: normanAction },
141
+ nameDisplay: 'node-1',
142
+ schema: 'node'
143
+ };
144
+
145
+ const wrapper = createWrapper({ resources: [resource] });
146
+
147
+ await (wrapper.vm as any).remove();
148
+
149
+ expect(normanAction).toHaveBeenCalledWith('scaledown');
150
+ });
151
+ });
152
+
153
+ describe('showScaling', () => {
154
+ it('should return true if replicas do not match readyReplicas', async() => {
155
+ const wrapper = createWrapper();
156
+
157
+ await wrapper.setData({
158
+ workerMachineSets: [{
159
+ data: [{
160
+ spec: { replicas: 2 },
161
+ status: { readyReplicas: 1 }
162
+ }]
163
+ }]
164
+ });
165
+
166
+ expect(wrapper.vm.showScaling).toBe(true);
167
+ });
168
+
169
+ it('should return false if replicas match readyReplicas', async() => {
170
+ const wrapper = createWrapper();
171
+
172
+ await wrapper.setData({
173
+ workerMachineSets: [{
174
+ data: [{
175
+ spec: { replicas: 2 },
176
+ status: { readyReplicas: 2 }
177
+ }]
178
+ }]
179
+ });
180
+
181
+ expect(wrapper.vm.showScaling).toBe(false);
182
+ });
183
+ });
184
+ });
@@ -1,5 +1,6 @@
1
1
  import { mount } from '@vue/test-utils';
2
2
  import { _CREATE, _EDIT, _VIEW } from '@shell/config/query-params';
3
+ import { AUTH_TYPE } from '@shell/config/types';
3
4
  import GitRepo from '@shell/models/fleet.cattle.io.gitrepo';
4
5
  import GitRepoComponent from '@shell/edit/fleet.cattle.io.gitrepo.vue';
5
6
 
@@ -237,3 +238,91 @@ describe.each([
237
238
 
238
239
  it.todo('test paths and subpaths');
239
240
  });
241
+
242
+ describe('view: fleet.cattle.io.gitrepo, GitHub password banner - should', () => {
243
+ it('show GitHub password banner when GitHub.com repository and basic auth is selected', async() => {
244
+ const wrapper = mount(GitRepoComponent, initGitRepo({ mode: _CREATE }, { spec: { repo: 'https://github.com/rancher/fleet-examples' } }));
245
+
246
+ // Check computed properties
247
+ expect(wrapper.vm.isGitHubDotComRepository).toBeTruthy();
248
+
249
+ // Set basic auth selection in tempCachedValues
250
+ await wrapper.setData({ tempCachedValues: { clientSecretName: { selected: AUTH_TYPE._BASIC } } });
251
+
252
+ // Check computed property after setting data
253
+ expect(wrapper.vm.isBasicAuthSelected).toBeTruthy();
254
+
255
+ await wrapper.vm.$nextTick();
256
+
257
+ const githubBanner = wrapper.find('[data-testid="gitrepo-githubdotcom-password-warning"]');
258
+
259
+ expect(githubBanner.exists()).toBeTruthy();
260
+
261
+ // Check the banner element
262
+ const bannerElement = wrapper.find('.banner.warning');
263
+
264
+ expect(bannerElement.exists()).toBeTruthy();
265
+ });
266
+
267
+ it('show GitHub password banner in edit mode when conditions are met', async() => {
268
+ const wrapper = mount(GitRepoComponent, initGitRepo({ mode: _EDIT }, { spec: { repo: 'https://github.com/rancher/fleet-examples' } }));
269
+
270
+ await wrapper.setData({ tempCachedValues: { clientSecretName: { selected: AUTH_TYPE._BASIC } } });
271
+
272
+ const githubBanner = wrapper.find('[data-testid="gitrepo-githubdotcom-password-warning"]');
273
+
274
+ expect(githubBanner.exists()).toBeTruthy();
275
+ });
276
+
277
+ it('hide GitHub password banner when not using GitHub.com repository', async() => {
278
+ const wrapper = mount(GitRepoComponent, initGitRepo({ mode: _CREATE }, { spec: { repo: 'https://gitlab.com/user/repo' } }));
279
+
280
+ await wrapper.setData({ tempCachedValues: { clientSecretName: { selected: AUTH_TYPE._SSH } } });
281
+
282
+ const githubBanner = wrapper.find('[data-testid="gitrepo-githubdotcom-password-warning"]');
283
+
284
+ expect(githubBanner.exists()).toBeFalsy();
285
+ });
286
+
287
+ it('hide GitHub password banner when not using basic auth', async() => {
288
+ const wrapper = mount(GitRepoComponent, initGitRepo({ mode: _CREATE }, { spec: { repo: 'https://github.com/rancher/fleet-examples' } }));
289
+
290
+ await wrapper.setData({ tempCachedValues: { clientSecretName: { selected: AUTH_TYPE._SSH } } });
291
+
292
+ const githubBanner = wrapper.find('[data-testid="gitrepo-githubdotcom-password-warning"]');
293
+
294
+ expect(githubBanner.exists()).toBeFalsy();
295
+ });
296
+
297
+ it('hide GitHub password banner when no auth is selected', async() => {
298
+ const wrapper = mount(GitRepoComponent, initGitRepo({ mode: _CREATE }, { spec: { repo: 'https://github.com/rancher/fleet-examples' } }));
299
+
300
+ await wrapper.setData({ tempCachedValues: { clientSecretName: { selected: AUTH_TYPE._NONE } } });
301
+
302
+ const githubBanner = wrapper.find('[data-testid="gitrepo-githubdotcom-password-warning"]');
303
+
304
+ expect(githubBanner.exists()).toBeFalsy();
305
+ });
306
+
307
+ it.each([
308
+ ['https://github.com/user/repo', true],
309
+ ['https://GitHub.com/user/repo', true],
310
+ ['HTTPS://GITHUB.COM/user/repo', true],
311
+ ['https://api.github.com/user/repo', false], // subdomain doesn't match
312
+ ['https://raw.github.com/user/repo', false], // subdomain doesn't match
313
+ ['https://company-github.com/user/repo', false], // doesn't contain exact 'https://github.com'
314
+ ['https://github.company.com/user/repo', true], // contains exact 'https://github.com' at start
315
+ ['https://gitlab.com/user/repo', false],
316
+ ['https://bitbucket.org/user/repo', false],
317
+ ['http://github.com/user/repo', false], // not https
318
+ ['git@github.com:user/repo.git', false], // not https
319
+ ])('correctly detect GitHub.com repository for URL: %s (expected: %s)', async(repoUrl, shouldShowBanner) => {
320
+ const wrapper = mount(GitRepoComponent, initGitRepo({ mode: _CREATE }, { spec: { repo: repoUrl } }));
321
+
322
+ await wrapper.setData({ tempCachedValues: { clientSecretName: { selected: AUTH_TYPE._BASIC } } });
323
+
324
+ const githubBanner = wrapper.find('[data-testid="gitrepo-githubdotcom-password-warning"]');
325
+
326
+ expect(githubBanner.exists()).toBe(shouldShowBanner);
327
+ });
328
+ });
@@ -1,4 +1,5 @@
1
1
  import { shallowMount } from '@vue/test-utils';
2
+ import CruResource from '@shell/components/CruResource';
2
3
  import ManagementCattleIoProject from '@shell/edit/management.cattle.io.project.vue';
3
4
  import { _EDIT } from '@shell/config/query-params';
4
5
 
@@ -15,7 +16,13 @@ const mockStore = {
15
16
  };
16
17
 
17
18
  const defaultMountOptions = {
18
- props: { mode: _EDIT },
19
+ props: {
20
+ mode: _EDIT,
21
+ value: {
22
+ spec: { containerDefaultResourceLimit: {} },
23
+ metadata: { namespace: 'test-ns', annotations: {} },
24
+ }
25
+ },
19
26
  global: { mocks: { $store: mockStore } }
20
27
  };
21
28
 
@@ -134,4 +141,52 @@ describe('component: ManagementCattleIoProject', () => {
134
141
  expect(wrapper.vm.value.spec.namespaceDefaultResourceQuota.limit.extended.test1).toBeUndefined();
135
142
  expect(wrapper.vm.value.spec.namespaceDefaultResourceQuota.limit.extended.test2).toBe('4');
136
143
  });
144
+
145
+ it('should update isQuotasValid when validateQuotas is called', () => {
146
+ const wrapper = shallowMount(ManagementCattleIoProject, defaultMountOptions);
147
+
148
+ wrapper.vm.validateQuotas(false);
149
+ expect(wrapper.vm.isQuotasValid).toStrictEqual(false);
150
+
151
+ wrapper.vm.validateQuotas(true);
152
+ expect(wrapper.vm.isQuotasValid).toStrictEqual(true);
153
+ });
154
+
155
+ it('cruResource validation-passed should be false if isQuotasValid is false', async() => {
156
+ const wrapper = shallowMount(ManagementCattleIoProject, defaultMountOptions);
157
+
158
+ await wrapper.setData({ isQuotasValid: false });
159
+
160
+ const cruResource = wrapper.findComponent(CruResource);
161
+
162
+ expect(cruResource.props('validationPassed')).toStrictEqual(false);
163
+ });
164
+
165
+ it('should remove an extended resource quota correctly with fixed startsWith', async() => {
166
+ const value = {
167
+ spec: {
168
+ resourceQuota: { limit: { extended: { test2: '1' } } },
169
+ namespaceDefaultResourceQuota: { limit: { extended: { test2: '2' } } }
170
+ },
171
+ metadata: { namespace: 'test-ns', annotations: {} },
172
+ save: jest.fn(),
173
+ listLocation: { name: 'list' }
174
+ };
175
+
176
+ const wrapper = shallowMount(
177
+ ManagementCattleIoProject,
178
+ {
179
+ ...defaultMountOptions,
180
+ props: {
181
+ ...defaultMountOptions.props,
182
+ value,
183
+ },
184
+ }
185
+ );
186
+
187
+ wrapper.vm.removeQuota('extended.test2');
188
+
189
+ expect(wrapper.vm.value.spec.resourceQuota.limit.extended).toBeUndefined();
190
+ expect(wrapper.vm.value.spec.namespaceDefaultResourceQuota.limit.extended).toBeUndefined();
191
+ });
137
192
  });
@@ -0,0 +1,114 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref } from 'vue';
3
+ import Tab from '@shell/components/Tabbed/Tab.vue';
4
+ import Tabbed from '@shell/components/Tabbed/index.vue';
5
+ import ArrayList from '@shell/components/form/ArrayList.vue';
6
+ import { AuditPolicy } from '@shell/edit/auditlog.cattle.io.auditpolicy/types';
7
+
8
+ // Component Props & Emits
9
+ const props = defineProps({
10
+ value: {
11
+ type: Object,
12
+ default: () => ({})
13
+ },
14
+ mode: {
15
+ type: String,
16
+ default: 'create'
17
+ },
18
+ });
19
+
20
+ const emit = defineEmits<{
21
+ 'update:value': [value: AuditPolicy];
22
+ }>();
23
+
24
+ // Default Values & Reactive Spec
25
+ const defaults: AuditPolicy = { additionalRedactions: [] };
26
+ const spec = ref<AuditPolicy>({ ...defaults, ...props.value });
27
+
28
+ // Methods
29
+ function addRedaction() {
30
+ const valueToEmit = { ...props.value, ...spec.value };
31
+
32
+ valueToEmit.additionalRedactions?.push({
33
+ headers: [],
34
+ paths: [],
35
+ });
36
+ emit('update:value', valueToEmit);
37
+ }
38
+
39
+ function removeRedaction(tab: number) {
40
+ const valueToEmit = { ...props.value, ...spec.value };
41
+
42
+ valueToEmit.additionalRedactions?.splice(tab, 1);
43
+ emit('update:value', valueToEmit);
44
+ }
45
+
46
+ const redactionLabel = computed(() => {
47
+ return (i: number) => `Rule ${ i + 1 }`;
48
+ });
49
+ </script>
50
+
51
+ <template>
52
+ <div>
53
+ <div class="row mb-40">
54
+ <div class="col span-12">
55
+ <Tabbed
56
+ :side-tabs="true"
57
+ :show-tabs-add-remove="mode !== 'view'"
58
+ :use-hash="true"
59
+ @addTab="addRedaction"
60
+ @removeTab="removeRedaction"
61
+ >
62
+ <Tab
63
+ v-for="(redaction, idx) in spec.additionalRedactions"
64
+ :key="idx"
65
+ :name="`rule-${idx}`"
66
+ :label="redactionLabel(idx)"
67
+ :show-header="false"
68
+ class="container-group"
69
+ >
70
+ <fieldset>
71
+ <h2>{{ t("auditPolicy.additionalRedactions.headers.title") }}</h2>
72
+ <div class="row">
73
+ <div class="col span-12">
74
+ <ArrayList
75
+ key="headers"
76
+ v-model:value="redaction.headers"
77
+ :value-placeholder="t('auditPolicy.additionalRedactions.headers.placeholder')"
78
+ :add-label="t('auditPolicy.additionalRedactions.headers.add')"
79
+ :mode="mode"
80
+ :protip="false"
81
+ />
82
+ </div>
83
+ </div>
84
+ </fieldset>
85
+ <div class="spacer" />
86
+ <fieldset>
87
+ <h2>
88
+ {{ t("auditPolicy.additionalRedactions.paths.title") }} <i
89
+ v-clean-tooltip="{content: t('auditPolicy.additionalRedactions.paths.tooltip'), triggers: ['hover', 'touch', 'focus'] }"
90
+ v-stripped-aria-label="t('auditPolicy.additionalRedactions.paths.tooltip')"
91
+ class="icon icon-info"
92
+ tabindex="0"
93
+ role="tooltip"
94
+ />
95
+ </h2>
96
+ <div class="row">
97
+ <div class="col span-12">
98
+ <ArrayList
99
+ key="paths"
100
+ v-model:value="redaction.paths"
101
+ :value-placeholder="t('auditPolicy.additionalRedactions.paths.placeholder')"
102
+ :add-label="t('auditPolicy.additionalRedactions.paths.add')"
103
+ :mode="mode"
104
+ :protip="false"
105
+ />
106
+ </div>
107
+ </div>
108
+ </fieldset>
109
+ </Tab>
110
+ </Tabbed>
111
+ </div>
112
+ </div>
113
+ </div>
114
+ </template>
@@ -0,0 +1,119 @@
1
+ <script setup lang="ts">
2
+ import { ref } from 'vue';
3
+ import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
4
+ import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
5
+ import ArrayList from '@shell/components/form/ArrayList.vue';
6
+ import { AuditPolicy, FilterRule } from '@shell/edit/auditlog.cattle.io.auditpolicy/types';
7
+
8
+ // Component Props & Emits
9
+ const props = defineProps({
10
+ value: {
11
+ type: Object,
12
+ default: () => ({})
13
+ },
14
+ mode: {
15
+ type: String,
16
+ default: 'create'
17
+ },
18
+ });
19
+
20
+ const emit = defineEmits<{
21
+ 'update:value': [value: AuditPolicy];
22
+ }>();
23
+
24
+ // Default Values & Reactive Spec
25
+ const defaults: AuditPolicy = { filters: [] };
26
+ const spec = ref<AuditPolicy>({ ...defaults, ...props.value });
27
+ const defaultAddValue: FilterRule = {
28
+ action: 'allow',
29
+ requestURI: '',
30
+ };
31
+
32
+ // Methods
33
+ function addRow(key: 'action' | 'requestURI', filters: FilterRule[]) {
34
+ const valueToEmit = { ...props.value, ...spec.value };
35
+
36
+ valueToEmit.filters = filters;
37
+
38
+ emit('update:value', valueToEmit);
39
+ }
40
+
41
+ function updateRow(key: 'action' | 'requestURI', index: number, value: string) {
42
+ const valueToEmit = { ...props.value, ...spec.value };
43
+
44
+ if (!valueToEmit.filters) {
45
+ valueToEmit.filters = [];
46
+ }
47
+
48
+ // Ensure the filter exists at the given index
49
+ if (!valueToEmit.filters[index]) {
50
+ valueToEmit.filters[index] = { action: '', requestURI: '' };
51
+ }
52
+
53
+ valueToEmit.filters[index][key] = value;
54
+ emit('update:value', valueToEmit);
55
+ }
56
+ </script>
57
+
58
+ <template>
59
+ <div>
60
+ <div class="row mb-40">
61
+ <div class="col span-12">
62
+ <ArrayList
63
+ key="headers"
64
+ v-model:value="spec.filters"
65
+ :value-placeholder="t('auditPolicy.filters.placeholder')"
66
+ :add-label="t('auditPolicy.filters.add')"
67
+ :mode="mode"
68
+ :protip="false"
69
+ :defaultAddValue="defaultAddValue"
70
+ :show-header="true"
71
+ @update:value="addRow('action', $event)"
72
+ >
73
+ <template v-slot:column-headers>
74
+ <div class="filters-heading mb-10">
75
+ <div
76
+ class="row"
77
+ >
78
+ <div class="col span-6">
79
+ <span class="text-label">{{ t('auditPolicy.filters.action.title') }}</span>
80
+ </div>
81
+ <div class="col span-6 send-to">
82
+ <span class="text-label">{{ t('auditPolicy.filters.requestURI.title') }}</span>
83
+ </div>
84
+ </div>
85
+ </div>
86
+ </template>
87
+ <template v-slot:columns="scope">
88
+ <div class="row">
89
+ <div class="col span-6">
90
+ <LabeledSelect
91
+ v-model:value="scope.row.value.action"
92
+ :options="[{ value: 'allow', label: t('auditPolicy.filters.action.allow')},{ value: 'deny', label: t('auditPolicy.filters.action.deny') }]"
93
+ :mode="mode"
94
+ :placeholder="t('auditPolicy.filters.action.placeholder')"
95
+ @update:value="updateRow('action', scope.i, $event)"
96
+ />
97
+ </div>
98
+ <div class="col span-6">
99
+ <LabeledInput
100
+ v-model:value="scope.row.value.requestURI"
101
+ :mode="mode"
102
+ :placeholder="t('auditPolicy.filters.requestURI.placeholder')"
103
+ @update:value="updateRow('requestURI', scope.i, $event, )"
104
+ />
105
+ </div>
106
+ </div>
107
+ </template>
108
+ </ArrayList>
109
+ </div>
110
+ </div>
111
+ </div>
112
+ </template>
113
+
114
+ <style lang="scss" scoped>
115
+ .filters-heading {
116
+ display: grid;
117
+ grid-template-columns: auto $array-list-remove-margin;
118
+ }
119
+ </style>