@rancher/shell 3.0.2-rc.3 → 3.0.2-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 (63) 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 +35 -4
  5. package/assets/translations/zh-hans.yaml +2 -3
  6. package/components/AppModal.vue +50 -0
  7. package/components/ButtonGroup.vue +8 -1
  8. package/components/ButtonMultiAction.vue +5 -1
  9. package/components/Carousel.vue +54 -47
  10. package/components/CopyToClipboardText.vue +6 -1
  11. package/components/Dialog.vue +20 -1
  12. package/components/ExplorerProjectsNamespaces.vue +7 -0
  13. package/components/PromptChangePassword.vue +3 -0
  14. package/components/ResourceDetail/Masthead.vue +1 -1
  15. package/components/ResourceTable.vue +1 -14
  16. package/components/SelectIconGrid.vue +2 -0
  17. package/components/SortableTable/index.vue +32 -3
  18. package/components/Tabbed/index.vue +4 -7
  19. package/components/__tests__/Carousel.test.ts +56 -27
  20. package/components/form/LabeledSelect.vue +2 -1
  21. package/components/form/SSHKnownHosts/KnownHostsEditDialog.vue +192 -0
  22. package/components/form/SSHKnownHosts/__tests__/KnownHostsEditDialog.test.ts +104 -0
  23. package/components/form/SSHKnownHosts/index.vue +101 -0
  24. package/components/form/Select.vue +2 -1
  25. package/components/form/SelectOrCreateAuthSecret.vue +43 -11
  26. package/components/form/__tests__/SSHKnownHosts.test.ts +59 -0
  27. package/components/nav/WindowManager/ContainerLogs.vue +13 -3
  28. package/components/nav/WindowManager/index.vue +14 -3
  29. package/composables/focusTrap.ts +68 -0
  30. package/config/home-links.js +1 -1
  31. package/detail/secret.vue +25 -0
  32. package/edit/fleet.cattle.io.gitrepo.vue +27 -22
  33. package/edit/provisioning.cattle.io.cluster/index.vue +18 -9
  34. package/edit/secret/index.vue +1 -1
  35. package/edit/secret/ssh.vue +21 -3
  36. package/list/management.cattle.io.setting.vue +5 -2
  37. package/list/provisioning.cattle.io.cluster.vue +1 -0
  38. package/mixins/auth-config.js +1 -1
  39. package/models/fleet.cattle.io.gitrepo.js +2 -2
  40. package/models/provisioning.cattle.io.cluster.js +2 -12
  41. package/models/secret.js +5 -0
  42. package/package.json +1 -1
  43. package/pages/account/index.vue +4 -0
  44. package/pages/c/_cluster/apps/charts/chart.vue +1 -0
  45. package/pages/c/_cluster/explorer/ConfigBadge.vue +5 -4
  46. package/pages/c/_cluster/explorer/tools/index.vue +14 -1
  47. package/pages/c/_cluster/uiplugins/AddExtensionRepos.vue +3 -1
  48. package/pages/c/_cluster/uiplugins/CatalogList/CatalogLoadDialog.vue +3 -0
  49. package/pages/c/_cluster/uiplugins/CatalogList/CatalogUninstallDialog.vue +7 -1
  50. package/pages/c/_cluster/uiplugins/CatalogList/index.vue +3 -1
  51. package/pages/c/_cluster/uiplugins/DeveloperInstallDialog.vue +10 -7
  52. package/pages/c/_cluster/uiplugins/InstallDialog.vue +7 -0
  53. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +181 -106
  54. package/pages/c/_cluster/uiplugins/SetupUIPlugins.vue +2 -0
  55. package/pages/c/_cluster/uiplugins/UninstallDialog.vue +9 -1
  56. package/pages/c/_cluster/uiplugins/index.vue +52 -12
  57. package/rancher-components/Card/Card.vue +7 -21
  58. package/rancher-components/Form/LabeledInput/LabeledInput.vue +1 -0
  59. package/rancher-components/RcDropdown/RcDropdown.vue +11 -0
  60. package/rancher-components/RcDropdown/RcDropdownItem.vue +0 -12
  61. package/rancher-components/RcDropdown/RcDropdownTrigger.vue +2 -3
  62. package/rancher-components/RcDropdown/useDropdownCollection.ts +1 -0
  63. package/rancher-components/RcDropdown/useDropdownContext.ts +28 -1
@@ -190,20 +190,6 @@ export default {
190
190
  },
191
191
  },
192
192
 
193
- mounted() {
194
- /**
195
- * v-shortkey prevents the event's propagation:
196
- * https://github.com/fgr-araujo/vue-shortkey/blob/55d802ea305cadcc2ea970b55a3b8b86c7b44c05/src/index.js#L156-L157
197
- *
198
- * 'Enter' key press is handled via event listener in order to allow the event propagation
199
- */
200
- window.addEventListener('keyup', this.handleEnterKeyPress);
201
- },
202
-
203
- beforeUnmount() {
204
- window.removeEventListener('keyup', this.handleEnterKeyPress);
205
- },
206
-
207
193
  data() {
208
194
  // Confirm which store we're in, if schema isn't available we're probably showing a list with different types
209
195
  const inStore = this.schema?.id ? this.$store.getters['currentStore'](this.schema.id) : undefined;
@@ -602,6 +588,7 @@ export default {
602
588
  :mandatory-sort="_mandatorySort"
603
589
  @clickedActionButton="handleActionButtonClick"
604
590
  @group-value-change="group = $event"
591
+ @enter="handleEnterKeyPress"
605
592
  >
606
593
  <template
607
594
  v-if="showGrouping"
@@ -148,10 +148,12 @@ export default {
148
148
  <i
149
149
  v-if="r.iconClass"
150
150
  :class="r.iconClass"
151
+ :alt="t('catalog.charts.iconAlt', { app: get(r, nameField) })"
151
152
  />
152
153
  <LazyImage
153
154
  v-else
154
155
  :src="get(r, iconField)"
156
+ :alt="t('catalog.charts.iconAlt', { app: get(r, nameField) })"
155
157
  />
156
158
  </div>
157
159
  <h4 class="name">
@@ -1,6 +1,6 @@
1
1
  <script>
2
2
  import { mapGetters } from 'vuex';
3
- import { defineAsyncComponent } from 'vue';
3
+ import { defineAsyncComponent, useTemplateRef, onMounted, onBeforeUnmount } from 'vue';
4
4
  import day from 'dayjs';
5
5
  import isEmpty from 'lodash/isEmpty';
6
6
  import { dasherize, ucFirst } from '@shell/utils/string';
@@ -42,7 +42,14 @@ import ButtonMultiAction from '@shell/components/ButtonMultiAction.vue';
42
42
  export default {
43
43
  name: 'SortableTable',
44
44
 
45
- emits: ['clickedActionButton', 'pagination-changed', 'group-value-change', 'selection', 'rowClick'],
45
+ emits: [
46
+ 'clickedActionButton',
47
+ 'pagination-changed',
48
+ 'group-value-change',
49
+ 'selection',
50
+ 'rowClick',
51
+ 'enter',
52
+ ],
46
53
 
47
54
  components: {
48
55
  THead,
@@ -518,6 +525,23 @@ export default {
518
525
  immediate: true
519
526
  },
520
527
  },
528
+ setup(_props, { emit }) {
529
+ const table = useTemplateRef('table');
530
+
531
+ const handleEnterKey = (event) => {
532
+ if (event.key === 'Enter' && !event.target?.classList?.contains('checkbox-custom')) {
533
+ emit('enter', event);
534
+ }
535
+ };
536
+
537
+ onMounted(() => {
538
+ table.value.addEventListener('keyup', handleEnterKey);
539
+ });
540
+
541
+ onBeforeUnmount(() => {
542
+ table.value.removeEventListener('keyup', handleEnterKey);
543
+ });
544
+ },
521
545
 
522
546
  created() {
523
547
  this.debouncedRefreshTableData = debounce(this.refreshTableData, 500);
@@ -1056,6 +1080,7 @@ export default {
1056
1080
  :disabled="!act.enabled"
1057
1081
  :data-testid="componentTestid + '-' + act.action"
1058
1082
  @click="applyTableAction(act, null, $event)"
1083
+ @keydown.enter.stop
1059
1084
  @mouseover="setBulkActionOfInterest(act)"
1060
1085
  @mouseleave="setBulkActionOfInterest(null)"
1061
1086
  >
@@ -1221,6 +1246,7 @@ export default {
1221
1246
  </div>
1222
1247
  </div>
1223
1248
  <table
1249
+ ref="table"
1224
1250
  class="sortable-table"
1225
1251
  :class="classObject"
1226
1252
  width="100%"
@@ -1299,6 +1325,7 @@ export default {
1299
1325
  v-for="(groupedRows) in displayRows"
1300
1326
  v-else
1301
1327
  :key="groupedRows.key"
1328
+ tabindex="-1"
1302
1329
  :class="{ group: groupBy }"
1303
1330
  >
1304
1331
  <slot
@@ -1350,7 +1377,8 @@ export default {
1350
1377
  class="row-check"
1351
1378
  align="middle"
1352
1379
  >
1353
- {{ row.mainRowKey }}<Checkbox
1380
+ {{ row.mainRowKey }}
1381
+ <Checkbox
1354
1382
  class="selection-checkbox"
1355
1383
  :data-node-id="row.key"
1356
1384
  :data-testid="componentTestid + '-' + i + '-checkbox'"
@@ -1451,6 +1479,7 @@ export default {
1451
1479
  :ref="`actionButton${i}`"
1452
1480
  aria-haspopup="true"
1453
1481
  aria-expanded="false"
1482
+ :aria-label="t('sortableTable.tableActionsLabel', { resource: row?.row?.id || '' })"
1454
1483
  :data-testid="componentTestid + '-' + i + '-action-button'"
1455
1484
  :borderless="true"
1456
1485
  @click="handleActionButtonClick(i, $event)"
@@ -276,12 +276,11 @@ export default {
276
276
  :data-testid="`btn-${tab.name}`"
277
277
  :aria-controls="'#' + tab.name"
278
278
  :aria-selected="tab.active"
279
- :aria-label="tab.labelDisplay"
279
+ :aria-label="tab.labelDisplay || ''"
280
280
  role="tab"
281
281
  tabindex="0"
282
282
  @click.prevent="select(tab.name, $event)"
283
- @keyup.enter="select(tab.name, $event)"
284
- @keyup.space="select(tab.name, $event)"
283
+ @keyup.enter.space="select(tab.name, $event)"
285
284
  >
286
285
  <span>{{ tab.labelDisplay }}</span>
287
286
  <span
@@ -409,10 +408,8 @@ export default {
409
408
 
410
409
  &:focus-visible {
411
410
  @include focus-outline;
412
-
413
- span {
414
- text-decoration: underline;
415
- }
411
+ outline-offset: -4px;
412
+ text-decoration: none;
416
413
  }
417
414
  }
418
415
 
@@ -1,35 +1,41 @@
1
1
  import { mount } from '@vue/test-utils';
2
2
  import Carousel from '@shell/components/Carousel.vue';
3
3
 
4
+ const sliders = [
5
+ {
6
+ key: 'key-0',
7
+ repoName: 'some-repo-name-0',
8
+ chartNameDisplay: 'chart-name-display-0',
9
+ chartDescription: 'chart-description-0'
10
+ },
11
+ {
12
+ key: 'key-1',
13
+ repoName: 'some-repo-name-1',
14
+ chartNameDisplay: 'chart-name-display-1',
15
+ chartDescription: 'chart-description-1'
16
+ },
17
+ {
18
+ key: 'key-2',
19
+ repoName: 'some-repo-name-2',
20
+ chartNameDisplay: 'chart-name-display-2',
21
+ chartDescription: 'chart-description-2'
22
+ },
23
+ {
24
+ key: 'key-3',
25
+ repoName: 'some-repo-name-3',
26
+ chartNameDisplay: 'chart-name-display-3',
27
+ chartDescription: 'chart-description-3'
28
+ },
29
+ {
30
+ key: 'key-4',
31
+ repoName: 'some-repo-name-4',
32
+ chartNameDisplay: 'chart-name-display-4',
33
+ chartDescription: 'chart-description-4'
34
+ }
35
+ ];
36
+
4
37
  describe('component: Carousel', () => {
5
38
  it('should render component with the correct data applied', async() => {
6
- const sliders = [
7
- {
8
- key: 'key-0',
9
- repoName: 'some-repo-name-0',
10
- chartNameDisplay: 'chart-name-display-0',
11
- chartDescription: 'chart-description-0'
12
- },
13
- {
14
- key: 'key-1',
15
- repoName: 'some-repo-name-1',
16
- chartNameDisplay: 'chart-name-display-1',
17
- chartDescription: 'chart-description-1'
18
- },
19
- {
20
- key: 'key-2',
21
- repoName: 'some-repo-name-2',
22
- chartNameDisplay: 'chart-name-display-2',
23
- chartDescription: 'chart-description-2'
24
- },
25
- {
26
- key: 'key-3',
27
- repoName: 'some-repo-name-3',
28
- chartNameDisplay: 'chart-name-display-3',
29
- chartDescription: 'chart-description-3'
30
- }
31
- ];
32
-
33
39
  const wrapper = mount(Carousel, {
34
40
  props: { sliders },
35
41
  global: { mocks: { $store: { getters: { clusterId: () => 'some-cluster-id' } } } }
@@ -40,4 +46,27 @@ describe('component: Carousel', () => {
40
46
  expect(wrapper.find(`#slide${ index } h1`).text()).toContain(slider.chartNameDisplay);
41
47
  });
42
48
  });
49
+
50
+ it.each([
51
+ [sliders.slice(0, 2)],
52
+ [sliders.slice(0, 3)],
53
+ [sliders.slice(0, 4)],
54
+ [sliders.slice(0, 5)]
55
+ ])('should have the correct width and left position', async(sliders) => {
56
+ const wrapper = mount(Carousel, {
57
+ props: { sliders },
58
+ global: { mocks: { $store: { getters: { clusterId: () => 'some-cluster-id' } } } }
59
+ });
60
+
61
+ const width = 60 * (wrapper.vm.slider.length + 2);
62
+ const initialLeft = -(40 + wrapper.vm.activeItemId * 60);
63
+
64
+ expect(wrapper.vm.trackStyle).toContain(`width: ${ width }%`);
65
+ expect(wrapper.vm.trackStyle).toContain(`left: ${ initialLeft }`);
66
+
67
+ wrapper.vm.activeItemId = wrapper.vm.activeItemId + 1; // next slide
68
+ expect(wrapper.vm.trackStyle).toContain(`left: ${ -(40 + wrapper.vm.activeItemId * 60) }`);
69
+ wrapper.vm.activeItemId = wrapper.vm.activeItemId - 1; // previous slide
70
+ expect(wrapper.vm.trackStyle).toContain(`left: ${ initialLeft }`);
71
+ });
43
72
  });
@@ -291,7 +291,8 @@ export default {
291
291
  ]"
292
292
  :tabindex="isView || disabled ? -1 : 0"
293
293
  @click="focusSearch"
294
- @keyup.enter.space.down="focusSearch"
294
+ @keydown.enter.down="focusSearch"
295
+ @keydown.space.prevent="focusSearch"
295
296
  >
296
297
  <div
297
298
  :class="{ 'labeled-container': true, raised, empty, [mode]: true }"
@@ -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,8 @@ export default {
256
256
  }"
257
257
  :tabindex="disabled || isView ? -1 : 0"
258
258
  @click="focusSearch"
259
- @keyup.enter.space.down="focusSearch"
259
+ @keydown.enter.down="focusSearch"
260
+ @keydown.space.prevent="focusSearch"
260
261
  >
261
262
  <v-select
262
263
  ref="select-input"