mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 04:34:06 -08:00
fix(editor): Add ssh key type selection to source control settings when regenerating key (#7172)
This commit is contained in:
parent
fdac2c8572
commit
54bf66d335
|
@ -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 {
|
||||
|
@ -35,16 +41,10 @@ import type {
|
|||
IUserSettings,
|
||||
IN8nUISettings,
|
||||
BannerName,
|
||||
INodeProperties,
|
||||
} 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 { PartialBy } from '@/utils/typeHelpers';
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
import type { PartialBy, TupleToUnion } from '@/utils/typeHelpers';
|
||||
|
||||
export * from 'n8n-design-system/types';
|
||||
|
||||
|
@ -734,11 +734,10 @@ export type ActionsRecord<T extends SimplifiedNodeType[]> = {
|
|||
[K in ExtractActionKeys<T[number]>]: ActionTypeDescription[];
|
||||
};
|
||||
|
||||
export interface SimplifiedNodeType
|
||||
extends Pick<
|
||||
INodeTypeDescription,
|
||||
'displayName' | 'description' | 'name' | 'group' | 'icon' | 'iconUrl' | 'codex' | 'defaults'
|
||||
> {}
|
||||
export type SimplifiedNodeType = Pick<
|
||||
INodeTypeDescription,
|
||||
'displayName' | 'description' | 'name' | 'group' | 'icon' | 'iconUrl' | 'codex' | 'defaults'
|
||||
>;
|
||||
export interface SubcategoryItemProps {
|
||||
description?: string;
|
||||
iconType?: string;
|
||||
|
@ -1462,6 +1461,8 @@ export type SamlPreferencesExtractedData = {
|
|||
returnUrl: string;
|
||||
};
|
||||
|
||||
export type SshKeyTypes = ['ed25519', 'rsa'];
|
||||
|
||||
export type SourceControlPreferences = {
|
||||
connected: boolean;
|
||||
repositoryUrl: string;
|
||||
|
@ -1470,6 +1471,7 @@ export type SourceControlPreferences = {
|
|||
branchReadOnly: boolean;
|
||||
branchColor: string;
|
||||
publicKey?: string;
|
||||
keyGeneratorType?: TupleToUnion<SshKeyTypes>;
|
||||
currentBranch?: string;
|
||||
};
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ export function routesForSourceControl(server: Server) {
|
|||
branchColor: '#1d6acb',
|
||||
connected: false,
|
||||
publicKey: 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHEX+25m',
|
||||
keyGeneratorType: 'ed25519',
|
||||
};
|
||||
|
||||
server.get(`${sourceControlApiRoot}/preferences`, (schema: AppSchema, request: Request) => {
|
||||
|
|
|
@ -3,9 +3,11 @@ import type {
|
|||
SourceControlAggregatedFile,
|
||||
SourceControlPreferences,
|
||||
SourceControlStatus,
|
||||
SshKeyTypes,
|
||||
} from '@/Interface';
|
||||
import { makeRestApiRequest } from '@/utils';
|
||||
import type { IDataObject } from 'n8n-workflow';
|
||||
import type { TupleToUnion } from '@/utils/typeHelpers';
|
||||
|
||||
const sourceControlApiRoot = '/source-control';
|
||||
|
||||
|
@ -70,6 +72,11 @@ export const disconnect = async (
|
|||
});
|
||||
};
|
||||
|
||||
export const generateKeyPair = async (context: IRestApiContext): Promise<string> => {
|
||||
return makeRestApiRequest(context, 'POST', `${sourceControlApiRoot}/generate-key-pair`);
|
||||
export const generateKeyPair = async (
|
||||
context: IRestApiContext,
|
||||
keyGeneratorType?: TupleToUnion<SshKeyTypes>,
|
||||
): Promise<string> => {
|
||||
return makeRestApiRequest(context, 'POST', `${sourceControlApiRoot}/generate-key-pair`, {
|
||||
keyGeneratorType,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -3,25 +3,22 @@ import { defineStore } from 'pinia';
|
|||
import { EnterpriseEditionFeature } from '@/constants';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useRootStore } from '@/stores/n8nRoot.store';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
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', () => {
|
||||
const rootStore = useRootStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const usersStore = useUsersStore();
|
||||
|
||||
const isEnterpriseSourceControlEnabled = computed(() =>
|
||||
settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.SourceControl),
|
||||
);
|
||||
const defaultAuthor = computed(() => {
|
||||
const user = usersStore.currentUser;
|
||||
return {
|
||||
name: user?.fullName ?? `${user?.firstName} ${user?.lastName}`.trim(),
|
||||
email: user?.email ?? '',
|
||||
};
|
||||
});
|
||||
|
||||
const sshKeyTypes: SshKeyTypes = ['ed25519', 'rsa'];
|
||||
const sshKeyTypesWithLabel = reactive(
|
||||
sshKeyTypes.map((value) => ({ value, label: value.toUpperCase() })),
|
||||
);
|
||||
|
||||
const preferences = reactive<SourceControlPreferences>({
|
||||
branchName: '',
|
||||
|
@ -31,6 +28,7 @@ export const useSourceControlStore = defineStore('sourceControl', () => {
|
|||
branchColor: '#5296D6',
|
||||
connected: false,
|
||||
publicKey: '',
|
||||
keyGeneratorType: 'ed25519',
|
||||
});
|
||||
|
||||
const state = reactive<{
|
||||
|
@ -95,8 +93,8 @@ export const useSourceControlStore = defineStore('sourceControl', () => {
|
|||
setPreferences({ connected: false, branches: [] });
|
||||
};
|
||||
|
||||
const generateKeyPair = async () => {
|
||||
await vcApi.generateKeyPair(rootStore.getRestApiContext);
|
||||
const generateKeyPair = async (keyGeneratorType?: TupleToUnion<SshKeyTypes>) => {
|
||||
await vcApi.generateKeyPair(rootStore.getRestApiContext, keyGeneratorType);
|
||||
const data = await vcApi.getPreferences(rootStore.getRestApiContext); // To be removed once the API is updated
|
||||
|
||||
preferences.publicKey = data.publicKey;
|
||||
|
@ -127,5 +125,6 @@ export const useSourceControlStore = defineStore('sourceControl', () => {
|
|||
disconnect,
|
||||
getStatus,
|
||||
getAggregatedStatus,
|
||||
sshKeyTypesWithLabel,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
export type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
||||
export type TupleToUnion<T extends readonly unknown[]> = T[number];
|
||||
|
|
|
@ -5,6 +5,8 @@ import { MODAL_CONFIRM } from '@/constants';
|
|||
import { useUIStore, useSourceControlStore } from '@/stores';
|
||||
import { useToast, useMessage, useLoadingService, useI18n } from '@/composables';
|
||||
import CopyInput from '@/components/CopyInput.vue';
|
||||
import type { TupleToUnion } from '@/utils/typeHelpers';
|
||||
import type { SshKeyTypes } from '@/Interface';
|
||||
|
||||
const locale = useI18n();
|
||||
const sourceControlStore = useSourceControlStore();
|
||||
|
@ -111,6 +113,7 @@ onMounted(async () => {
|
|||
|
||||
const formValidationStatus = reactive<Record<string, boolean>>({
|
||||
repoUrl: false,
|
||||
keyGeneratorType: false,
|
||||
});
|
||||
|
||||
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 branchNameValidationRules: Array<Rule | RuleGroup> = [{ name: 'REQUIRED' }];
|
||||
|
||||
|
@ -144,7 +149,7 @@ async function refreshSshKey() {
|
|||
);
|
||||
|
||||
if (confirmation === MODAL_CONFIRM) {
|
||||
await sourceControlStore.generateKeyPair();
|
||||
await sourceControlStore.generateKeyPair(sourceControlStore.preferences.keyGeneratorType);
|
||||
toast.showMessage({
|
||||
title: locale.baseText('settings.sourceControl.refreshSshKey.successful.title'),
|
||||
type: 'success',
|
||||
|
@ -166,6 +171,13 @@ const refreshBranches = async () => {
|
|||
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>
|
||||
|
||||
<template>
|
||||
|
@ -219,7 +231,23 @@ const refreshBranches = async () => {
|
|||
<div v-if="sourceControlStore.preferences.publicKey" :class="$style.group">
|
||||
<label>{{ locale.baseText('settings.sourceControl.sshKey') }}</label>
|
||||
<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
|
||||
:class="$style.copyInput"
|
||||
collapse
|
||||
size="medium"
|
||||
:value="sourceControlStore.preferences.publicKey"
|
||||
|
@ -230,8 +258,8 @@ const refreshBranches = async () => {
|
|||
size="large"
|
||||
type="tertiary"
|
||||
icon="sync"
|
||||
class="ml-s"
|
||||
@click="refreshSshKey"
|
||||
data-test-id="source-control-refresh-ssh-key-button"
|
||||
>
|
||||
{{ locale.baseText('settings.sourceControl.refreshSshKey') }}
|
||||
</n8n-button>
|
||||
|
@ -412,12 +440,24 @@ const refreshBranches = async () => {
|
|||
align-items: center;
|
||||
|
||||
> div {
|
||||
width: calc(100% - 144px - var(--spacing-s));
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
> button {
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
.copyInput {
|
||||
margin: 0 var(--spacing-2xs);
|
||||
}
|
||||
}
|
||||
|
||||
.sshKeyTypeSelect {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.copyInput {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.branchSelection {
|
||||
|
|
|
@ -68,8 +68,9 @@ describe('SettingsSourceControl', () => {
|
|||
await nextTick();
|
||||
|
||||
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,
|
||||
global: {
|
||||
stubs: ['teleport'],
|
||||
|
@ -127,6 +128,25 @@ describe('SettingsSourceControl', () => {
|
|||
await waitFor(() =>
|
||||
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);
|
||||
|
||||
describe('should test repo URLs', () => {
|
||||
|
|
Loading…
Reference in a new issue