@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.
- 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 +22 -4
- package/assets/translations/zh-hans.yaml +2 -3
- package/components/AppModal.vue +50 -0
- package/components/Carousel.vue +54 -47
- package/components/CopyToClipboardText.vue +3 -0
- package/components/Dialog.vue +20 -1
- package/components/PromptChangePassword.vue +3 -0
- package/components/ResourceDetail/Masthead.vue +1 -1
- package/components/Tabbed/index.vue +4 -7
- package/components/__tests__/Carousel.test.ts +56 -27
- package/components/form/LabeledSelect.vue +1 -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 +1 -1
- package/components/form/SelectOrCreateAuthSecret.vue +43 -11
- package/components/form/__tests__/SSHKnownHosts.test.ts +59 -0
- package/composables/focusTrap.ts +68 -0
- package/detail/secret.vue +25 -0
- package/edit/fleet.cattle.io.gitrepo.vue +27 -22
- package/edit/provisioning.cattle.io.cluster/index.vue +26 -19
- package/edit/secret/index.vue +1 -1
- package/edit/secret/ssh.vue +21 -3
- package/list/provisioning.cattle.io.cluster.vue +1 -0
- 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/explorer/ConfigBadge.vue +5 -4
- 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 +50 -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/RcDropdownTrigger.vue +2 -3
- package/rancher-components/RcDropdown/useDropdownCollection.ts +1 -0
- 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>
|
|
@@ -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
|
-
|
|
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:
|
|
381
|
-
publicKey:
|
|
382
|
-
privateKey:
|
|
383
|
-
|
|
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
|
-
|
|
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
|
|