diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index 738dcc3f6e..1008a09639 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -833,6 +833,9 @@ export interface ISettingsState { promptsData: IN8nPrompts; userManagement: IUserManagementConfig; templatesEndpointHealthy: boolean; + api: { + key: string | undefined; + }; } export interface ITemplateState { diff --git a/packages/editor-ui/src/api/api-keys.ts b/packages/editor-ui/src/api/api-keys.ts index 9d0a03660a..27598149be 100644 --- a/packages/editor-ui/src/api/api-keys.ts +++ b/packages/editor-ui/src/api/api-keys.ts @@ -6,7 +6,7 @@ export function getApiKey(context: IRestApiContext): Promise<{ apiKey: string | } export function createApiKey(context: IRestApiContext): Promise<{ apiKey: string | null }> { - return makeRestApiRequest(context, 'POST', '/users/me/api-key'); + return makeRestApiRequest(context, 'POST', '/me/api-key'); } export function deleteApiKey(context: IRestApiContext): Promise<{ success: boolean }> { diff --git a/packages/editor-ui/src/components/CopyInput.vue b/packages/editor-ui/src/components/CopyInput.vue index baad3bb8c9..1837d827b3 100644 --- a/packages/editor-ui/src/components/CopyInput.vue +++ b/packages/editor-ui/src/components/CopyInput.vue @@ -2,11 +2,11 @@
- {{ copyContent }} -
{{ copyButtonText }}
+ {{ value }} +
{{ copyButtonText || $locale.baseText('generic.copyToClipboard') }}
-
{{ subtitle }}
+
{{ hint }}
@@ -20,26 +20,29 @@ export default mixins(copyPaste, showMessage).extend({ label: { type: String, }, - subtitle: { + hint: { type: String, }, - copyContent: { + value: { type: String, }, copyButtonText: { type: String, }, - successMessage: { + toastTitle: { + type: String, + }, + toastMessage: { type: String, }, }, methods: { copy(): void { - this.copyToClipboard(this.$props.copyContent); + this.copyToClipboard(this.value); this.$showMessage({ - title: this.$locale.baseText('credentialEdit.credentialEdit.showMessage.title'), - message: this.$props.successMessage, + title: this.toastTitle || this.$locale.baseText('generic.copiedToClipboard'), + message: this.toastMessage, type: 'success', }); }, @@ -54,6 +57,10 @@ export default mixins(copyPaste, showMessage).extend({ font-family: Monaco, Consolas; line-height: 1.5; font-size: var(--font-size-s); + overflow: hidden; + width: 100%; + display: block; + text-overflow: ellipsis; } padding: var(--spacing-xs); @@ -86,7 +93,7 @@ export default mixins(copyPaste, showMessage).extend({ } } -.subtitle { +.hint { margin-top: var(--spacing-2xs); font-size: var(--font-size-2xs); line-height: var(--font-line-height-loose); diff --git a/packages/editor-ui/src/components/CredentialEdit/CredentialConfig.vue b/packages/editor-ui/src/components/CredentialEdit/CredentialConfig.vue index 705f5f51b5..d5eb29441f 100644 --- a/packages/editor-ui/src/components/CredentialEdit/CredentialConfig.vue +++ b/packages/editor-ui/src/components/CredentialEdit/CredentialConfig.vue @@ -48,10 +48,11 @@ + + + + + + + + + + diff --git a/packages/editor-ui/src/components/Modals.vue b/packages/editor-ui/src/components/Modals.vue index 9492273b3c..53a6924212 100644 --- a/packages/editor-ui/src/components/Modals.vue +++ b/packages/editor-ui/src/components/Modals.vue @@ -88,6 +88,10 @@ + + + + @@ -95,6 +99,7 @@ import Vue from "vue"; import { ABOUT_MODAL_KEY, + DELETE_API_KEY_MODAL_KEY, CHANGE_PASSWORD_MODAL_KEY, CONTACT_PROMPT_MODAL_KEY, CREDENTIAL_EDIT_MODAL_KEY, @@ -120,6 +125,7 @@ import CredentialEdit from "./CredentialEdit/CredentialEdit.vue"; import CredentialsList from "./CredentialsList.vue"; import InviteUsersModal from "./InviteUsersModal.vue"; import CredentialsSelectModal from "./CredentialsSelectModal.vue"; +import DeleteApiKeyModal from "./DeleteApiKeyModal.vue"; import DuplicateWorkflowDialog from "./DuplicateWorkflowDialog.vue"; import ModalRoot from "./ModalRoot.vue"; import PersonalizationModal from "./PersonalizationModal.vue"; @@ -142,6 +148,7 @@ export default Vue.extend({ CredentialEdit, CredentialsList, CredentialsSelectModal, + DeleteApiKeyModal, DeleteUserModal, DuplicateWorkflowDialog, InviteUsersModal, @@ -155,6 +162,7 @@ export default Vue.extend({ WorkflowOpen, }, data: () => ({ + DELETE_API_KEY_MODAL_KEY, CONTACT_PROMPT_MODAL_KEY, CREDENTIAL_EDIT_MODAL_KEY, CREDENTIAL_LIST_MODAL_KEY, diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts index 8d7f09e806..d80a3a1289 100644 --- a/packages/editor-ui/src/constants.ts +++ b/packages/editor-ui/src/constants.ts @@ -22,6 +22,7 @@ export const CHANGE_PASSWORD_MODAL_KEY = 'changePassword'; export const CREDENTIAL_EDIT_MODAL_KEY = 'editCredential'; export const CREDENTIAL_SELECT_MODAL_KEY = 'selectCredential'; export const DELETE_USER_MODAL_KEY = 'deleteUser'; +export const DELETE_API_KEY_MODAL_KEY = 'deleteApiKey'; export const INVITE_USER_MODAL_KEY = 'inviteUser'; export const DUPLICATE_MODAL_KEY = 'duplicate'; export const TAGS_MANAGER_MODAL_KEY = 'tagsManager'; diff --git a/packages/editor-ui/src/modules/settings.ts b/packages/editor-ui/src/modules/settings.ts index b25fb137ec..1452d27921 100644 --- a/packages/editor-ui/src/modules/settings.ts +++ b/packages/editor-ui/src/modules/settings.ts @@ -12,6 +12,7 @@ import Vue from 'vue'; import { CONTACT_PROMPT_MODAL_KEY, VALUE_SURVEY_MODAL_KEY } from '@/constants'; import { ITelemetrySettings } from 'n8n-workflow'; import { testHealthEndpoint } from '@/api/templates'; +import {createApiKey, deleteApiKey, getApiKey} from "@/api/api-keys"; const module: Module = { namespaced: true, @@ -24,6 +25,9 @@ const module: Module = { smtpSetup: false, }, templatesEndpointHealthy: false, + api: { + key: undefined, + }, }, getters: { versionCli(state: ISettingsState) { @@ -68,6 +72,9 @@ const module: Module = { templatesHost: (state): string => { return state.settings.templates.host; }, + apiKey: (state): string | undefined => { + return state.api.key; + }, }, mutations: { setSettings(state: ISettingsState, settings: IN8nUISettings) { @@ -85,6 +92,9 @@ const module: Module = { setTemplatesEndpointHealthy(state: ISettingsState) { state.templatesEndpointHealthy = true; }, + setApiKey(state: ISettingsState, apiKey: string | undefined) { + state.api.key = apiKey; + }, }, actions: { async getSettings(context: ActionContext) { @@ -153,6 +163,18 @@ const module: Module = { await Promise.race([testHealthEndpoint(context.getters.templatesHost), timeout]); context.commit('setTemplatesEndpointHealthy', true); }, + async getApiKey(context: ActionContext) { + const { apiKey } = await getApiKey(context.rootGetters['getRestApiContext']); + context.commit('setApiKey', apiKey); + }, + async createApiKey(context: ActionContext) { + const { apiKey } = await createApiKey(context.rootGetters['getRestApiContext']); + context.commit('setApiKey', apiKey); + }, + async deleteApiKey(context: ActionContext) { + await deleteApiKey(context.rootGetters['getRestApiContext']); + context.commit('setApiKey', undefined); + }, }, }; diff --git a/packages/editor-ui/src/modules/ui.ts b/packages/editor-ui/src/modules/ui.ts index 6342553d6f..cb63f3eaa8 100644 --- a/packages/editor-ui/src/modules/ui.ts +++ b/packages/editor-ui/src/modules/ui.ts @@ -1,5 +1,6 @@ import { ABOUT_MODAL_KEY, + DELETE_API_KEY_MODAL_KEY, CREDENTIAL_EDIT_MODAL_KEY, CREDENTIAL_SELECT_MODAL_KEY, CHANGE_PASSWORD_MODAL_KEY, @@ -32,6 +33,9 @@ const module: Module = { [ABOUT_MODAL_KEY]: { open: false, }, + [DELETE_API_KEY_MODAL_KEY]: { + open: false, + }, [CHANGE_PASSWORD_MODAL_KEY]: { open: false, }, @@ -145,6 +149,9 @@ const module: Module = { }, }, actions: { + closeModal: async (context: ActionContext, modalKey: string) => { + context.commit('closeModal', modalKey); + }, openModal: async (context: ActionContext, modalKey: string) => { context.commit('openModal', modalKey); }, diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index 352b189b12..1f52788f37 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -4,6 +4,12 @@ "_reusableBaseText.save": "Save", "_reusableDynamicText.oauth2.clientId": "Client ID", "_reusableDynamicText.oauth2.clientSecret": "Client Secret", + "generic.learnMore": "Learn more", + "generic.confirm": "Confirm", + "generic.cancel": "Cancel", + "generic.delete": "Delete", + "generic.copyToClipboard": "Copy to clipboard", + "generic.copiedToClipboard": "Copied to clipboard", "about.aboutN8n": "About n8n", "about.close": "Close", "about.license": "License", @@ -653,12 +659,16 @@ "settings.api.create.description.link": "REST API", "settings.api.create.button": "Create an API Key", "settings.api.create.button.loading": "Creating API Key...", - "settings.api.error.title": "Something went wrong", - "settings.api.error.get": "Could not check if an api key already exists.", - "settings.api.error.create": "Creating the API Key failed.", - "settings.api.error.delete": "Deleting the API Key failed.", + "settings.api.create.error": "Creating the API Key failed.", + "settings.api.delete.title": "Delete this API Key?", + "settings.api.delete.description": "Any application using this API Key will no longer be granted access to n8n data. This operation cannot be undone.", + "settings.api.delete.button": "Delete Forever", + "settings.api.delete.error": "Deleting the API Key failed.", + "settings.api.delete.toast": "API Key deleted", "settings.api.view.info": "Use your API Key to access n8n's REST API to build your own integrations.", "settings.api.view.myKey": "My API Key", + "settings.api.view.copy.toast": "API Key copied to clipboard", + "settings.api.view.error": "Could not check if an api key already exists.", "settings.version": "Version", "showMessage.cancel": "@:_reusableBaseText.cancel", "showMessage.ok": "OK", diff --git a/packages/editor-ui/src/views/SettingsApiView.vue b/packages/editor-ui/src/views/SettingsApiView.vue index 750b77d1af..109622348f 100644 --- a/packages/editor-ui/src/views/SettingsApiView.vue +++ b/packages/editor-ui/src/views/SettingsApiView.vue @@ -6,18 +6,41 @@ {{ $locale.baseText('settings.api') }} - - Hello - + +
+

+ + + {{ $locale.baseText('settings.api.view.info') }} + + {{ $locale.baseText('generic.learnMore') }} + + +

+ + + + {{ $locale.baseText('generic.delete') }} + + + + +
{{ $locale.baseText('settings.api.create.title') }}

- {{$locale.baseText('settings.api.create.description')}} - - {{$locale.baseText('settings.api.create.description.link')}} - + + {{$locale.baseText('settings.api.create.description')}} + + {{$locale.baseText('settings.api.create.description.link')}} + +

{{$locale.baseText(loading ? 'settings.api.create.button.loading' : 'settings.api.create.button')}} @@ -34,6 +57,8 @@ import { IUser } from '@/Interface'; import mixins from 'vue-typed-mixins'; import SettingsView from './SettingsView.vue'; +import CopyInput from '../components/CopyInput.vue'; +import {DELETE_API_KEY_MODAL_KEY} from "../constants"; export default mixins( showMessage, @@ -41,30 +66,35 @@ export default mixins( name: 'SettingsPersonalView', components: { SettingsView, + CopyInput, }, data() { return { - apiKey: '' as string | null, loading: false, mounted: false, error: '', }; }, - async mounted() { + mounted() { this.getApiKey(); }, computed: { currentUser() { return this.$store.getters['users/currentUser'] as IUser; }, + apiKey() { + return this.$store.getters['settings/apiKey']; + }, }, methods: { + showDeleteModal() { + this.$store.dispatch('ui/openModal', DELETE_API_KEY_MODAL_KEY); + }, async getApiKey() { try { - const { apiKey } = await getApiKey(this.$store.getters['getRestApiContext']); - this.apiKey = apiKey; + this.$store.dispatch('settings/getApiKey'); } catch (error) { - this.$showError(error, this.$locale.baseText('settings.api.error.get')); + this.$showError(error, this.$locale.baseText('settings.api.view.error')); } finally { this.mounted = true; } @@ -73,23 +103,13 @@ export default mixins( this.loading = true; try { - const { apiKey } = await createApiKey(this.$store.getters['getRestApiContext']); - this.apiKey = apiKey; + this.$store.dispatch('settings/createApiKey'); } catch (error) { - this.$showError(error, this.$locale.baseText('settings.api.error.create')); + this.$showError(error, this.$locale.baseText('settings.api.create.error')); } finally { this.loading = false; } }, - async deleteApiKey() { - this.loading = true; - - try { - await deleteApiKey(this.$store.getters['getRestApiContext']); - } catch (error) { - this.$showError(error, this.$locale.baseText('settings.api.error.delete')); - } - }, }, }); @@ -120,5 +140,16 @@ export default mixins( align-items: center; flex-direction: column; } + +.card { + position: relative; +} + +.delete { + position: absolute; + display: inline-block; + top: var(--spacing-s); + right: var(--spacing-s); +}