fix(editor): Add ssh key type selection to source control settings when regenerating key (#7172)

This commit is contained in:
Csaba Tuncsik 2023-09-14 14:40:34 +02:00 committed by GitHub
parent fdac2c8572
commit 54bf66d335
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 102 additions and 32 deletions

View file

@ -1,4 +1,10 @@
import type { CREDENTIAL_EDIT_MODAL_KEY } from './constants'; import type {
CREDENTIAL_EDIT_MODAL_KEY,
SignInType,
FAKE_DOOR_FEATURES,
TRIGGER_NODE_CREATOR_VIEW,
REGULAR_NODE_CREATOR_VIEW,
} from './constants';
import type { IMenuItem } from 'n8n-design-system'; import type { IMenuItem } from 'n8n-design-system';
import type { import type {
@ -35,16 +41,10 @@ import type {
IUserSettings, IUserSettings,
IN8nUISettings, IN8nUISettings,
BannerName, BannerName,
INodeProperties,
} from 'n8n-workflow'; } from 'n8n-workflow';
import type { SignInType } from './constants';
import type {
FAKE_DOOR_FEATURES,
TRIGGER_NODE_CREATOR_VIEW,
REGULAR_NODE_CREATOR_VIEW,
} from './constants';
import type { BulkCommand, Undoable } from '@/models/history'; import type { BulkCommand, Undoable } from '@/models/history';
import type { PartialBy } from '@/utils/typeHelpers'; import type { PartialBy, TupleToUnion } from '@/utils/typeHelpers';
import type { INodeProperties } from 'n8n-workflow';
export * from 'n8n-design-system/types'; export * from 'n8n-design-system/types';
@ -734,11 +734,10 @@ export type ActionsRecord<T extends SimplifiedNodeType[]> = {
[K in ExtractActionKeys<T[number]>]: ActionTypeDescription[]; [K in ExtractActionKeys<T[number]>]: ActionTypeDescription[];
}; };
export interface SimplifiedNodeType export type SimplifiedNodeType = Pick<
extends Pick<
INodeTypeDescription, INodeTypeDescription,
'displayName' | 'description' | 'name' | 'group' | 'icon' | 'iconUrl' | 'codex' | 'defaults' 'displayName' | 'description' | 'name' | 'group' | 'icon' | 'iconUrl' | 'codex' | 'defaults'
> {} >;
export interface SubcategoryItemProps { export interface SubcategoryItemProps {
description?: string; description?: string;
iconType?: string; iconType?: string;
@ -1462,6 +1461,8 @@ export type SamlPreferencesExtractedData = {
returnUrl: string; returnUrl: string;
}; };
export type SshKeyTypes = ['ed25519', 'rsa'];
export type SourceControlPreferences = { export type SourceControlPreferences = {
connected: boolean; connected: boolean;
repositoryUrl: string; repositoryUrl: string;
@ -1470,6 +1471,7 @@ export type SourceControlPreferences = {
branchReadOnly: boolean; branchReadOnly: boolean;
branchColor: string; branchColor: string;
publicKey?: string; publicKey?: string;
keyGeneratorType?: TupleToUnion<SshKeyTypes>;
currentBranch?: string; currentBranch?: string;
}; };

View file

@ -14,6 +14,7 @@ export function routesForSourceControl(server: Server) {
branchColor: '#1d6acb', branchColor: '#1d6acb',
connected: false, connected: false,
publicKey: 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHEX+25m', publicKey: 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHEX+25m',
keyGeneratorType: 'ed25519',
}; };
server.get(`${sourceControlApiRoot}/preferences`, (schema: AppSchema, request: Request) => { server.get(`${sourceControlApiRoot}/preferences`, (schema: AppSchema, request: Request) => {

View file

@ -3,9 +3,11 @@ import type {
SourceControlAggregatedFile, SourceControlAggregatedFile,
SourceControlPreferences, SourceControlPreferences,
SourceControlStatus, SourceControlStatus,
SshKeyTypes,
} from '@/Interface'; } from '@/Interface';
import { makeRestApiRequest } from '@/utils'; import { makeRestApiRequest } from '@/utils';
import type { IDataObject } from 'n8n-workflow'; import type { IDataObject } from 'n8n-workflow';
import type { TupleToUnion } from '@/utils/typeHelpers';
const sourceControlApiRoot = '/source-control'; const sourceControlApiRoot = '/source-control';
@ -70,6 +72,11 @@ export const disconnect = async (
}); });
}; };
export const generateKeyPair = async (context: IRestApiContext): Promise<string> => { export const generateKeyPair = async (
return makeRestApiRequest(context, 'POST', `${sourceControlApiRoot}/generate-key-pair`); context: IRestApiContext,
keyGeneratorType?: TupleToUnion<SshKeyTypes>,
): Promise<string> => {
return makeRestApiRequest(context, 'POST', `${sourceControlApiRoot}/generate-key-pair`, {
keyGeneratorType,
});
}; };

View file

@ -3,25 +3,22 @@ import { defineStore } from 'pinia';
import { EnterpriseEditionFeature } from '@/constants'; import { EnterpriseEditionFeature } from '@/constants';
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
import { useRootStore } from '@/stores/n8nRoot.store'; import { useRootStore } from '@/stores/n8nRoot.store';
import { useUsersStore } from '@/stores/users.store';
import * as vcApi from '@/api/sourceControl'; import * as vcApi from '@/api/sourceControl';
import type { SourceControlPreferences } from '@/Interface'; import type { SourceControlPreferences, SshKeyTypes } from '@/Interface';
import type { TupleToUnion } from '@/utils/typeHelpers';
export const useSourceControlStore = defineStore('sourceControl', () => { export const useSourceControlStore = defineStore('sourceControl', () => {
const rootStore = useRootStore(); const rootStore = useRootStore();
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const usersStore = useUsersStore();
const isEnterpriseSourceControlEnabled = computed(() => const isEnterpriseSourceControlEnabled = computed(() =>
settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.SourceControl), settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.SourceControl),
); );
const defaultAuthor = computed(() => {
const user = usersStore.currentUser; const sshKeyTypes: SshKeyTypes = ['ed25519', 'rsa'];
return { const sshKeyTypesWithLabel = reactive(
name: user?.fullName ?? `${user?.firstName} ${user?.lastName}`.trim(), sshKeyTypes.map((value) => ({ value, label: value.toUpperCase() })),
email: user?.email ?? '', );
};
});
const preferences = reactive<SourceControlPreferences>({ const preferences = reactive<SourceControlPreferences>({
branchName: '', branchName: '',
@ -31,6 +28,7 @@ export const useSourceControlStore = defineStore('sourceControl', () => {
branchColor: '#5296D6', branchColor: '#5296D6',
connected: false, connected: false,
publicKey: '', publicKey: '',
keyGeneratorType: 'ed25519',
}); });
const state = reactive<{ const state = reactive<{
@ -95,8 +93,8 @@ export const useSourceControlStore = defineStore('sourceControl', () => {
setPreferences({ connected: false, branches: [] }); setPreferences({ connected: false, branches: [] });
}; };
const generateKeyPair = async () => { const generateKeyPair = async (keyGeneratorType?: TupleToUnion<SshKeyTypes>) => {
await vcApi.generateKeyPair(rootStore.getRestApiContext); await vcApi.generateKeyPair(rootStore.getRestApiContext, keyGeneratorType);
const data = await vcApi.getPreferences(rootStore.getRestApiContext); // To be removed once the API is updated const data = await vcApi.getPreferences(rootStore.getRestApiContext); // To be removed once the API is updated
preferences.publicKey = data.publicKey; preferences.publicKey = data.publicKey;
@ -127,5 +125,6 @@ export const useSourceControlStore = defineStore('sourceControl', () => {
disconnect, disconnect,
getStatus, getStatus,
getAggregatedStatus, getAggregatedStatus,
sshKeyTypesWithLabel,
}; };
}); });

View file

@ -1 +1,2 @@
export type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>; export type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
export type TupleToUnion<T extends readonly unknown[]> = T[number];

View file

@ -5,6 +5,8 @@ import { MODAL_CONFIRM } from '@/constants';
import { useUIStore, useSourceControlStore } from '@/stores'; import { useUIStore, useSourceControlStore } from '@/stores';
import { useToast, useMessage, useLoadingService, useI18n } from '@/composables'; import { useToast, useMessage, useLoadingService, useI18n } from '@/composables';
import CopyInput from '@/components/CopyInput.vue'; import CopyInput from '@/components/CopyInput.vue';
import type { TupleToUnion } from '@/utils/typeHelpers';
import type { SshKeyTypes } from '@/Interface';
const locale = useI18n(); const locale = useI18n();
const sourceControlStore = useSourceControlStore(); const sourceControlStore = useSourceControlStore();
@ -111,6 +113,7 @@ onMounted(async () => {
const formValidationStatus = reactive<Record<string, boolean>>({ const formValidationStatus = reactive<Record<string, boolean>>({
repoUrl: false, repoUrl: false,
keyGeneratorType: false,
}); });
function onValidate(key: string, value: boolean) { function onValidate(key: string, value: boolean) {
@ -129,6 +132,8 @@ const repoUrlValidationRules: Array<Rule | RuleGroup> = [
}, },
]; ];
const keyGeneratorTypeValidationRules: Array<Rule | RuleGroup> = [{ name: 'REQUIRED' }];
const validForConnection = computed(() => formValidationStatus.repoUrl); const validForConnection = computed(() => formValidationStatus.repoUrl);
const branchNameValidationRules: Array<Rule | RuleGroup> = [{ name: 'REQUIRED' }]; const branchNameValidationRules: Array<Rule | RuleGroup> = [{ name: 'REQUIRED' }];
@ -144,7 +149,7 @@ async function refreshSshKey() {
); );
if (confirmation === MODAL_CONFIRM) { if (confirmation === MODAL_CONFIRM) {
await sourceControlStore.generateKeyPair(); await sourceControlStore.generateKeyPair(sourceControlStore.preferences.keyGeneratorType);
toast.showMessage({ toast.showMessage({
title: locale.baseText('settings.sourceControl.refreshSshKey.successful.title'), title: locale.baseText('settings.sourceControl.refreshSshKey.successful.title'),
type: 'success', type: 'success',
@ -166,6 +171,13 @@ const refreshBranches = async () => {
toast.showError(error, locale.baseText('settings.sourceControl.refreshBranches.error')); toast.showError(error, locale.baseText('settings.sourceControl.refreshBranches.error'));
} }
}; };
const onSelectSshKeyType = async (sshKeyType: TupleToUnion<SshKeyTypes>) => {
if (sshKeyType === sourceControlStore.preferences.keyGeneratorType) {
return;
}
sourceControlStore.preferences.keyGeneratorType = sshKeyType;
};
</script> </script>
<template> <template>
@ -219,7 +231,23 @@ const refreshBranches = async () => {
<div v-if="sourceControlStore.preferences.publicKey" :class="$style.group"> <div v-if="sourceControlStore.preferences.publicKey" :class="$style.group">
<label>{{ locale.baseText('settings.sourceControl.sshKey') }}</label> <label>{{ locale.baseText('settings.sourceControl.sshKey') }}</label>
<div :class="{ [$style.sshInput]: !isConnected }"> <div :class="{ [$style.sshInput]: !isConnected }">
<n8n-form-input
v-if="!isConnected"
:class="$style.sshKeyTypeSelect"
label
type="select"
id="keyGeneratorType"
name="keyGeneratorType"
data-test-id="source-control-ssh-key-type-select"
validateOnBlur
:validationRules="keyGeneratorTypeValidationRules"
:options="sourceControlStore.sshKeyTypesWithLabel"
:modelValue="sourceControlStore.preferences.keyGeneratorType"
@validate="(value) => onValidate('keyGeneratorType', value)"
@update:modelValue="onSelectSshKeyType"
/>
<CopyInput <CopyInput
:class="$style.copyInput"
collapse collapse
size="medium" size="medium"
:value="sourceControlStore.preferences.publicKey" :value="sourceControlStore.preferences.publicKey"
@ -230,8 +258,8 @@ const refreshBranches = async () => {
size="large" size="large"
type="tertiary" type="tertiary"
icon="sync" icon="sync"
class="ml-s"
@click="refreshSshKey" @click="refreshSshKey"
data-test-id="source-control-refresh-ssh-key-button"
> >
{{ locale.baseText('settings.sourceControl.refreshSshKey') }} {{ locale.baseText('settings.sourceControl.refreshSshKey') }}
</n8n-button> </n8n-button>
@ -412,12 +440,24 @@ const refreshBranches = async () => {
align-items: center; align-items: center;
> div { > div {
width: calc(100% - 144px - var(--spacing-s)); flex: 1 1 auto;
} }
> button { > button {
height: 42px; height: 42px;
} }
.copyInput {
margin: 0 var(--spacing-2xs);
}
}
.sshKeyTypeSelect {
min-width: 120px;
}
.copyInput {
overflow: auto;
} }
.branchSelection { .branchSelection {

View file

@ -68,8 +68,9 @@ describe('SettingsSourceControl', () => {
await nextTick(); await nextTick();
const updatePreferencesSpy = vi.spyOn(sourceControlStore, 'updatePreferences'); const updatePreferencesSpy = vi.spyOn(sourceControlStore, 'updatePreferences');
const generateKeyPairSpy = vi.spyOn(sourceControlStore, 'generateKeyPair');
const { html, container, getByTestId, getByText, queryByTestId, getByRole } = renderComponent({ const { container, getByTestId, getByText, queryByTestId, getByRole } = renderComponent({
pinia, pinia,
global: { global: {
stubs: ['teleport'], stubs: ['teleport'],
@ -127,6 +128,25 @@ describe('SettingsSourceControl', () => {
await waitFor(() => await waitFor(() =>
expect(queryByTestId('source-control-connected-content')).not.toBeInTheDocument(), expect(queryByTestId('source-control-connected-content')).not.toBeInTheDocument(),
); );
const sshKeyTypeSelect = getByTestId('source-control-ssh-key-type-select');
const refreshSshKeyButton = getByTestId('source-control-refresh-ssh-key-button');
await waitFor(() => {
expect(sshKeyTypeSelect).toBeVisible();
expect(refreshSshKeyButton).toBeVisible();
});
await userEvent.click(within(sshKeyTypeSelect).getByRole('textbox'));
await waitFor(() => expect(getByText('RSA')).toBeVisible());
await userEvent.click(getByText('RSA'));
await userEvent.click(refreshSshKeyButton);
const refreshSshKeyDialog = getByRole('dialog');
await waitFor(() => expect(refreshSshKeyDialog).toBeVisible());
await userEvent.click(within(refreshSshKeyDialog).getAllByRole('button')[1]);
await waitFor(() => expect(refreshSshKeyDialog).not.toBeVisible());
expect(generateKeyPairSpy).toHaveBeenCalledWith('rsa');
}, 10000); }, 10000);
describe('should test repo URLs', () => { describe('should test repo URLs', () => {