@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.
- package/assets/styles/base/_basic.scss +2 -1
- package/assets/styles/global/_form.scss +2 -1
- package/assets/styles/themes/_dark.scss +1 -1
- package/assets/translations/en-us.yaml +35 -4
- package/assets/translations/zh-hans.yaml +2 -3
- package/components/AppModal.vue +50 -0
- package/components/ButtonGroup.vue +8 -1
- package/components/ButtonMultiAction.vue +5 -1
- package/components/Carousel.vue +54 -47
- package/components/CopyToClipboardText.vue +6 -1
- package/components/Dialog.vue +20 -1
- package/components/ExplorerProjectsNamespaces.vue +7 -0
- package/components/PromptChangePassword.vue +3 -0
- package/components/ResourceDetail/Masthead.vue +1 -1
- package/components/ResourceTable.vue +1 -14
- package/components/SelectIconGrid.vue +2 -0
- package/components/SortableTable/index.vue +32 -3
- package/components/Tabbed/index.vue +4 -7
- package/components/__tests__/Carousel.test.ts +56 -27
- package/components/form/LabeledSelect.vue +2 -1
- package/components/form/SSHKnownHosts/KnownHostsEditDialog.vue +192 -0
- package/components/form/SSHKnownHosts/__tests__/KnownHostsEditDialog.test.ts +104 -0
- package/components/form/SSHKnownHosts/index.vue +101 -0
- package/components/form/Select.vue +2 -1
- package/components/form/SelectOrCreateAuthSecret.vue +43 -11
- package/components/form/__tests__/SSHKnownHosts.test.ts +59 -0
- package/components/nav/WindowManager/ContainerLogs.vue +13 -3
- package/components/nav/WindowManager/index.vue +14 -3
- package/composables/focusTrap.ts +68 -0
- package/config/home-links.js +1 -1
- package/detail/secret.vue +25 -0
- package/edit/fleet.cattle.io.gitrepo.vue +27 -22
- package/edit/provisioning.cattle.io.cluster/index.vue +18 -9
- package/edit/secret/index.vue +1 -1
- package/edit/secret/ssh.vue +21 -3
- package/list/management.cattle.io.setting.vue +5 -2
- package/list/provisioning.cattle.io.cluster.vue +1 -0
- package/mixins/auth-config.js +1 -1
- package/models/fleet.cattle.io.gitrepo.js +2 -2
- package/models/provisioning.cattle.io.cluster.js +2 -12
- package/models/secret.js +5 -0
- package/package.json +1 -1
- package/pages/account/index.vue +4 -0
- package/pages/c/_cluster/apps/charts/chart.vue +1 -0
- package/pages/c/_cluster/explorer/ConfigBadge.vue +5 -4
- package/pages/c/_cluster/explorer/tools/index.vue +14 -1
- package/pages/c/_cluster/uiplugins/AddExtensionRepos.vue +3 -1
- package/pages/c/_cluster/uiplugins/CatalogList/CatalogLoadDialog.vue +3 -0
- package/pages/c/_cluster/uiplugins/CatalogList/CatalogUninstallDialog.vue +7 -1
- package/pages/c/_cluster/uiplugins/CatalogList/index.vue +3 -1
- package/pages/c/_cluster/uiplugins/DeveloperInstallDialog.vue +10 -7
- package/pages/c/_cluster/uiplugins/InstallDialog.vue +7 -0
- package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +181 -106
- package/pages/c/_cluster/uiplugins/SetupUIPlugins.vue +2 -0
- package/pages/c/_cluster/uiplugins/UninstallDialog.vue +9 -1
- package/pages/c/_cluster/uiplugins/index.vue +52 -12
- package/rancher-components/Card/Card.vue +7 -21
- package/rancher-components/Form/LabeledInput/LabeledInput.vue +1 -0
- package/rancher-components/RcDropdown/RcDropdown.vue +11 -0
- package/rancher-components/RcDropdown/RcDropdownItem.vue +0 -12
- package/rancher-components/RcDropdown/RcDropdownTrigger.vue +2 -3
- package/rancher-components/RcDropdown/useDropdownCollection.ts +1 -0
- 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: [
|
|
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 }}
|
|
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
|
-
|
|
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
|
-
@
|
|
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
|
-
@
|
|
259
|
+
@keydown.enter.down="focusSearch"
|
|
260
|
+
@keydown.space.prevent="focusSearch"
|
|
260
261
|
>
|
|
261
262
|
<v-select
|
|
262
263
|
ref="select-input"
|