@rancher/shell 3.0.2-rc.3 → 3.0.2-rc.4

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 (49) hide show
  1. package/assets/styles/base/_basic.scss +2 -1
  2. package/assets/styles/global/_form.scss +2 -1
  3. package/assets/styles/themes/_dark.scss +1 -1
  4. package/assets/translations/en-us.yaml +22 -4
  5. package/assets/translations/zh-hans.yaml +2 -3
  6. package/components/AppModal.vue +50 -0
  7. package/components/Carousel.vue +54 -47
  8. package/components/CopyToClipboardText.vue +3 -0
  9. package/components/Dialog.vue +20 -1
  10. package/components/PromptChangePassword.vue +3 -0
  11. package/components/ResourceDetail/Masthead.vue +1 -1
  12. package/components/Tabbed/index.vue +4 -7
  13. package/components/__tests__/Carousel.test.ts +56 -27
  14. package/components/form/LabeledSelect.vue +1 -1
  15. package/components/form/SSHKnownHosts/KnownHostsEditDialog.vue +192 -0
  16. package/components/form/SSHKnownHosts/__tests__/KnownHostsEditDialog.test.ts +104 -0
  17. package/components/form/SSHKnownHosts/index.vue +101 -0
  18. package/components/form/Select.vue +1 -1
  19. package/components/form/SelectOrCreateAuthSecret.vue +43 -11
  20. package/components/form/__tests__/SSHKnownHosts.test.ts +59 -0
  21. package/composables/focusTrap.ts +68 -0
  22. package/detail/secret.vue +25 -0
  23. package/edit/fleet.cattle.io.gitrepo.vue +27 -22
  24. package/edit/provisioning.cattle.io.cluster/index.vue +26 -19
  25. package/edit/secret/index.vue +1 -1
  26. package/edit/secret/ssh.vue +21 -3
  27. package/list/provisioning.cattle.io.cluster.vue +1 -0
  28. package/models/fleet.cattle.io.gitrepo.js +2 -2
  29. package/models/provisioning.cattle.io.cluster.js +2 -12
  30. package/models/secret.js +5 -0
  31. package/package.json +1 -1
  32. package/pages/account/index.vue +4 -0
  33. package/pages/c/_cluster/explorer/ConfigBadge.vue +5 -4
  34. package/pages/c/_cluster/uiplugins/AddExtensionRepos.vue +3 -1
  35. package/pages/c/_cluster/uiplugins/CatalogList/CatalogLoadDialog.vue +3 -0
  36. package/pages/c/_cluster/uiplugins/CatalogList/CatalogUninstallDialog.vue +7 -1
  37. package/pages/c/_cluster/uiplugins/CatalogList/index.vue +3 -1
  38. package/pages/c/_cluster/uiplugins/DeveloperInstallDialog.vue +10 -7
  39. package/pages/c/_cluster/uiplugins/InstallDialog.vue +7 -0
  40. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +181 -106
  41. package/pages/c/_cluster/uiplugins/SetupUIPlugins.vue +2 -0
  42. package/pages/c/_cluster/uiplugins/UninstallDialog.vue +9 -1
  43. package/pages/c/_cluster/uiplugins/index.vue +50 -12
  44. package/rancher-components/Card/Card.vue +7 -21
  45. package/rancher-components/Form/LabeledInput/LabeledInput.vue +1 -0
  46. package/rancher-components/RcDropdown/RcDropdown.vue +11 -0
  47. package/rancher-components/RcDropdown/RcDropdownTrigger.vue +2 -3
  48. package/rancher-components/RcDropdown/useDropdownCollection.ts +1 -0
  49. package/rancher-components/RcDropdown/useDropdownContext.ts +28 -1
@@ -0,0 +1,192 @@
1
+ <script>
2
+ import { _EDIT, _VIEW } from '@shell/config/query-params';
3
+ import CodeMirror from '@shell/components/CodeMirror';
4
+ import FileSelector from '@shell/components/form/FileSelector.vue';
5
+ import AppModal from '@shell/components/AppModal.vue';
6
+
7
+ export default {
8
+ emits: ['closed'],
9
+
10
+ components: {
11
+ FileSelector,
12
+ AppModal,
13
+ CodeMirror,
14
+ },
15
+
16
+ props: {
17
+ value: {
18
+ type: String,
19
+ required: true
20
+ },
21
+
22
+ mode: {
23
+ type: String,
24
+ default: _EDIT
25
+ },
26
+ },
27
+
28
+ data() {
29
+ const codeMirrorOptions = {
30
+ readOnly: this.isView,
31
+ gutters: ['CodeMirror-foldgutter'],
32
+ mode: 'text/x-properties',
33
+ lint: false,
34
+ lineNumbers: !this.isView,
35
+ styleActiveLine: false,
36
+ tabSize: 2,
37
+ indentWithTabs: false,
38
+ cursorBlinkRate: 530,
39
+ };
40
+
41
+ return {
42
+ codeMirrorOptions,
43
+ text: this.value,
44
+ showModal: false,
45
+ };
46
+ },
47
+
48
+ computed: {
49
+ isView() {
50
+ return this.mode === _VIEW;
51
+ }
52
+ },
53
+
54
+ methods: {
55
+ onTextChange(value) {
56
+ this.text = value?.trim();
57
+ },
58
+
59
+ showDialog() {
60
+ this.showModal = true;
61
+ },
62
+
63
+ closeDialog(result) {
64
+ if (!result) {
65
+ this.text = this.value;
66
+ }
67
+
68
+ this.showModal = false;
69
+
70
+ this.$emit('closed', {
71
+ success: result,
72
+ value: this.text,
73
+ });
74
+ },
75
+ }
76
+ };
77
+ </script>
78
+
79
+ <template>
80
+ <app-modal
81
+ v-if="showModal"
82
+ ref="sshKnownHostsDialog"
83
+ height="auto"
84
+ :scrollable="true"
85
+ @close="closeDialog(false)"
86
+ >
87
+ <div
88
+ class="ssh-known-hosts-dialog"
89
+ >
90
+ <h4 class="mt-10">
91
+ {{ t('secret.ssh.editKnownHosts.title') }}
92
+ </h4>
93
+ <div class="custom mt-10">
94
+ <div class="dialog-panel">
95
+ <CodeMirror
96
+ class="code-mirror"
97
+ :value="text"
98
+ data-testid="ssh-known-hosts-dialog_code-mirror"
99
+ :options="codeMirrorOptions"
100
+ :showKeyMapBox="true"
101
+ @onInput="onTextChange"
102
+ />
103
+ </div>
104
+ <div class="dialog-actions">
105
+ <div class="action-pannel file-selector">
106
+ <FileSelector
107
+ class="btn role-secondary"
108
+ data-testid="ssh-known-hosts-dialog_file-selector"
109
+ :label="t('generic.readFromFile')"
110
+ @selected="onTextChange"
111
+ />
112
+ </div>
113
+ <div class="action-pannel form-actions">
114
+ <button
115
+ class="btn role-secondary"
116
+ data-testid="ssh-known-hosts-dialog_cancel-btn"
117
+ @click="closeDialog(false)"
118
+ >
119
+ {{ t('generic.cancel') }}
120
+ </button>
121
+ <button
122
+ class="btn role-primary"
123
+ data-testid="ssh-known-hosts-dialog_save-btn"
124
+ @click="closeDialog(true)"
125
+ >
126
+ {{ t('generic.save') }}
127
+ </button>
128
+ </div>
129
+ </div>
130
+ </div>
131
+ </div>
132
+ </app-modal>
133
+ </template>
134
+
135
+ <style lang="scss" scoped>
136
+ .ssh-known-hosts-dialog {
137
+ padding: 15px;
138
+
139
+ h4 {
140
+ font-weight: bold;
141
+ margin-bottom: 20px;
142
+ }
143
+
144
+ .dialog-panel {
145
+ display: flex;
146
+ flex-direction: column;
147
+ min-height: 100px;
148
+ border: 1px solid var(--border);
149
+
150
+ :deep() .code-mirror {
151
+ display: flex;
152
+ flex-direction: column;
153
+ resize: none;
154
+ max-height: 400px;
155
+ height: 50vh;
156
+
157
+ .CodeMirror,
158
+ .CodeMirror-gutters {
159
+ min-height: 400px;
160
+ max-height: 400px;
161
+ background-color: var(--yaml-editor-bg);
162
+ }
163
+
164
+ .CodeMirror-gutters {
165
+ width: 25px;
166
+ }
167
+
168
+ .CodeMirror-linenumber {
169
+ padding-left: 0;
170
+ }
171
+ }
172
+ }
173
+
174
+ .dialog-actions {
175
+ display: flex;
176
+ justify-content: space-between;
177
+
178
+ .action-pannel {
179
+ margin-top: 10px;
180
+ }
181
+
182
+ .form-actions {
183
+ display: flex;
184
+ justify-content: flex-end;
185
+
186
+ > *:not(:last-child) {
187
+ margin-right: 10px;
188
+ }
189
+ }
190
+ }
191
+ }
192
+ </style>
@@ -0,0 +1,104 @@
1
+ import { mount, VueWrapper } from '@vue/test-utils';
2
+ import { _EDIT } from '@shell/config/query-params';
3
+ import KnownHostsEditDialog from '@shell/components/form/SSHKnownHosts/KnownHostsEditDialog.vue';
4
+ import CodeMirror from '@shell/components/CodeMirror.vue';
5
+ import FileSelector from '@shell/components/form/FileSelector.vue';
6
+
7
+ let wrapper: VueWrapper<InstanceType<typeof KnownHostsEditDialog>>;
8
+
9
+ const mockedStore = () => {
10
+ return { getters: { 'prefs/get': () => jest.fn() } };
11
+ };
12
+
13
+ const requiredSetup = () => {
14
+ return { global: { mocks: { $store: mockedStore() } } };
15
+ };
16
+
17
+ describe('component: KnownHostsEditDialog', () => {
18
+ beforeEach(() => {
19
+ document.body.innerHTML = '<div id="modals"></div>';
20
+ wrapper = mount(KnownHostsEditDialog, {
21
+ attachTo: document.body,
22
+ props: {
23
+ mode: _EDIT,
24
+ value: 'line1\nline2\n',
25
+ },
26
+ ...requiredSetup(),
27
+ });
28
+ });
29
+
30
+ afterEach(() => {
31
+ wrapper.unmount();
32
+ document.body.innerHTML = '';
33
+ });
34
+
35
+ it('should update text from CodeMirror', async() => {
36
+ await wrapper.setData({ showModal: true });
37
+
38
+ expect(wrapper.vm.text).toBe('line1\nline2\n');
39
+
40
+ const codeMirror = wrapper.getComponent(CodeMirror);
41
+
42
+ expect(codeMirror.element).toBeDefined();
43
+
44
+ await codeMirror.setData({ loaded: true });
45
+
46
+ // Emit CodeMirror value
47
+ codeMirror.vm.$emit('onInput', 'bar');
48
+ await codeMirror.vm.$nextTick();
49
+
50
+ expect(wrapper.vm.text).toBe('bar');
51
+ });
52
+
53
+ it('should update text from FileSelector', async() => {
54
+ await wrapper.setData({ showModal: true });
55
+
56
+ expect(wrapper.vm.text).toBe('line1\nline2\n');
57
+
58
+ const fileSelector = wrapper.getComponent(FileSelector);
59
+
60
+ expect(fileSelector.element).toBeDefined();
61
+
62
+ // Emit Fileselector value
63
+ fileSelector.vm.$emit('selected', 'foo');
64
+ await fileSelector.vm.$nextTick();
65
+
66
+ expect(wrapper.vm.text).toBe('foo');
67
+ });
68
+
69
+ it('should save changes and close dialog', async() => {
70
+ await wrapper.setData({
71
+ showModal: true,
72
+ text: 'foo',
73
+ });
74
+
75
+ expect(wrapper.vm.value).toBe('line1\nline2\n');
76
+ expect(wrapper.vm.text).toBe('foo');
77
+
78
+ await wrapper.vm.closeDialog(true);
79
+
80
+ expect((wrapper.emitted('closed') as any)[0][0].value).toBe('foo');
81
+
82
+ const dialog = wrapper.vm.$refs['sshKnownHostsDialog'];
83
+
84
+ expect(dialog).toBeNull();
85
+ });
86
+
87
+ it('should discard changes and close dialog', async() => {
88
+ await wrapper.setData({
89
+ showModal: true,
90
+ text: 'foo',
91
+ });
92
+
93
+ expect(wrapper.vm.value).toBe('line1\nline2\n');
94
+ expect(wrapper.vm.text).toBe('foo');
95
+
96
+ await wrapper.vm.closeDialog(false);
97
+
98
+ expect((wrapper.emitted('closed') as any)[0][0].value).toBe('line1\nline2\n');
99
+
100
+ const dialog = wrapper.vm.$refs['sshKnownHostsDialog'];
101
+
102
+ expect(dialog).toBeNull();
103
+ });
104
+ });
@@ -0,0 +1,101 @@
1
+ <script lang="ts">
2
+ import { defineComponent } from 'vue';
3
+ import KnownHostsEditDialog from './KnownHostsEditDialog.vue';
4
+ import { _EDIT, _VIEW } from '@shell/config/query-params';
5
+
6
+ export default defineComponent({
7
+ name: 'SSHKnownHosts',
8
+
9
+ emits: ['update:value'],
10
+
11
+ props: {
12
+ value: {
13
+ type: String,
14
+ required: true
15
+ },
16
+
17
+ mode: {
18
+ type: String,
19
+ default: _EDIT
20
+ },
21
+ },
22
+
23
+ components: { KnownHostsEditDialog },
24
+
25
+ computed: {
26
+ isViewMode() {
27
+ return this.mode === _VIEW;
28
+ },
29
+
30
+ // The number of entries - exclude empty lines and comments
31
+ entries() {
32
+ return this.value.split('\n').filter((line: string) => !!line.trim().length && !line.startsWith('#')).length;
33
+ },
34
+
35
+ summary() {
36
+ return this.t('secret.ssh.editKnownHosts.entries', { entries: this.entries });
37
+ }
38
+ },
39
+
40
+ methods: {
41
+ openDialog() {
42
+ (this.$refs.button as HTMLInputElement)?.blur();
43
+ (this.$refs.editDialog as any).showDialog();
44
+ },
45
+
46
+ dialogClosed(result: any) {
47
+ if (result.success) {
48
+ this.$emit('update:value', result.value);
49
+ }
50
+ }
51
+ }
52
+ });
53
+ </script>
54
+ <template>
55
+ <div
56
+ class="input-known-ssh-hosts labeled-input"
57
+ data-testid="input-known-ssh-hosts"
58
+ >
59
+ <label>{{ t('secret.ssh.knownHosts') }}</label>
60
+ <div
61
+ class="hosts-input"
62
+ data-testid="input-known-ssh-hosts_summary"
63
+ >
64
+ {{ summary }}
65
+ </div>
66
+ <template v-if="!isViewMode">
67
+ <button
68
+ ref="button"
69
+ data-testid="input-known-ssh-hosts_open-dialog"
70
+ class="show-dialog-btn btn"
71
+ @click="openDialog"
72
+ >
73
+ <i class="icon icon-edit" />
74
+ </button>
75
+
76
+ <KnownHostsEditDialog
77
+ ref="editDialog"
78
+ :value="value"
79
+ :mode="mode"
80
+ @closed="dialogClosed"
81
+ />
82
+ </template>
83
+ </div>
84
+ </template>
85
+ <style lang="scss" scoped>
86
+ .input-known-ssh-hosts {
87
+ display: flex;
88
+ justify-content: space-between;
89
+
90
+ .hosts-input {
91
+ cursor: default;
92
+ line-height: calc(18px + 1px);
93
+ padding: 18px 0 0 0;
94
+ }
95
+
96
+ .show-dialog-btn {
97
+ display: contents;
98
+ background-color: transparent;
99
+ }
100
+ }
101
+ </style>
@@ -256,7 +256,7 @@ export default {
256
256
  }"
257
257
  :tabindex="disabled || isView ? -1 : 0"
258
258
  @click="focusSearch"
259
- @keyup.enter.space.down="focusSearch"
259
+ @keydown.enter.space.down="focusSearch"
260
260
  >
261
261
  <v-select
262
262
  ref="select-input"
@@ -3,6 +3,7 @@ import { _EDIT } from '@shell/config/query-params';
3
3
  import { Banner } from '@components/Banner';
4
4
  import { LabeledInput } from '@components/Form/LabeledInput';
5
5
  import LabeledSelect from '@shell/components/form/LabeledSelect';
6
+ import SSHKnownHosts from '@shell/components/form/SSHKnownHosts';
6
7
  import { AUTH_TYPE, NORMAN, SECRET } from '@shell/config/types';
7
8
  import { SECRET_TYPES } from '@shell/config/secret';
8
9
  import { base64Encode } from '@shell/utils/crypto';
@@ -23,6 +24,7 @@ export default {
23
24
  Banner,
24
25
  LabeledInput,
25
26
  LabeledSelect,
27
+ SSHKnownHosts,
26
28
  },
27
29
 
28
30
  props: {
@@ -142,6 +144,11 @@ export default {
142
144
  type: Boolean,
143
145
  default: false,
144
146
  },
147
+
148
+ showSshKnownHosts: {
149
+ type: Boolean,
150
+ default: true,
151
+ },
145
152
  },
146
153
 
147
154
  async fetch() {
@@ -172,6 +179,7 @@ export default {
172
179
  if ( !this.value ) {
173
180
  this.publicKey = this.preSelect?.publicKey || '';
174
181
  this.privateKey = this.preSelect?.privateKey || '';
182
+ this.sshKnownHosts = this.preSelect?.sshKnownHosts || '';
175
183
  }
176
184
 
177
185
  this.updateSelectedFromValue();
@@ -189,9 +197,10 @@ export default {
189
197
 
190
198
  filterByNamespace: this.namespace && this.limitToNamespace,
191
199
 
192
- publicKey: '',
193
- privateKey: '',
194
- uniqueId: new Date().getTime(), // Allows form state to be individually tracked if the form is in a list
200
+ publicKey: '',
201
+ privateKey: '',
202
+ sshKnownHosts: '',
203
+ uniqueId: new Date().getTime(), // Allows form state to be individually tracked if the form is in a list
195
204
 
196
205
  SSH: AUTH_TYPE._SSH,
197
206
  BASIC: AUTH_TYPE._BASIC,
@@ -372,15 +381,16 @@ export default {
372
381
  return 'mt-20';
373
382
  }
374
383
 
375
- return 'col span-4';
384
+ return (this.selected === AUTH_TYPE._SSH) && this.showSshKnownHosts ? 'col span-3' : 'col span-4';
376
385
  }
377
386
  },
378
387
 
379
388
  watch: {
380
- selected: 'update',
381
- publicKey: 'updateKeyVal',
382
- privateKey: 'updateKeyVal',
383
- value: 'updateSelectedFromValue',
389
+ selected: 'update',
390
+ publicKey: 'updateKeyVal',
391
+ privateKey: 'updateKeyVal',
392
+ sshKnownHosts: 'updateKeyVal',
393
+ value: 'updateSelectedFromValue',
384
394
 
385
395
  async namespace(ns) {
386
396
  if (ns && !this.selected.startsWith(`${ ns }/`)) {
@@ -463,13 +473,20 @@ export default {
463
473
  if ( ![AUTH_TYPE._SSH, AUTH_TYPE._BASIC, AUTH_TYPE._S3, AUTH_TYPE._RKE].includes(this.selected) ) {
464
474
  this.privateKey = '';
465
475
  this.publicKey = '';
476
+ this.sshKnownHosts = '';
466
477
  }
467
478
 
468
- this.$emit('inputauthval', {
479
+ const value = {
469
480
  selected: this.selected,
470
481
  privateKey: this.privateKey,
471
- publicKey: this.publicKey
472
- });
482
+ publicKey: this.publicKey,
483
+ };
484
+
485
+ if (this.sshKnownHosts) {
486
+ value.sshKnownHosts = this.sshKnownHosts;
487
+ }
488
+
489
+ this.$emit('inputauthval', value);
473
490
  },
474
491
 
475
492
  update() {
@@ -550,6 +567,12 @@ export default {
550
567
  [publicField]: base64Encode(this.publicKey),
551
568
  [privateField]: base64Encode(this.privateKey),
552
569
  };
570
+
571
+ // Add ssh known hosts data key - we will add a key with an empty value if the inout field was left blank
572
+ // This ensures on edit of the secret, we allow the user to edit the known_hosts field
573
+ if ((this.selected === AUTH_TYPE._SSH) && this.showSshKnownHosts) {
574
+ secret.data.known_hosts = base64Encode(this.sshKnownHosts || '');
575
+ }
553
576
  }
554
577
  }
555
578
 
@@ -603,6 +626,15 @@ export default {
603
626
  label-key="selectOrCreateAuthSecret.ssh.privateKey"
604
627
  />
605
628
  </div>
629
+ <div
630
+ v-if="showSshKnownHosts"
631
+ class="col span-2"
632
+ >
633
+ <SSHKnownHosts
634
+ v-model:value="sshKnownHosts"
635
+ :mode="mode"
636
+ />
637
+ </div>
606
638
  </template>
607
639
  <template v-else-if="selected === BASIC || selected === RKE">
608
640
  <Banner
@@ -0,0 +1,59 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import { _EDIT, _VIEW } from '@shell/config/query-params';
3
+ import SSHKnownHosts from '@shell/components/form/SSHKnownHosts/index.vue';
4
+
5
+ describe('component: SSHKnownHosts', () => {
6
+ it.each([
7
+ ['0 entities', '', 0],
8
+ ['0 entities (multiple empty lines)', '\n \n \n', 0],
9
+ ['1 entity', 'line1\n', 1],
10
+ ['1 entity (multiple empty lines)', 'line1\n\n\n', 1],
11
+ ['2 entities', 'line1\nline2\n', 2],
12
+ ['2 entities (multiple empty lines)', 'line1\n \n line2\n \n', 2],
13
+ ])('mode view: summary should be: %p', (_, value, entities) => {
14
+ const wrapper = mount(SSHKnownHosts, {
15
+ props: {
16
+ mode: _VIEW,
17
+ value,
18
+ }
19
+ });
20
+
21
+ const knownSshHostsSummary = wrapper.find('[data-testid="input-known-ssh-hosts_summary"]');
22
+ const knownSshHostsOpenDialog = wrapper.findAll('[data-testid="input-known-ssh-hosts_open-dialog"]');
23
+
24
+ expect(wrapper.vm.entries).toBe(entities);
25
+ expect(knownSshHostsSummary.element).toBeDefined();
26
+ expect(knownSshHostsOpenDialog).toHaveLength(0);
27
+ });
28
+
29
+ it('mode edit: should display summary and edit button', () => {
30
+ const wrapper = mount(SSHKnownHosts, {
31
+ props: {
32
+ mode: _EDIT,
33
+ value: 'line1\nline2\n',
34
+ }
35
+ });
36
+
37
+ const knownSshHostsSummary = wrapper.find('[data-testid="input-known-ssh-hosts_summary"]');
38
+ const knownSshHostsOpenDialog = wrapper.find('[data-testid="input-known-ssh-hosts_open-dialog"]');
39
+
40
+ expect(knownSshHostsSummary.element).toBeDefined();
41
+ expect(knownSshHostsOpenDialog.element).toBeDefined();
42
+ });
43
+
44
+ it('mode edit: should open edit dialog', async() => {
45
+ const wrapper = mount(SSHKnownHosts, {
46
+ props: {
47
+ mode: _EDIT,
48
+ value: '',
49
+ }
50
+ });
51
+
52
+ const knownSshHostsOpenDialog = wrapper.find('[data-testid="input-known-ssh-hosts_open-dialog"]');
53
+ const editDialog = wrapper.vm.$refs['editDialog'] as any;
54
+
55
+ await knownSshHostsOpenDialog.trigger('click');
56
+
57
+ expect(editDialog.showModal).toBe(true);
58
+ });
59
+ });
@@ -0,0 +1,68 @@
1
+ /**
2
+ * focusTrap is a composable based on the "focus-trap" package that allows us to implement focus traps
3
+ * on components for keyboard navigation is a safe and reusable way
4
+ */
5
+ import { watch, nextTick, onMounted, onBeforeUnmount } from 'vue';
6
+ import { createFocusTrap, FocusTrap } from 'focus-trap';
7
+
8
+ export function getFirstFocusableElement(element:any = document):any {
9
+ const focusableElements = element.querySelectorAll(
10
+ 'a, button, input, textarea, select, details,[tabindex]:not([tabindex="-1"])'
11
+ );
12
+ const filteredFocusableElements:any = [];
13
+
14
+ focusableElements.forEach((el:any) => {
15
+ if (!el.hasAttribute('disabled')) {
16
+ filteredFocusableElements.push(el);
17
+ }
18
+ });
19
+
20
+ return filteredFocusableElements.length ? filteredFocusableElements[0] : document.body;
21
+ }
22
+
23
+ export const DEFAULT_FOCUS_TRAP_OPTS = {
24
+ escapeDeactivates: true,
25
+ allowOutsideClick: true
26
+ };
27
+
28
+ export function useBasicSetupFocusTrap(focusElement: string | HTMLElement, opts:any = DEFAULT_FOCUS_TRAP_OPTS) {
29
+ let focusTrapInstance: FocusTrap;
30
+ let focusEl;
31
+
32
+ onMounted(() => {
33
+ focusEl = typeof focusElement === 'string' ? document.querySelector(focusElement) as HTMLElement : focusElement;
34
+
35
+ focusTrapInstance = createFocusTrap(focusEl, opts);
36
+
37
+ nextTick(() => {
38
+ focusTrapInstance.activate();
39
+ });
40
+ });
41
+
42
+ onBeforeUnmount(() => {
43
+ if (Object.keys(focusTrapInstance).length) {
44
+ focusTrapInstance.deactivate();
45
+ }
46
+ });
47
+ }
48
+
49
+ export function useWatcherBasedSetupFocusTrapWithDestroyIncluded(watchVar:any, focusElement: string | HTMLElement, opts:any = DEFAULT_FOCUS_TRAP_OPTS) {
50
+ let focusTrapInstance: FocusTrap;
51
+ let focusEl;
52
+
53
+ watch(watchVar, (neu) => {
54
+ if (neu) {
55
+ nextTick(() => {
56
+ focusEl = typeof focusElement === 'string' ? document.querySelector(focusElement) as HTMLElement : focusElement;
57
+
58
+ focusTrapInstance = createFocusTrap(focusEl, opts);
59
+
60
+ nextTick(() => {
61
+ focusTrapInstance.activate();
62
+ });
63
+ });
64
+ } else if (!neu && Object.keys(focusTrapInstance).length) {
65
+ focusTrapInstance.deactivate();
66
+ }
67
+ });
68
+ }
package/detail/secret.vue CHANGED
@@ -122,6 +122,10 @@ export default {
122
122
  return this.value._type === TYPES.SSH;
123
123
  },
124
124
 
125
+ showKnownHosts() {
126
+ return this.isSsh && this.value.supportsSshKnownHosts;
127
+ },
128
+
125
129
  isBasicAuth() {
126
130
  return this.value._type === TYPES.BASIC;
127
131
  },
@@ -142,6 +146,12 @@ export default {
142
146
  return rows;
143
147
  },
144
148
 
149
+ knownHosts() {
150
+ const { data = {} } = this.value;
151
+
152
+ return data.known_hosts ? base64Decode(data.known_hosts) : '';
153
+ },
154
+
145
155
  dataLabel() {
146
156
  switch (this.value._type) {
147
157
  case TYPES.TLS:
@@ -274,6 +284,21 @@ export default {
274
284
  </div>
275
285
  </div>
276
286
  </Tab>
287
+ <Tab
288
+ v-if="showKnownHosts"
289
+ name="known_hosts"
290
+ label-key="secret.ssh.knownHosts"
291
+ >
292
+ <div class="row">
293
+ <div class="col span-12">
294
+ <DetailText
295
+ :value="knownHosts"
296
+ label-key="secret.ssh.knownHosts"
297
+ :conceal="false"
298
+ />
299
+ </div>
300
+ </div>
301
+ </Tab>
277
302
  </ResourceTabs>
278
303
  </template>
279
304