From 2d610f73e41c3dcbad3d88a820c8060523a127db Mon Sep 17 00:00:00 2001 From: Luis de Dios Date: Wed, 21 Jan 2026 20:41:09 +0100 Subject: [PATCH] :tada: Add MCP server to integrations section in dashboard --- CHANGES.md | 2 + backend/src/app/migrations.clj | 4 +- .../sql/0146-mod-access-token-table.sql | 2 + backend/src/app/rpc/commands/access_token.clj | 12 +- backend/src/app/rpc/commands/profile.clj | 1 + .../backend_tests/http_middleware_test.clj | 2 +- common/src/app/common/flags.cljc | 4 +- frontend/src/app/config.cljs | 3 + frontend/src/app/main/data/profile.cljs | 1 - frontend/src/app/main/ui.cljs | 2 +- frontend/src/app/main/ui/confirm.cljs | 39 +- frontend/src/app/main/ui/confirm.scss | 32 +- frontend/src/app/main/ui/ds/_sizes.scss | 1 + .../src/app/main/ui/ds/controls/input.cljs | 11 +- frontend/src/app/main/ui/forms.cljs | 20 +- frontend/src/app/main/ui/routes.cljs | 2 +- frontend/src/app/main/ui/settings.cljs | 6 +- .../app/main/ui/settings/access_tokens.cljs | 291 --------- .../app/main/ui/settings/access_tokens.scss | 202 ------ .../app/main/ui/settings/integrations.cljs | 573 ++++++++++++++++++ .../app/main/ui/settings/integrations.scss | 221 +++++++ .../src/app/main/ui/settings/sidebar.cljs | 17 +- frontend/translations/en.po | 322 ++++++---- frontend/translations/es.po | 322 ++++++---- 24 files changed, 1306 insertions(+), 786 deletions(-) create mode 100644 backend/src/app/migrations/sql/0146-mod-access-token-table.sql delete mode 100644 frontend/src/app/main/ui/settings/access_tokens.cljs delete mode 100644 frontend/src/app/main/ui/settings/access_tokens.scss create mode 100644 frontend/src/app/main/ui/settings/integrations.cljs create mode 100644 frontend/src/app/main/ui/settings/integrations.scss diff --git a/CHANGES.md b/CHANGES.md index fc36ed5c03..2cfe7d70b6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,6 +17,8 @@ - Optimize sidebar performance for deeply nested shapes [Taiga #13017](https://tree.taiga.io/project/penpot/task/13017) - Remove tokens path node and bulk remove tokens [Taiga #13007](https://tree.taiga.io/project/penpot/us/13007) - Replace themes management modal radio buttons for switches [Taiga #9215](https://tree.taiga.io/project/penpot/us/9215) +- [MCP server] Integrations section [Taiga #13112](https://tree.taiga.io/project/penpot/us/13112) +- [Access Tokens] Look & feel refinement [Taiga #13114](https://tree.taiga.io/project/penpot/us/13114) ### :bug: Bugs fixed diff --git a/backend/src/app/migrations.clj b/backend/src/app/migrations.clj index 2a9d9eba0b..4c9199a6f5 100644 --- a/backend/src/app/migrations.clj +++ b/backend/src/app/migrations.clj @@ -463,8 +463,10 @@ :fn (mg/resource "app/migrations/sql/0144-mod-server-error-report-table.sql")} {:name "0145-fix-plugins-uri-on-profile" - :fn mg0145/migrate}]) + :fn mg0145/migrate} + {:name "0146-mod-access-token-table" + :fn (mg/resource "app/migrations/sql/0146-mod-access-token-table.sql")}]) (defn apply-migrations! [pool name migrations] diff --git a/backend/src/app/migrations/sql/0146-mod-access-token-table.sql b/backend/src/app/migrations/sql/0146-mod-access-token-table.sql new file mode 100644 index 0000000000..574257859d --- /dev/null +++ b/backend/src/app/migrations/sql/0146-mod-access-token-table.sql @@ -0,0 +1,2 @@ +ALTER TABLE access_token + ADD COLUMN type text NULL; diff --git a/backend/src/app/rpc/commands/access_token.clj b/backend/src/app/rpc/commands/access_token.clj index a302b82053..73cc3c4dea 100644 --- a/backend/src/app/rpc/commands/access_token.clj +++ b/backend/src/app/rpc/commands/access_token.clj @@ -23,7 +23,7 @@ (dissoc row :perms)) (defn create-access-token - [{:keys [::db/conn] :as cfg} profile-id name expiration] + [{:keys [::db/conn] :as cfg} profile-id name expiration type] (let [token-id (uuid/next) expires-at (some-> expiration (ct/in-future)) created-at (ct/now) @@ -36,6 +36,7 @@ {:id token-id :name name :token token + :type type :profile-id profile-id :created-at created-at :updated-at created-at @@ -50,17 +51,18 @@ (def ^:private schema:create-access-token [:map {:title "create-access-token"} [:name [:string {:max 250 :min 1}]] - [:expiration {:optional true} ::ct/duration]]) + [:expiration {:optional true} ::ct/duration] + [:type {:optional true} :string]]) (sv/defmethod ::create-access-token {::doc/added "1.18" ::sm/params schema:create-access-token} - [cfg {:keys [::rpc/profile-id name expiration]}] + [cfg {:keys [::rpc/profile-id name expiration type]}] (quotes/check! cfg {::quotes/id ::quotes/access-tokens-per-profile ::quotes/profile-id profile-id}) - (db/tx-run! cfg create-access-token profile-id name expiration)) + (db/tx-run! cfg create-access-token profile-id name expiration type)) (def ^:private schema:delete-access-token [:map {:title "delete-access-token"} @@ -83,5 +85,5 @@ (->> (db/query pool :access-token {:profile-id profile-id} {:order-by [[:expires-at :asc] [:created-at :asc]] - :columns [:id :name :perms :created-at :updated-at :expires-at]}) + :columns [:id :name :perms :type :created-at :updated-at :expires-at]}) (mapv decode-row))) diff --git a/backend/src/app/rpc/commands/profile.clj b/backend/src/app/rpc/commands/profile.clj index 3d2f2b1351..4383ab794f 100644 --- a/backend/src/app/rpc/commands/profile.clj +++ b/backend/src/app/rpc/commands/profile.clj @@ -48,6 +48,7 @@ (def schema:props [:map {:title "ProfileProps"} [:plugins {:optional true} schema:plugin-registry] + [:mcp-status {:optional true} ::sm/boolean] [:newsletter-updates {:optional true} ::sm/boolean] [:newsletter-news {:optional true} ::sm/boolean] [:onboarding-team-id {:optional true} ::sm/uuid] diff --git a/backend/test/backend_tests/http_middleware_test.clj b/backend/test/backend_tests/http_middleware_test.clj index b4fa5062d5..809e43f9e3 100644 --- a/backend/test/backend_tests/http_middleware_test.clj +++ b/backend/test/backend_tests/http_middleware_test.clj @@ -102,7 +102,7 @@ (t/deftest access-token-authz (let [profile (th/create-profile* 1) - token (db/tx-run! th/*system* app.rpc.commands.access-token/create-access-token (:id profile) "test" nil) + token (db/tx-run! th/*system* app.rpc.commands.access-token/create-access-token (:id profile) "test" nil nil) handler (#'app.http.access-token/wrap-authz identity th/*system*)] (let [response (handler nil)] diff --git a/common/src/app/common/flags.cljc b/common/src/app/common/flags.cljc index 816bc2edbb..64cb7f9d68 100644 --- a/common/src/app/common/flags.cljc +++ b/common/src/app/common/flags.cljc @@ -152,7 +152,9 @@ :redis-cache ;; Activates the nitrate module - :nitrate}) + :nitrate + + :mcp}) (def all-flags (set/union email login varia)) diff --git a/frontend/src/app/config.cljs b/frontend/src/app/config.cljs index c32c76bbb8..576211de0b 100644 --- a/frontend/src/app/config.cljs +++ b/frontend/src/app/config.cljs @@ -147,6 +147,9 @@ (let [f (obj/get global "initializeExternalConfigInfo")] (when (fn? f) (f)))) +(def mcp-server-url (-> public-uri u/ensure-path-slash (u/join "mcp") str)) +(def mcp-help-center-uri "https://help.penpot.app/technical-guide/") + ;; --- Helper Functions (defn ^boolean check-browser? [candidate] diff --git a/frontend/src/app/main/data/profile.cljs b/frontend/src/app/main/data/profile.cljs index e7828a0302..66ded6fc8b 100644 --- a/frontend/src/app/main/data/profile.cljs +++ b/frontend/src/app/main/data/profile.cljs @@ -498,4 +498,3 @@ (->> (rp/cmd! :delete-access-token params) (rx/tap on-success) (rx/catch on-error)))))) - diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index a247a982a8..41e18c846f 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -197,7 +197,7 @@ :settings-options :settings-feedback :settings-subscription - :settings-access-tokens + :settings-integrations :settings-notifications) (let [params (get params :query) error-report-id (some-> params :error-report-id uuid/parse*)] diff --git a/frontend/src/app/main/ui/confirm.cljs b/frontend/src/app/main/ui/confirm.cljs index ca8a78aea0..d2c068ebf2 100644 --- a/frontend/src/app/main/ui/confirm.cljs +++ b/frontend/src/app/main/ui/confirm.cljs @@ -9,14 +9,17 @@ (:require [app.main.data.modal :as modal] [app.main.store :as st] + [app.main.ui.ds.buttons.button :refer [button*]] + [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] + [app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]] [app.main.ui.ds.notifications.context-notification :refer [context-notification*]] - [app.main.ui.icons :as deprecated-icon] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as k] [goog.events :as events] [rumext.v2 :as mf]) - (:import goog.events.EventType)) + (:import + goog.events.EventType)) (mf/defc confirm-dialog {::mf/register modal/components @@ -68,8 +71,11 @@ [:div {:class (stl/css :modal-container)} [:div {:class (stl/css :modal-header)} [:h2 {:class (stl/css :modal-title)} title] - [:button {:class (stl/css :modal-close-btn) - :on-click cancel-fn} deprecated-icon/close]] + [:div {:class (stl/css :modal-close-btn)} + [:> icon-button* {:variant "ghost" + :aria-label (tr "labels.close") + :on-click cancel-fn + :icon i/close}]]] [:div {:class (stl/css :modal-content)} (when (and (string? message) (not= message "")) @@ -87,24 +93,19 @@ [:ul {:class (stl/css :component-list)} (for [item items] [:li {:class (stl/css :modal-item-element)} - [:span {:class (stl/css :modal-component-icon)} - deprecated-icon/component] + [:> icon* {:icon-id i/component + :class (stl/css :modal-component-icon) + :size "s"}] [:span {:class (stl/css :modal-component-name)} (:name item)]])]])] [:div {:class (stl/css :modal-footer)} [:div {:class (stl/css :action-buttons)} (when-not (= cancel-label :omit) - [:input - {:class (stl/css :cancel-button) - :type "button" - :value cancel-label - :on-click cancel-fn}]) - - [:input - {:class (stl/css-case :accept-btn true - :danger (= accept-style :danger) - :primary (= accept-style :primary)) - :type "button" - :value accept-label - :on-click accept-fn}]]]]])) + [:> button* {:variant "secondary" + :on-click cancel-fn} + cancel-label]) + [:> button* {:variant (cond (= accept-style :danger) "destructive" + (= accept-style :primary) "primary") + :on-click accept-fn} + accept-label]]]]])) diff --git a/frontend/src/app/main/ui/confirm.scss b/frontend/src/app/main/ui/confirm.scss index e517a7b685..09b23426f3 100644 --- a/frontend/src/app/main/ui/confirm.scss +++ b/frontend/src/app/main/ui/confirm.scss @@ -15,10 +15,9 @@ .modal-container { @extend .modal-container-base; -} - -.modal-header { - margin-bottom: deprecated.$s-24; + display: flex; + flex-direction: column; + gap: var(--sp-xxl); } .modal-title { @@ -27,12 +26,13 @@ } .modal-close-btn { - @extend .modal-close-btn-base; + position: absolute; + top: var(--sp-m); + right: var(--sp-m); } .modal-content { @include deprecated.bodyLargeTypography; - margin-bottom: deprecated.$s-24; } .modal-item-element { @@ -41,32 +41,18 @@ .modal-component-icon { @include deprecated.flexCenter; - height: deprecated.$s-16; - width: deprecated.$s-16; - svg { - @extend .button-icon-small; - stroke: var(--color); - } + color: var(--color-foreground-secondary); } + .modal-component-name { @include deprecated.bodyLargeTypography; + color: var(--color-foreground-secondary); } .action-buttons { @extend .modal-action-btns; } -.cancel-button { - @extend .modal-cancel-btn; -} - -.accept-btn { - @extend .modal-accept-btn; - &.danger { - @extend .modal-danger-btn; - } -} - .modal-scd-msg, .modal-subtitle, .modal-msg { diff --git a/frontend/src/app/main/ui/ds/_sizes.scss b/frontend/src/app/main/ui/ds/_sizes.scss index 067bd0b416..9daa3a5ec7 100644 --- a/frontend/src/app/main/ui/ds/_sizes.scss +++ b/frontend/src/app/main/ui/ds/_sizes.scss @@ -18,6 +18,7 @@ $sz-32: px2rem(32); $sz-36: px2rem(36); $sz-40: px2rem(40); $sz-48: px2rem(48); +$sz-64: px2rem(64); $sz-88: px2rem(88); $sz-96: px2rem(96); $sz-120: px2rem(120); diff --git a/frontend/src/app/main/ui/ds/controls/input.cljs b/frontend/src/app/main/ui/ds/controls/input.cljs index 29ae0cc804..918e5a446b 100644 --- a/frontend/src/app/main/ui/ds/controls/input.cljs +++ b/frontend/src/app/main/ui/ds/controls/input.cljs @@ -8,7 +8,6 @@ (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] - [app.common.data.macros :as dm] [app.main.constants :refer [max-input-length]] [app.main.ui.ds.controls.utilities.hint-message :refer [hint-message*]] [app.main.ui.ds.controls.utilities.input-field :refer [input-field*]] @@ -52,10 +51,11 @@ :has-hint has-hint :hint-type hint-type :variant variant})] - [:div {:class (dm/str class " " (stl/css-case :input-wrapper true - :variant-dense (= variant "dense") - :variant-comfortable (= variant "comfortable") - :has-hint has-hint))} + + [:div {:class [class (stl/css-case :input-wrapper true + :variant-dense (= variant "dense") + :variant-comfortable (= variant "comfortable") + :has-hint has-hint)]} (when has-label [:> label* {:for id :is-optional is-optional} label]) [:> input-field* props] @@ -64,4 +64,3 @@ :class hint-class :message hint-message :type hint-type}])])) - diff --git a/frontend/src/app/main/ui/forms.cljs b/frontend/src/app/main/ui/forms.cljs index 7f1244dcad..9aede980cf 100644 --- a/frontend/src/app/main/ui/forms.cljs +++ b/frontend/src/app/main/ui/forms.cljs @@ -8,6 +8,7 @@ (:require [app.main.ui.ds.buttons.button :refer [button*]] [app.main.ui.ds.controls.input :refer [input*]] + [app.main.ui.ds.controls.select :refer [select*]] [app.util.dom :as dom] [app.util.forms :as fm] [app.util.keyboard :as k] @@ -47,6 +48,23 @@ [:> input* props])) +(mf/defc form-select* + [{:keys [name] :as props}] + (let [select-name name + form (mf/use-ctx context) + value (get-in @form [:data select-name] "") + + handle-change + (fn [event] + (let [value (if (string? event) event (dom/get-target-val event))] + (fm/on-input-change form select-name value))) + + props + (mf/spread-props props {:on-change handle-change + :value value})] + + [:> select* props])) + (mf/defc form-submit* [{:keys [disabled on-submit] :rest props}] (let [form (mf/use-ctx context) @@ -79,4 +97,4 @@ (when (fn? on-submit) (on-submit form event))))] [:> (mf/provider context) {:value form} - [:form {:class class :on-submit on-submit'} children]])) \ No newline at end of file + [:form {:class class :on-submit on-submit'} children]])) diff --git a/frontend/src/app/main/ui/routes.cljs b/frontend/src/app/main/ui/routes.cljs index e8159d3852..ca45bc5133 100644 --- a/frontend/src/app/main/ui/routes.cljs +++ b/frontend/src/app/main/ui/routes.cljs @@ -36,7 +36,7 @@ ["/feedback" :settings-feedback] ["/options" :settings-options] ["/subscriptions" :settings-subscription] - ["/access-tokens" :settings-access-tokens] + ["/integrations" :settings-integrations] ["/notifications" :settings-notifications]] ["/frame-preview" :frame-preview] diff --git a/frontend/src/app/main/ui/settings.cljs b/frontend/src/app/main/ui/settings.cljs index bc40ae20fa..2cb617c939 100644 --- a/frontend/src/app/main/ui/settings.cljs +++ b/frontend/src/app/main/ui/settings.cljs @@ -13,10 +13,10 @@ [app.main.store :as st] [app.main.ui.hooks :as hooks] [app.main.ui.modal :refer [modal-container*]] - [app.main.ui.settings.access-tokens :refer [access-tokens-page]] [app.main.ui.settings.change-email] [app.main.ui.settings.delete-account] [app.main.ui.settings.feedback :refer [feedback-page*]] + [app.main.ui.settings.integrations :refer [integrations-page*]] [app.main.ui.settings.notifications :refer [notifications-page*]] [app.main.ui.settings.options :refer [options-page]] [app.main.ui.settings.password :refer [password-page]] @@ -73,8 +73,8 @@ :settings-subscription [:> subscription-page* {:profile profile}] - :settings-access-tokens - [:& access-tokens-page] + :settings-integrations + [:> integrations-page*] :settings-notifications [:& notifications-page* {:profile profile}])]]]])) diff --git a/frontend/src/app/main/ui/settings/access_tokens.cljs b/frontend/src/app/main/ui/settings/access_tokens.cljs deleted file mode 100644 index 29a09476b0..0000000000 --- a/frontend/src/app/main/ui/settings/access_tokens.cljs +++ /dev/null @@ -1,291 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) KALEIDOS INC - -(ns app.main.ui.settings.access-tokens - (:require-macros [app.main.style :as stl]) - (:require - [app.common.schema :as sm] - [app.common.time :as ct] - [app.main.data.modal :as modal] - [app.main.data.notifications :as ntf] - [app.main.data.profile :as du] - [app.main.store :as st] - [app.main.ui.components.context-menu-a11y :refer [context-menu*]] - [app.main.ui.components.forms :as fm] - [app.main.ui.icons :as deprecated-icon] - [app.util.clipboard :as clipboard] - [app.util.dom :as dom] - [app.util.i18n :as i18n :refer [tr]] - [app.util.keyboard :as kbd] - [okulary.core :as l] - [rumext.v2 :as mf])) - -(def ^:private clipboard-icon - (deprecated-icon/icon-xref :clipboard (stl/css :clipboard-icon))) - -(def ^:private close-icon - (deprecated-icon/icon-xref :close (stl/css :close-icon))) - -(def ^:private menu-icon - (deprecated-icon/icon-xref :menu (stl/css :menu-icon))) - -(def tokens-ref - (l/derived :access-tokens st/state)) - -(def token-created-ref - (l/derived :access-token-created st/state)) - -(def ^:private schema:form - [:map {:title "AccessTokenForm"} - [:name [::sm/text {:max 250}]] - [:expiration-date [::sm/text {:max 250}]]]) - -(def initial-data - {:name "" :expiration-date "never"}) - -(mf/defc access-token-modal - {::mf/register modal/components - ::mf/register-as :access-token} - [] - (let [form (fm/use-form - :initial initial-data - :schema schema:form) - - created (mf/deref token-created-ref) - created? (mf/use-state false) - - on-success - (mf/use-fn - (mf/deps created) - (fn [_] - (let [message (tr "dashboard.access-tokens.create.success")] - (st/emit! (du/fetch-access-tokens) - (ntf/success message) - (reset! created? true))))) - - on-close - (mf/use-fn - (mf/deps created) - (fn [_] - (reset! created? false) - (st/emit! (modal/hide)))) - - on-error - (mf/use-fn - (fn [_] - (st/emit! (ntf/error (tr "errors.generic")) - (modal/hide)))) - - on-submit - (mf/use-fn - (fn [form] - (let [cdata (:clean-data @form) - mdata {:on-success (partial on-success form) - :on-error (partial on-error form)} - expiration (:expiration-date cdata) - params (cond-> {:name (:name cdata) - :perms (:perms cdata)} - (not= "never" expiration) (assoc :expiration expiration))] - (st/emit! (du/create-access-token - (with-meta params mdata)))))) - - copy-token - (mf/use-fn - (mf/deps created) - (fn [event] - (dom/prevent-default event) - (clipboard/to-clipboard (:token created)) - (st/emit! (ntf/show {:level :info - :type :toast - :content (tr "dashboard.access-tokens.copied-success") - :timeout 7000}))))] - - [:div {:class (stl/css :modal-overlay)} - [:div {:class (stl/css :modal-container)} - [:& fm/form {:form form :on-submit on-submit} - - [:div {:class (stl/css :modal-header)} - [:h2 {:class (stl/css :modal-title)} (tr "modals.create-access-token.title")] - - [:button {:class (stl/css :modal-close-btn) - :on-click on-close} - close-icon]] - - [:div {:class (stl/css :modal-content)} - [:div {:class (stl/css :fields-row)} - [:& fm/input {:type "text" - :auto-focus? true - :form form - :name :name - :disabled @created? - :label (tr "modals.create-access-token.name.label") - :show-success? true - :placeholder (tr "modals.create-access-token.name.placeholder")}]] - - [:div {:class (stl/css :fields-row)} - [:div {:class (stl/css :select-title)} - (tr "modals.create-access-token.expiration-date.label")] - [:& fm/select {:options [{:label (tr "dashboard.access-tokens.expiration-never") :value "never" :key "never"} - {:label (tr "dashboard.access-tokens.expiration-30-days") :value "720h" :key "720h"} - {:label (tr "dashboard.access-tokens.expiration-60-days") :value "1440h" :key "1440h"} - {:label (tr "dashboard.access-tokens.expiration-90-days") :value "2160h" :key "2160h"} - {:label (tr "dashboard.access-tokens.expiration-180-days") :value "4320h" :key "4320h"}] - :default "never" - :disabled @created? - :name :expiration-date}] - (when @created? - [:span {:class (stl/css :token-created-info)} - (if (:expires-at created) - (tr "dashboard.access-tokens.token-will-expire" (ct/format-inst (:expires-at created) "PPP")) - (tr "dashboard.access-tokens.token-will-not-expire"))])] - - [:div {:class (stl/css :fields-row)} - (when @created? - [:div {:class (stl/css :custon-input-wrapper)} - [:input {:type "text" - :value (:token created "") - :class (stl/css :custom-input-token) - :read-only true}] - [:button {:title (tr "modals.create-access-token.copy-token") - :class (stl/css :copy-btn) - :on-click copy-token} - clipboard-icon]]) - #_(when @created? - [:button {:class (stl/css :copy-btn) - :title (tr "modals.create-access-token.copy-token") - :on-click copy-token} - [:span {:class (stl/css :token-value)} (:token created "")] - [:span {:class (stl/css :icon)} - i/clipboard]])]] - - [:div {:class (stl/css :modal-footer)} - [:div {:class (stl/css :action-buttons)} - - (if @created? - [:input {:class (stl/css :cancel-button) - :type "button" - :value (tr "labels.close") - :on-click modal/hide!}] - [:* - [:input {:class (stl/css :cancel-button) - :type "button" - :value (tr "labels.cancel") - :on-click modal/hide!}] - [:> fm/submit-button* - {:large? false :label (tr "modals.create-access-token.submit-label")}]])]]]]])) - -(mf/defc access-tokens-hero - [] - (let [on-click (mf/use-fn #(st/emit! (modal/show :access-token {})))] - [:div {:class (stl/css :access-tokens-hero)} - [:h2 {:class (stl/css :hero-title)} (tr "dashboard.access-tokens.personal")] - [:p {:class (stl/css :hero-desc)} (tr "dashboard.access-tokens.personal.description")] - - [:button {:class (stl/css :hero-btn) - :on-click on-click} - (tr "dashboard.access-tokens.create")]])) - -(mf/defc access-token-actions - [{:keys [on-delete]}] - (let [local (mf/use-state {:menu-open false}) - show? (:menu-open @local) - options (mf/with-memo [on-delete] - [{:name (tr "labels.delete") - :id "access-token-delete" - :handler on-delete}]) - - menu-ref (mf/use-ref) - - on-menu-close - (mf/use-fn #(swap! local assoc :menu-open false)) - - on-menu-click - (mf/use-fn - (fn [event] - (dom/prevent-default event) - (swap! local assoc :menu-open true))) - - on-keydown - (mf/use-fn - (mf/deps on-menu-click) - (fn [event] - (when (kbd/enter? event) - (dom/stop-propagation event) - (on-menu-click event))))] - - [:button {:class (stl/css :menu-btn) - :tab-index "0" - :ref menu-ref - :on-click on-menu-click - :on-key-down on-keydown} - menu-icon - [:> context-menu* - {:on-close on-menu-close - :show show? - :fixed true - :min-width true - :top "auto" - :left "auto" - :options options}]])) - -(mf/defc access-token-item - {::mf/wrap [mf/memo]} - [{:keys [token] :as props}] - (let [expires-at (:expires-at token) - expires-txt (some-> expires-at (ct/format-inst "PPP")) - expired? (and (some? expires-at) (> (ct/now) expires-at)) - - delete-fn - (mf/use-fn - (mf/deps token) - (fn [] - (let [params {:id (:id token)} - mdata {:on-success #(st/emit! (du/fetch-access-tokens))}] - (st/emit! (du/delete-access-token (with-meta params mdata)))))) - - on-delete - (mf/use-fn - (mf/deps delete-fn) - (fn [] - (st/emit! (modal/show - {:type :confirm - :title (tr "modals.delete-acces-token.title") - :message (tr "modals.delete-acces-token.message") - :accept-label (tr "modals.delete-acces-token.accept") - :on-accept delete-fn}))))] - - [:div {:class (stl/css :table-row)} - [:div {:class (stl/css :table-field :field-name)} - (str (:name token))] - - [:div {:class (stl/css-case :expiration-date true - :expired expired?)} - (cond - (nil? expires-at) (tr "dashboard.access-tokens.no-expiration") - expired? (tr "dashboard.access-tokens.expired-on" expires-txt) - :else (tr "dashboard.access-tokens.expires-on" expires-txt))] - [:div {:class (stl/css :table-field :actions)} - [:& access-token-actions - {:on-delete on-delete}]]])) - -(mf/defc access-tokens-page - [] - (let [tokens (mf/deref tokens-ref)] - (mf/with-effect [] - (dom/set-html-title (tr "title.settings.access-tokens")) - (st/emit! (du/fetch-access-tokens))) - - [:div {:class (stl/css :dashboard-access-tokens)} - [:& access-tokens-hero] - (if (empty? tokens) - [:div {:class (stl/css :access-tokens-empty)} - [:div (tr "dashboard.access-tokens.empty.no-access-tokens")] - [:div (tr "dashboard.access-tokens.empty.add-one")]] - [:div {:class (stl/css :dashboard-table)} - [:div {:class (stl/css :table-rows)} - (for [token tokens] - [:& access-token-item {:token token :key (:id token)}])]])])) - diff --git a/frontend/src/app/main/ui/settings/access_tokens.scss b/frontend/src/app/main/ui/settings/access_tokens.scss deleted file mode 100644 index 5e9f139765..0000000000 --- a/frontend/src/app/main/ui/settings/access_tokens.scss +++ /dev/null @@ -1,202 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. -// -// Copyright (c) KALEIDOS INC - -@use "refactor/common-refactor.scss" as deprecated; - -// ACCESS TOKENS PAGE -.dashboard-access-tokens { - display: grid; - grid-template-rows: auto 1fr; - margin: deprecated.$s-80 auto deprecated.$s-120 auto; - gap: deprecated.$s-32; - width: deprecated.$s-800; -} - -// hero -.access-tokens-hero { - display: grid; - grid-template-rows: auto auto 1fr; - gap: deprecated.$s-32; - width: deprecated.$s-500; - font-size: deprecated.$fs-14; - margin: deprecated.$s-16 auto 0 auto; -} - -.hero-title { - @include deprecated.bigTitleTipography; - color: var(--title-foreground-color-hover); -} - -.hero-desc { - color: var(--title-foreground-color); - margin-bottom: 0; - font-size: deprecated.$fs-14; -} - -.hero-btn { - @extend .button-primary; -} - -// table empty -.access-tokens-empty { - display: grid; - place-items: center; - align-content: center; - height: deprecated.$s-156; - max-width: deprecated.$s-1000; - width: 100%; - padding: deprecated.$s-32; - border: deprecated.$s-1 solid var(--panel-border-color); - border-radius: deprecated.$br-8; - color: var(--dashboard-list-text-foreground-color); -} - -// Access tokens table -.dashboard-table { - height: fit-content; -} - -.table-rows { - display: grid; - grid-auto-rows: deprecated.$s-64; - gap: deprecated.$s-16; - width: 100%; - height: 100%; - max-width: deprecated.$s-1000; - margin-top: deprecated.$s-16; - color: var(--title-foreground-color); -} - -.table-row { - display: grid; - grid-template-columns: 43% 1fr auto; - align-items: center; - height: deprecated.$s-64; - width: 100%; - padding: 0 deprecated.$s-16; - border-radius: deprecated.$br-8; - background-color: var(--dashboard-list-background-color); - color: var(--dashboard-list-foreground-color); -} - -.field-name { - @include deprecated.textEllipsis; - display: grid; - width: 43%; - min-width: deprecated.$s-300; -} - -.expiration-date { - @include deprecated.flexCenter; - min-width: deprecated.$s-76; - width: fit-content; - height: deprecated.$s-24; - border-radius: deprecated.$br-8; - color: var(--dashboard-list-text-foreground-color); -} - -.expired { - @include deprecated.headlineSmallTypography; - padding: 0 deprecated.$s-6; - color: var(--pill-foreground-color); - background-color: var(--status-widget-background-color-warning); -} - -.actions { - position: relative; -} -.menu-icon { - @extend .button-icon; - stroke: var(--icon-foreground); -} - -.menu-btn { - @include deprecated.buttonStyle; -} - -// Create access token modal -.modal-overlay { - @extend .modal-overlay-base; -} - -.modal-container { - @extend .modal-container-base; - min-width: deprecated.$s-408; -} - -.modal-header { - margin-bottom: deprecated.$s-24; -} - -.modal-title { - @include deprecated.uppercaseTitleTipography; - color: var(--modal-title-foreground-color); -} -.modal-close-btn { - @extend .modal-close-btn-base; -} - -.modal-content { - @include deprecated.flexColumn; - gap: deprecated.$s-24; - @include deprecated.bodySmallTypography; - margin-bottom: deprecated.$s-24; -} - -.select-title { - @include deprecated.bodySmallTypography; - color: var(--modal-title-foreground-color); -} - -.custon-input-wrapper { - @include deprecated.flexRow; - border-radius: deprecated.$br-8; - height: deprecated.$s-32; - background-color: var(--input-background-color); -} - -.custom-input-token { - @extend .input-element; - @include deprecated.bodySmallTypography; - margin: 0; - flex-grow: 1; - &:focus { - outline: none; - border: deprecated.$s-1 solid var(--input-border-color-active); - } -} - -.token-value { - @include deprecated.textEllipsis; - @include deprecated.bodySmallTypography; - flex-grow: 1; -} - -.copy-btn { - @include deprecated.flexCenter; - @extend .button-secondary; - height: deprecated.$s-28; - width: deprecated.$s-28; -} - -.clipboard-icon { - @extend .button-icon-small; -} - -.token-created-info { - color: var(--modal-text-foreground-color); -} - -.action-buttons { - @extend .modal-action-btns; - button { - @extend .modal-accept-btn; - } -} - -.cancel-button { - @extend .modal-cancel-btn; -} diff --git a/frontend/src/app/main/ui/settings/integrations.cljs b/frontend/src/app/main/ui/settings/integrations.cljs new file mode 100644 index 0000000000..82f90c47d4 --- /dev/null +++ b/frontend/src/app/main/ui/settings/integrations.cljs @@ -0,0 +1,573 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.settings.integrations + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data :as d] + [app.common.schema :as sm] + [app.common.time :as ct] + [app.config :as cf] + [app.main.data.event :as ev] + [app.main.data.modal :as modal] + [app.main.data.notifications :as ntf] + [app.main.data.profile :as du] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.components.context-menu-a11y :refer [context-menu*]] + [app.main.ui.ds.buttons.button :refer [button*]] + [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] + [app.main.ui.ds.controls.input :refer [input*]] + [app.main.ui.ds.controls.switch :refer [switch*]] + [app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]] + [app.main.ui.ds.foundations.typography :as t] + [app.main.ui.ds.foundations.typography.heading :refer [heading*]] + [app.main.ui.ds.foundations.typography.text :refer [text*]] + [app.main.ui.ds.notifications.shared.notification-pill :refer [notification-pill*]] + [app.main.ui.ds.tooltip :refer [tooltip*]] + [app.main.ui.forms :as fc] + [app.util.clipboard :as clipboard] + [app.util.dom :as dom] + [app.util.forms :as fm] + [app.util.i18n :as i18n :refer [tr]] + [okulary.core :as l] + [rumext.v2 :as mf])) + +(def tokens-ref + (l/derived :access-tokens st/state)) + +(def token-created-ref + (l/derived :access-token-created st/state)) + +(def notification-timeout 7000) + +(def ^:private schema:form + [:map + [:name [::sm/text {:max 250}]] + [:expiration-date [::sm/text {:max 250}]]]) + +(def form-initial-data + {:name "" + :expiration-date "never"}) + +(mf/defc token-created* + {::mf/private true} + [{:keys [title]}] + (let [token-created (mf/deref token-created-ref) + + on-copy-to-clipboard + (mf/use-fn + (mf/deps token-created) + (fn [event] + (dom/prevent-default event) + (clipboard/to-clipboard (:token token-created)) + (st/emit! (ntf/show {:level :info + :type :toast + :content (tr "integrations.notification.success.copied") + :timeout notification-timeout}))))] + + [:div {:class (stl/css :modal-form)} + [:> text* {:as "h2" + :typography t/headline-large + :class (stl/css :color-primary)} + title] + + [:> notification-pill* {:level :info + :type :context} + (tr "integrations.info.non-recuperable")] + + [:div {:class (stl/css :modal-content)} + [:div {:class (stl/css :modal-token)} + [:> input* {:type "text" + :default-value (:token token-created "") + :read-only true}] + [:div {:class (stl/css :modal-token-button)} + [:> icon-button* {:variant "secondary" + :aria-label (tr "integrations.copy-token") + :on-click on-copy-to-clipboard + :icon i/clipboard}]]] + + [:> text* {:as "div" + :typography t/body-small + :class (stl/css :color-secondary)} + (if (:expires-at token-created) + (tr "integrations.token-will-expire" (ct/format-inst (:expires-at token-created) "PPP")) + (tr "integrations.token-will-not-expire"))]] + + [:div {:class (stl/css :modal-footer)} + [:> button* {:variant "secondary" + :on-click modal/hide!} + (tr "labels.close")]]])) + + +(mf/defc create-token* + {::mf/private true} + [{:keys [title info mcp-key? on-created]}] + (let [form (fm/use-form + :initial form-initial-data + :schema schema:form) + + on-error + (mf/use-fn + #(st/emit! (ntf/error (tr "errors.generic")) + (modal/hide))) + + on-success + (mf/use-fn + #(st/emit! (du/fetch-access-tokens) + (ntf/success (tr "integrations.notification.success.created")) + (on-created))) + + on-submit + (mf/use-fn + (fn [form] + (let [cdata (:clean-data @form) + mdata {:on-success (partial on-success form) + :on-error (partial on-error form)} + expiration (:expiration-date cdata) + params (cond-> {:name (:name cdata) + :perms (:perms cdata)} + (not= "never" expiration) (assoc :expiration expiration) + (true? mcp-key?) (assoc :type "mcp"))] + (st/emit! (du/create-access-token (with-meta params mdata))))))] + + [:> fc/form* {:form form + :class (stl/css :modal-form) + :on-submit on-submit} + + [:> text* {:as "h2" + :typography t/headline-large + :class (stl/css :color-primary)} + title] + + (when (some? info) + [:> notification-pill* {:level :info + :type :context} + info]) + + [:div {:class (stl/css :modal-content)} + [:> fc/form-input* {:type "text" + :auto-focus? true + :form form + :name :name + :label (tr "integrations.name.label") + :placeholder (tr "integrations.name.placeholder")}]] + + [:div {:class (stl/css :modal-content)} + [:> text* {:as "label" + :typography t/body-small + :for :expiration-date + :class (stl/css :color-primary)} + (tr "integrations.expiration-date.label")] + [:> fc/form-select* {:options [{:label (tr "integrations.expiration-never") :value "never" :id "never"} + {:label (tr "integrations.expiration-30-days") :value "720h" :id "720h"} + {:label (tr "integrations.expiration-60-days") :value "1440h" :id "1440h"} + {:label (tr "integrations.expiration-90-days") :value "2160h" :id "2160h"} + {:label (tr "integrations.expiration-180-days") :value "4320h" :id "4320h"}] + :default-selected "never" + :name :expiration-date}]] + + [:div {:class (stl/css :modal-footer)} + [:> button* {:variant "secondary" + :on-click modal/hide!} + (tr "labels.cancel")] + [:> fc/form-submit* {:variant "primary"} + title]]])) + +(mf/defc create-access-token-modal + {::mf/register modal/components + ::mf/register-as :create-access-token} + [] + (let [created? (mf/use-state false) + + on-close + (mf/use-fn + (fn [] + (reset! created? false) + (st/emit! (modal/hide)))) + + on-created + (mf/use-fn + #(reset! created? true))] + + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css :modal-container)} + [:div {:class (stl/css :modal-close-button)} + [:> icon-button* {:variant "ghost" + :aria-label (tr "labels.close") + :on-click on-close + :icon i/close}]] + + (if @created? + [:> token-created* {:title (tr "integrations.create-access-token.title.created")}] + [:> create-token* {:title (tr "integrations.create-access-token.title") + :on-created on-created}])]])) + +(mf/defc create-mcp-key-modal + {::mf/register modal/components + ::mf/register-as :create-mcp-key} + [] + (let [created? (mf/use-state false) + + on-close + (mf/use-fn + (fn [] + (reset! created? false) + (st/emit! (modal/hide)))) + + on-created + (mf/use-fn + (fn [] + (st/emit! (du/update-profile-props {:mcp-status true}) + (ev/event {::ev/name "create-mcp-key" + ::ev/origin "integrations"}) + (ev/event {::ev/name "enable-mcp" + ::ev/origin "integrations" + :source "key-creation"})) + (reset! created? true)))] + + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css :modal-container)} + [:div {:class (stl/css :modal-close-button)} + [:> icon-button* {:variant "ghost" + :aria-label (tr "labels.close") + :on-click on-close + :icon i/close}]] + + (if @created? + [:> token-created* {:title (tr "integrations.create-mcp-key.title.created")}] + [:> create-token* {:title (tr "integrations.create-mcp-key.title") + :mcp-key? true + :on-created on-created}])]])) + +(mf/defc regenerate-mcp-key-modal + {::mf/register modal/components + ::mf/register-as :regenerate-mcp-key} + [] + (let [created? (mf/use-state false) + + tokens (mf/deref tokens-ref) + mcp-key (some #(when (= (:type %) "mcp") %) tokens) + mcp-key-id (:id mcp-key) + + on-close + (mf/use-fn + (fn [] + (reset! created? false) + (st/emit! (modal/hide)))) + + on-created + (mf/use-fn + (fn [] + (st/emit! (du/delete-access-token {:id mcp-key-id}) + (du/update-profile-props {:mcp-status true}) + (ev/event {::ev/name "regenerate-mcp-key" + ::ev/origin "integrations"})) + (reset! created? true)))] + + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css :modal-container)} + [:div {:class (stl/css :modal-close-button)} + [:> icon-button* {:variant "ghost" + :aria-label (tr "labels.close") + :on-click on-close + :icon i/close}]] + + (if @created? + [:> token-created* {:title (tr "integrations.regenerate-mcp-key.title.created")}] + [:> create-token* {:title (tr "integrations.regenerate-mcp-key.title") + :info (tr "integrations.regenerate-mcp-key.info") + :mcp-key? true + :on-created on-created}])]])) + +(mf/defc token-item* + {::mf/private true + ::mf/wrap [mf/memo]} + [{:keys [name expires-at on-delete]}] + (let [expires-txt (some-> expires-at (ct/format-inst "PPP")) + expired? (and (some? expires-at) (> (ct/now) expires-at)) + + menu-open* (mf/use-state false) + menu-open? (deref menu-open*) + + handle-menu-close + (mf/use-fn + #(reset! menu-open* false)) + + handle-menu-click + (mf/use-fn + #(reset! menu-open* (not menu-open?))) + + handle-open-confirm-modal + (mf/use-fn + (mf/deps on-delete) + (fn [] + (st/emit! (modal/show {:type :confirm + :title (tr "integrations.delete-token.title") + :message (tr "integrations.delete-token.message") + :accept-label (tr "integrations.delete-token.accept") + :on-accept on-delete})))) + + options + (mf/with-memo [on-delete] + [{:name (tr "labels.delete") + :id "token-delete" + :handler handle-open-confirm-modal}])] + + [:div {:class (stl/css :item)} + [:> text* {:as "div" + :typography t/body-medium + :title name + :class (stl/css :item-title)} + name] + + [:> text* {:as "div" + :typography t/body-small + :class (stl/css-case :item-subtitle true + :warning expired?)} + (cond + (nil? expires-at) (tr "integrations.no-expiration") + expired? (tr "integrations.expired-on" expires-txt) + :else (tr "integrations.expires-on" expires-txt))] + + [:div {:class (stl/css :item-actions)} + [:> icon-button* {:variant "ghost" + :class (stl/css :item-button) + :aria-pressed menu-open? + :aria-label (tr "labels.options") + :on-click handle-menu-click + :icon i/menu}] + [:> context-menu* {:on-close handle-menu-close + :show menu-open? + :min-width true + :top -10 + :left -138 + :options options}]]])) + +(mf/defc mcp-server-section* + {::mf/private true} + [] + (let [tokens (mf/deref tokens-ref) + profile (mf/deref refs/profile) + + mcp-key (some #(when (= (:type %) "mcp") %) tokens) + mcp-active? (d/nilv (-> profile :props :mcp-status) false) + + expires-at (:expires-at mcp-key) + expired? (and (some? expires-at) (> (ct/now) expires-at)) + + tooltip-id + (mf/use-id) + + handle-mcp-status-change + (mf/use-fn + (fn [mcp-status] + (st/emit! (du/update-profile-props {:mcp-status mcp-status}) + (ntf/show {:level :info + :type :toast + :content (if (true? mcp-status) + (tr "integrations.notification.success.mcp-server-enabled") + (tr "integrations.notification.success.mcp-server-disabled")) + :timeout notification-timeout}) + (ev/event {::ev/name (if (true? mcp-status) "enable-mcp" "disable-mcp") + ::ev/origin "integrations" + :source "toggle"})))) + + handle-initial-mcp-status + (mf/use-fn + #(st/emit! (modal/show {:type :create-mcp-key}))) + + handle-regenerate-mcp-key + (mf/use-fn + #(st/emit! (modal/show {:type :regenerate-mcp-key}))) + + handle-delete + (mf/use-fn + (mf/deps mcp-key) + (fn [] + (let [params {:id (:id mcp-key)} + mdata {:on-success #(st/emit! (du/fetch-access-tokens))}] + (st/emit! (du/delete-access-token (with-meta params mdata)) + (du/update-profile-props {:mcp-status false}))))) + + on-copy-to-clipboard + (mf/use-fn + (fn [event] + (dom/prevent-default event) + (clipboard/to-clipboard cf/mcp-server-url) + (st/emit! (ntf/show {:level :info + :type :toast + :content (tr "integrations.notification.success.copied-link") + :timeout notification-timeout}) + (ev/event {::ev/name "copy-mcp-url" + ::ev/origin "integrations"}))))] + + [:section {:class (stl/css :mcp-server-section)} + [:div + [:div {:class (stl/css :title)} + [:> heading* {:level 2 + :typography t/title-medium + :class (stl/css :color-primary :mcp-server-title)} + (tr "integrations.mcp-server.title")] + [:> text* {:as "span" + :typography t/body-small + :class (stl/css :beta)} + (tr "integrations.mcp-server.title.beta")]] + + [:> text* {:as "div" + :typography t/body-medium + :class (stl/css :color-secondary)} + (tr "integrations.mcp-server.description")]] + + [:div + [:> text* {:as "h3" + :typography t/headline-small + :class (stl/css :color-primary)} + (tr "integrations.mcp-server.status")] + + [:div {:class (stl/css :mcp-server-block)} + (when expired? + [:> notification-pill* {:level :error + :type :context} + [:div {:class (stl/css :mcp-server-notification)} + [:> text* {:as "div" + :typography t/body-medium + :class (stl/css :color-primary)} + (tr "integrations.mcp-server.status.expired.0")] + + [:> text* {:as "div" + :typography t/body-medium + :class (stl/css :color-primary)} + (tr "integrations.mcp-server.status.expired.1")]]]) + + [:div {:class (stl/css :mcp-server-switch)} + [:> switch* {:label (if mcp-active? + (tr "integrations.mcp-server.status.enabled") + (tr "integrations.mcp-server.status.disabled")) + :default-checked mcp-active? + :on-change handle-mcp-status-change}] + (when (and (false? mcp-active?) (nil? mcp-key)) + [:div {:class (stl/css :mcp-server-switch-cover) + :on-click handle-initial-mcp-status}])]]] + + (when (some? mcp-key) + [:div {:class (stl/css :mcp-server-key)} + [:> text* {:as "h3" + :typography t/headline-small + :class (stl/css :color-primary)} + (tr "integrations.mcp-server.mcp-keys.title")] + + [:div {:class (stl/css :mcp-server-block)} + [:div {:class (stl/css :mcp-server-regenerate)} + [:> button* {:variant "primary" + :class (stl/css :fit-content) + :on-click handle-regenerate-mcp-key} + (tr "integrations.mcp-server.mcp-keys.regenerate")] + [:> tooltip* {:content (tr "integrations.mcp-server.mcp-keys.tootip") + :id tooltip-id} + [:> icon* {:icon-id i/info + :class (stl/css :color-secondary)}]]] + + [:div {:class (stl/css :list)} + [:> token-item* {:key (:id mcp-key) + :name (:name mcp-key) + :expires-at (:expires-at mcp-key) + :on-delete handle-delete}]]]]) + + [:> notification-pill* {:level :default + :type :context} + [:div {:class (stl/css :mcp-server-notification)} + [:> text* {:as "div" + :typography t/body-medium + :class (stl/css :color-secondary)} + (tr "integrations.mcp-server.mcp-keys.info")] + [:div {:class (stl/css :mcp-server-notification-line)} + [:> text* {:as "div" + :typography t/body-medium + :class (stl/css :color-primary)} + cf/mcp-server-url] + [:> text* {:as "div" + :typography t/body-medium + :on-click on-copy-to-clipboard + :class (stl/css :mcp-server-notification-link)} + [:> icon* {:icon-id i/clipboard}] (tr "integrations.mcp-server.mcp-keys.copy")]] + + [:> text* {:as "div" + :typography t/body-medium + :class (stl/css :color-secondary)} + [:a {:href cf/mcp-help-center-uri + :class (stl/css :mcp-server-notification-link)} + (tr "integrations.mcp-server.mcp-keys.help") [:> icon* {:icon-id i/open-link}]]]]]])) + +(mf/defc access-tokens-section* + {::mf/private true} + [] + (let [tokens (mf/deref tokens-ref) + + handle-click + (mf/use-fn + #(st/emit! (modal/show {:type :create-access-token}))) + + handle-delete + (mf/use-fn + (fn [token-id] + (let [params {:id token-id} + mdata {:on-success #(st/emit! (du/fetch-access-tokens))}] + (st/emit! (du/delete-access-token (with-meta params mdata))))))] + + [:section {:class (stl/css :access-tokens-section)} + [:> heading* {:level 2 + :typography t/title-medium + :class (stl/css :color-primary)} + (tr "integrations.access-tokens.personal")] + + [:> text* {:as "div" + :typography t/body-medium + :class (stl/css :color-secondary)} + (tr "integrations.access-tokens.personal.description")] + + [:> button* {:variant "primary" + :class (stl/css :fit-content) + :on-click handle-click} + (tr "integrations.access-tokens.create")] + + (if (empty? tokens) + [:div {:class (stl/css :frame)} + [:> text* {:as "div" + :typography t/body-medium + :class (stl/css :color-secondary :text-center)} + [:div (tr "integrations.access-tokens.empty.no-access-tokens")] + [:div (tr "integrations.access-tokens.empty.add-one")]]] + + [:div {:class (stl/css :list)} + (for [token tokens] + (when (nil? (:type token)) + [:> token-item* {:key (:id token) + :name (:name token) + :expires-at (:expires-at token) + :on-delete (partial handle-delete (:id token))}]))])])) + +(mf/defc integrations-page* + [] + (mf/with-effect [] + (dom/set-html-title (tr "title.settings.integrations")) + (st/emit! (du/fetch-access-tokens))) + + [:div {:class (stl/css :integrations)} + [:> heading* {:level 1 + :typography t/title-large + :class (stl/css :color-primary)} + (tr "integrations.title")] + + (when (contains? cf/flags :mcp) + [:> mcp-server-section*]) + + (when (and (contains? cf/flags :mcp) + (contains? cf/flags :access-tokens)) + [:hr {:class (stl/css :separator)}]) + + (when (contains? cf/flags :access-tokens) + [:> access-tokens-section*])]) diff --git a/frontend/src/app/main/ui/settings/integrations.scss b/frontend/src/app/main/ui/settings/integrations.scss new file mode 100644 index 0000000000..00ddf72231 --- /dev/null +++ b/frontend/src/app/main/ui/settings/integrations.scss @@ -0,0 +1,221 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "refactor/common-refactor.scss" as deprecated; + +@use "ds/_borders.scss" as *; +@use "ds/_sizes.scss" as *; +@use "ds/spacing.scss" as *; +@use "ds/mixins.scss" as *; + +.color-primary { + color: var(--color-foreground-primary); +} + +.color-secondary { + color: var(--color-foreground-secondary); +} + +.text-center { + text-align: center; +} + +.fit-content { + inline-size: fit-content; +} + +.beta { + color: var(--color-accent-primary); + border: $b-1 solid var(--color-accent-primary); + inline-size: fit-content; + padding: var(--sp-xxs) var(--sp-s); + border-radius: $br-4; +} + +.title { + display: flex; + flex-direction: row; + align-items: baseline; + gap: var(--sp-s); +} + +.modal-overlay { + @extend .modal-overlay-base; +} + +.modal-container { + @extend .modal-container-base; + inline-size: $sz-400; + position: relative; +} + +.modal-content { + display: flex; + flex-direction: column; + gap: var(--sp-xs); +} + +.modal-form { + display: flex; + flex-direction: column; + gap: var(--sp-xxxl); +} + +.modal-close-button { + position: absolute; + top: var(--sp-s); + right: var(--sp-s); +} + +.modal-footer { + display: flex; + justify-content: right; + gap: var(--sp-s); +} + +.modal-token { + position: relative; +} + +.modal-token-button { + position: absolute; + top: 0; + right: 0; + border-start-start-radius: 0; + border-end-start-radius: 0; +} + +.integrations { + display: grid; + grid-template-rows: auto 1fr; + margin: $sz-88 auto $sz-120 auto; + gap: $sz-32; + inline-size: $sz-500; +} + +.access-tokens-section { + display: grid; + grid-template-rows: auto auto 1fr; + gap: var(--sp-m); +} + +.mcp-server-section { + display: flex; + flex-direction: column; + gap: var(--sp-l); +} + +.mcp-server-key { + display: flex; + flex-direction: column; +} + +.mcp-server-notification { + display: flex; + flex-direction: column; + gap: var(--sp-s); +} + +.mcp-server-notification-line { + display: flex; + flex-direction: row; + align-items: center; + gap: var(--sp-m); +} + +.mcp-server-notification-link { + cursor: pointer; + color: var(--color-accent-primary); + display: flex; + flex-direction: row; + align-items: center; + gap: var(--sp-xs); +} + +.mcp-server-title { + margin: var(--sp-s) 0; +} + +.mcp-server-block { + display: flex; + flex-direction: column; + gap: var(--sp-l); +} + +.mcp-server-regenerate { + display: flex; + align-items: center; + gap: var(--sp-s); +} + +.mcp-server-switch { + position: relative; +} + +.mcp-server-switch-cover { + position: absolute; + inset-block: 0; + inset-inline: 0; +} + +.separator { + border: $b-1 solid var(--color-background-quaternary); + margin: var(--sp-s) 0; +} + +.frame { + border: $b-1 solid var(--color-background-quaternary); + padding: var(--sp-m); + border-radius: $br-8; +} + +.list { + display: grid; + grid-auto-rows: $sz-64; + gap: var(--sp-m); +} + +.item { + display: grid; + grid-template-columns: 45% 1fr auto; + align-items: center; + background-color: var(--color-background-tertiary); + border-radius: $br-8; +} + +.item-title { + @include textEllipsis; + align-content: center; + block-size: $sz-64; + padding: 0 var(--sp-l); + color: var(--color-foreground-primary); +} + +.item-subtitle { + align-content: center; + block-size: $sz-64; + color: var(--color-foreground-secondary); + + &.warning { + padding: var(--sp-s) var(--sp-m); + block-size: fit-content; + inline-size: fit-content; + color: var(--color-foreground-primary); + background-color: var(--color-background-warning); + border: $b-1 solid var(--color-accent-warning); + border-radius: $br-8; + } +} + +.item-actions { + position: relative; +} + +.item-button { + block-size: $sz-64; + inline-size: $sz-48; + border-radius: 0 var(--sp-s) var(--sp-s) 0; +} diff --git a/frontend/src/app/main/ui/settings/sidebar.cljs b/frontend/src/app/main/ui/settings/sidebar.cljs index 0808e2299d..49ffcb6d19 100644 --- a/frontend/src/app/main/ui/settings/sidebar.cljs +++ b/frontend/src/app/main/ui/settings/sidebar.cljs @@ -43,8 +43,8 @@ (def ^:private go-settings-subscription #(st/emit! (rt/nav :settings-subscription))) -(def ^:private go-settings-access-tokens - #(st/emit! (rt/nav :settings-access-tokens))) +(def ^:private go-settings-integrations + #(st/emit! (rt/nav :settings-integrations))) (def ^:private go-settings-notifications #(st/emit! (rt/nav :settings-notifications))) @@ -66,7 +66,7 @@ options? (= section :settings-options) feedback? (= section :settings-feedback) subscription? (= section :settings-subscription) - access-tokens? (= section :settings-access-tokens) + integrations? (= section :settings-integrations) notifications? (= section :settings-notifications) team-id (or (dtm/get-last-team-id) (:default-team-id profile)) @@ -115,12 +115,13 @@ :data-testid "settings-subscription"} [:span {:class (stl/css :element-title)} (tr "subscription.labels")]]) - (when (contains? cf/flags :access-tokens) - [:li {:class (stl/css-case :current access-tokens? + (when (or (contains? cf/flags :access-tokens) + (contains? cf/flags :mcp)) + [:li {:class (stl/css-case :current integrations? :settings-item true) - :on-click go-settings-access-tokens - :data-testid "settings-access-tokens"} - [:span {:class (stl/css :element-title)} (tr "labels.access-tokens")]]) + :on-click go-settings-integrations + :data-testid "settings-integrations"} + [:span {:class (stl/css :element-title)} (tr "labels.integrations")]]) [:hr {:class (stl/css :sidebar-separator)}] diff --git a/frontend/translations/en.po b/frontend/translations/en.po index fea252fdc4..3f0bf8df6f 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -338,77 +338,6 @@ msgstr "You're going to restore %s." msgid "dashboard-restore-file-confirmation.title" msgstr "Restore file" -#: src/app/main/ui/settings/access_tokens.cljs:103 -msgid "dashboard.access-tokens.copied-success" -msgstr "Copied token" - -#: src/app/main/ui/settings/access_tokens.cljs:189 -msgid "dashboard.access-tokens.create" -msgstr "Generate new token" - -#: src/app/main/ui/settings/access_tokens.cljs:64 -msgid "dashboard.access-tokens.create.success" -msgstr "Access token created successfully." - -#: src/app/main/ui/settings/access_tokens.cljs:286 -msgid "dashboard.access-tokens.empty.add-one" -msgstr "Press the button \"Generate new token\" to generate one." - -#: src/app/main/ui/settings/access_tokens.cljs:285 -msgid "dashboard.access-tokens.empty.no-access-tokens" -msgstr "You have no tokens so far." - -#: src/app/main/ui/settings/access_tokens.cljs:135 -msgid "dashboard.access-tokens.expiration-180-days" -msgstr "180 days" - -#: src/app/main/ui/settings/access_tokens.cljs:132 -msgid "dashboard.access-tokens.expiration-30-days" -msgstr "30 days" - -#: src/app/main/ui/settings/access_tokens.cljs:133 -msgid "dashboard.access-tokens.expiration-60-days" -msgstr "60 days" - -#: src/app/main/ui/settings/access_tokens.cljs:134 -msgid "dashboard.access-tokens.expiration-90-days" -msgstr "90 days" - -#: src/app/main/ui/settings/access_tokens.cljs:131 -msgid "dashboard.access-tokens.expiration-never" -msgstr "Never" - -#: src/app/main/ui/settings/access_tokens.cljs:268 -msgid "dashboard.access-tokens.expired-on" -msgstr "Expired on %s" - -#: src/app/main/ui/settings/access_tokens.cljs:269 -msgid "dashboard.access-tokens.expires-on" -msgstr "Expires on %s" - -#: src/app/main/ui/settings/access_tokens.cljs:267 -msgid "dashboard.access-tokens.no-expiration" -msgstr "No expiration date" - -#: src/app/main/ui/settings/access_tokens.cljs:184 -msgid "dashboard.access-tokens.personal" -msgstr "Personal access tokens" - -#: src/app/main/ui/settings/access_tokens.cljs:185 -msgid "dashboard.access-tokens.personal.description" -msgstr "" -"Personal access tokens function like an alternative to our login/password " -"authentication system and can be used to allow an application to access the " -"internal Penpot API" - -#: src/app/main/ui/settings/access_tokens.cljs:142 -msgid "dashboard.access-tokens.token-will-expire" -msgstr "The token will expire on %s" - -#: src/app/main/ui/settings/access_tokens.cljs:143 -msgid "dashboard.access-tokens.token-will-not-expire" -msgstr "The token has no expiration date" - #: src/app/main/ui/dashboard/placeholder.cljs:41 msgid "dashboard.add-file" msgstr "Add file" @@ -2123,6 +2052,209 @@ msgstr "Resolved value:" msgid "inspect.tabs.styles.variants-panel" msgstr "Variant Properties" +#: src/app/main/ui/settings/integrations.cljs:189 +msgid "integrations.access-tokens.create" +msgstr "Create new access token" + +#: src/app/main/ui/settings/integrations.cljs:286 +msgid "integrations.access-tokens.empty.add-one" +msgstr "Press the button \"Create new access token\" to generate one." + +#: src/app/main/ui/settings/integrations.cljs:285 +msgid "integrations.access-tokens.empty.no-access-tokens" +msgstr "You have no tokens so far." + +#: src/app/main/ui/settings/integrations.cljs:184 +msgid "integrations.access-tokens.personal" +msgstr "Personal access tokens" + +#: src/app/main/ui/settings/integrations.cljs:185 +msgid "integrations.access-tokens.personal.description" +msgstr "" +"Personal access tokens function like an alternative to our login/password " +"authentication system and can be used to allow an application to access the " +"internal Penpot API" + +#: src/app/main/ui/settings/integrations.cljs:152, src/app/main/ui/settings/integrations.cljs:158 +msgid "integrations.copy-token" +msgstr "Copy token" + +#: src/app/main/ui/settings/integrations.cljs:432 +msgid "integrations.create-access-token.title" +msgstr "Create access token" + +#: src/app/main/ui/settings/integrations.cljs:433 +msgid "integrations.create-access-token.title.created" +msgstr "Access token created" + +#: src/app/main/ui/settings/integrations.cljs:290 +msgid "integrations.create-mcp-key.title" +msgstr "Create new MCP key" + +#: src/app/main/ui/settings/integrations.cljs:291 +msgid "integrations.create-mcp-key.title.created" +msgstr "MCP key created" + +#: src/app/main/ui/settings/integrations.cljs:257 +msgid "integrations.delete-token.accept" +msgstr "Delete token" + +#: src/app/main/ui/settings/integrations.cljs:256 +msgid "integrations.delete-token.message" +msgstr "Are you sure you want to delete this token?" + +#: src/app/main/ui/settings/integrations.cljs:255 +msgid "integrations.delete-token.title" +msgstr "Delete token" + +#: src/app/main/ui/settings/integrations.cljs:135 +msgid "integrations.expiration-180-days" +msgstr "180 days" + +#: src/app/main/ui/settings/integrations.cljs:132 +msgid "integrations.expiration-30-days" +msgstr "30 days" + +#: src/app/main/ui/settings/integrations.cljs:133 +msgid "integrations.expiration-60-days" +msgstr "60 days" + +#: src/app/main/ui/settings/integrations.cljs:134 +msgid "integrations.expiration-90-days" +msgstr "90 days" + +#: src/app/main/ui/settings/integrations.cljs:131 +msgid "integrations.expiration-never" +msgstr "Never" + +#: src/app/main/ui/settings/integrations.cljs:268 +msgid "integrations.expired-on" +msgstr "Expired on %s" + +#: src/app/main/ui/settings/integrations.cljs:269 +msgid "integrations.expires-on" +msgstr "Expires on %s" + +#: src/app/main/ui/settings/integrations.cljs:267 +msgid "integrations.no-expiration" +msgstr "No expiration date" + +#: src/app/main/ui/settings/integrations.cljs:130 +msgid "integrations.expiration-date.label" +msgstr "Expiration date" + +#: src/app/main/ui/settings/integrations.cljs:131 +msgid "integrations.info.non-recuperable" +msgstr "This unique token is non-recuperable. If you lose it, you will need to create a new one." + +#: src/app/main/ui/settings/integrations.cljs:336 +msgid "integrations.mcp-server.title" +msgstr "MCP Server" + +#: src/app/main/ui/settings/integrations.cljs:336 +msgid "integrations.mcp-server.title.beta" +msgstr "Beta" + +#: src/app/main/ui/settings/integrations.cljs:347 +msgid "integrations.mcp-server.description" +msgstr "The Penpot MCP Server enables MCP clients to interact directly with Penpot design files." + +#: src/app/main/ui/settings/integrations.cljs:353 +msgid "integrations.mcp-server.status" +msgstr "Status" + +#: src/app/main/ui/settings/integrations.cljs:370 +msgid "integrations.mcp-server.status.disabled" +msgstr "Disabled" + +#: src/app/main/ui/settings/integrations.cljs:370 +msgid "integrations.mcp-server.status.enabled" +msgstr "Enabled" + +#: src/app/main/ui/settings/integrations.cljs:363 +msgid "integrations.mcp-server.status.expired.0" +msgstr "The MCP key used to connect to the MCP server has expired. As a result, the connection cannot be established." + +#: src/app/main/ui/settings/integrations.cljs:368 +msgid "integrations.mcp-server.status.expired.1" +msgstr "Please regenerate the MCP key and update your client configuration with the new key." + +#: src/app/main/ui/settings/integrations.cljs:415 +msgid "integrations.mcp-server.mcp-keys.copy" +msgstr "Copy link" + +#: src/app/main/ui/settings/integrations.cljs:422 +msgid "integrations.mcp-server.mcp-keys.help" +msgstr "How to configure MCP clients" + +#: src/app/main/ui/settings/integrations.cljs:405 +msgid "integrations.mcp-server.mcp-keys.info" +msgstr "This is the server url you'll need to configure your MCP client in order to connect it to the Penpot MCP server." + +#: src/app/main/ui/settings/integrations.cljs:387 +msgid "integrations.mcp-server.mcp-keys.regenerate" +msgstr "Regenerate MCP keys" + +#: src/app/main/ui/settings/integrations.cljs:381 +msgid "integrations.mcp-server.mcp-keys.title" +msgstr "MCP keys" + +#: src/app/main/ui/settings/integrations.cljs:388 +msgid "integrations.mcp-server.mcp-keys.tootip" +msgstr "The MCP key is needed for the MCP client set up" + +#: src/app/main/ui/settings/integrations.cljs:124 +msgid "integrations.name.label" +msgstr "Name" + +#: src/app/main/ui/settings/integrations.cljs:126 +msgid "integrations.name.placeholder" +msgstr "The name can help to know what's the token for" + +#: src/app/main/ui/settings/integrations.cljs:103 +msgid "integrations.notification.success.copied" +msgstr "Copied token" + +#: src/app/main/ui/settings/integrations.cljs:64 +msgid "integrations.notification.success.created" +msgstr "Token created successfully" + +#: src/app/main/ui/settings/integrations.cljs:327 +msgid "integrations.notification.success.copied-link" +msgstr "Link copied to clipboard" + +#: src/app/main/ui/settings/integrations.cljs:293 +msgid "integrations.notification.success.mcp-server-disabled" +msgstr "MCP server disabled" + +#: src/app/main/ui/settings/integrations.cljs:299 +msgid "integrations.notification.success.mcp-server-enabled" +msgstr "MCP server enabled" + +#: src/app/main/ui/settings/integrations.cljs:317 +msgid "integrations.regenerate-mcp-key.info" +msgstr "Regenerating the key will immediately revoke the current one. Any application using it will stop working." + +#: src/app/main/ui/settings/integrations.cljs:317 +msgid "integrations.regenerate-mcp-key.title" +msgstr "Regenerate MCP key" + +#: src/app/main/ui/settings/integrations.cljs:318 +msgid "integrations.regenerate-mcp-key.title.created" +msgstr "MCP key regenerated" + +#: src/app/main/ui/settings/integrations.cljs:480 +msgid "integrations.title" +msgstr "Integrations" + +#: src/app/main/ui/settings/integrations.cljs:142 +msgid "integrations.token-will-expire" +msgstr "The token will expire on %s" + +#: src/app/main/ui/settings/integrations.cljs:143 +msgid "integrations.token-will-not-expire" +msgstr "The token has no expiration date" + #: src/app/main/ui/dashboard/comments.cljs:96 msgid "label.mark-all-as-read" msgstr "Mark all as read" @@ -2474,6 +2606,10 @@ msgstr "Info" msgid "labels.installed-fonts" msgstr "Installed fonts" +#: src/app/main/ui/settings/sidebar.cljs:123 +msgid "labels.integrations" +msgstr "Integrations" + #: src/app/main/ui/static.cljs:396 msgid "labels.internal-error.desc-message-first" msgstr "Something bad happened." @@ -3134,30 +3270,6 @@ msgstr "Change email" msgid "modals.change-email.title" msgstr "Change your email" -#: src/app/main/ui/settings/access_tokens.cljs:152, src/app/main/ui/settings/access_tokens.cljs:158 -msgid "modals.create-access-token.copy-token" -msgstr "Copy token" - -#: src/app/main/ui/settings/access_tokens.cljs:130 -msgid "modals.create-access-token.expiration-date.label" -msgstr "Expiration date" - -#: src/app/main/ui/settings/access_tokens.cljs:124 -msgid "modals.create-access-token.name.label" -msgstr "Name" - -#: src/app/main/ui/settings/access_tokens.cljs:126 -msgid "modals.create-access-token.name.placeholder" -msgstr "The name can help to know what's the token for" - -#: src/app/main/ui/settings/access_tokens.cljs:178 -msgid "modals.create-access-token.submit-label" -msgstr "Create token" - -#: src/app/main/ui/settings/access_tokens.cljs:111 -msgid "modals.create-access-token.title" -msgstr "Generate access token" - #: src/app/main/ui/dashboard/team.cljs:1127 msgid "modals.create-webhook.submit-label" msgstr "Create webhook" @@ -3174,18 +3286,6 @@ msgstr "Payload URL" msgid "modals.create-webhook.url.placeholder" msgstr "https://example.com/postreceive" -#: src/app/main/ui/settings/access_tokens.cljs:257 -msgid "modals.delete-acces-token.accept" -msgstr "Delete token" - -#: src/app/main/ui/settings/access_tokens.cljs:256 -msgid "modals.delete-acces-token.message" -msgstr "Are you sure you want to delete this token?" - -#: src/app/main/ui/settings/access_tokens.cljs:255 -msgid "modals.delete-acces-token.title" -msgstr "Delete token" - #: src/app/main/ui/settings/delete_account.cljs:56 msgid "modals.delete-account.cancel" msgstr "Cancel and keep my account" @@ -5079,14 +5179,14 @@ msgstr "Shared Libraries - %s - Penpot" msgid "title.default" msgstr "Penpot - Design Freedom for Teams" -#: src/app/main/ui/settings/access_tokens.cljs:278 -msgid "title.settings.access-tokens" -msgstr "Profile - Access tokens" - #: src/app/main/ui/settings/feedback.cljs:161 msgid "title.settings.feedback" msgstr "Give feedback - Penpot" +#: src/app/main/ui/settings/integrations.cljs:278 +msgid "title.settings.integrations" +msgstr "Integrations - Penpot" + #: src/app/main/ui/settings/notifications.cljs:45 msgid "title.settings.notifications" msgstr "Notifications - Penpot" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index a06b5ee301..f8b055587a 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -347,77 +347,6 @@ msgstr "Vas a restaurar %s." msgid "dashboard-restore-file-confirmation.title" msgstr "Restaurar archivo" -#: src/app/main/ui/settings/access_tokens.cljs:103 -msgid "dashboard.access-tokens.copied-success" -msgstr "Token copiado" - -#: src/app/main/ui/settings/access_tokens.cljs:189 -msgid "dashboard.access-tokens.create" -msgstr "Generar nuevo token" - -#: src/app/main/ui/settings/access_tokens.cljs:64 -msgid "dashboard.access-tokens.create.success" -msgstr "Access token creado con éxito." - -#: src/app/main/ui/settings/access_tokens.cljs:286 -msgid "dashboard.access-tokens.empty.add-one" -msgstr "Pulsa el botón \"Generar nuevo token\" para generar uno." - -#: src/app/main/ui/settings/access_tokens.cljs:285 -msgid "dashboard.access-tokens.empty.no-access-tokens" -msgstr "Todavía no tienes ningún token." - -#: src/app/main/ui/settings/access_tokens.cljs:135 -msgid "dashboard.access-tokens.expiration-180-days" -msgstr "180 días" - -#: src/app/main/ui/settings/access_tokens.cljs:132 -msgid "dashboard.access-tokens.expiration-30-days" -msgstr "30 días" - -#: src/app/main/ui/settings/access_tokens.cljs:133 -msgid "dashboard.access-tokens.expiration-60-days" -msgstr "60 días" - -#: src/app/main/ui/settings/access_tokens.cljs:134 -msgid "dashboard.access-tokens.expiration-90-days" -msgstr "90 días" - -#: src/app/main/ui/settings/access_tokens.cljs:131 -msgid "dashboard.access-tokens.expiration-never" -msgstr "Nunca" - -#: src/app/main/ui/settings/access_tokens.cljs:268 -msgid "dashboard.access-tokens.expired-on" -msgstr "Expiró el %s" - -#: src/app/main/ui/settings/access_tokens.cljs:269 -msgid "dashboard.access-tokens.expires-on" -msgstr "Expira el %s" - -#: src/app/main/ui/settings/access_tokens.cljs:267 -msgid "dashboard.access-tokens.no-expiration" -msgstr "Sin fecha de expiración" - -#: src/app/main/ui/settings/access_tokens.cljs:184 -msgid "dashboard.access-tokens.personal" -msgstr "Access tokens personales" - -#: src/app/main/ui/settings/access_tokens.cljs:185 -msgid "dashboard.access-tokens.personal.description" -msgstr "" -"Los access tokens personales funcionan como una alternativa a nuestro " -"sistema de autenticación usuario/password y se pueden usar para permitir a " -"otras aplicaciones acceso a la API interna de Penpot" - -#: src/app/main/ui/settings/access_tokens.cljs:142 -msgid "dashboard.access-tokens.token-will-expire" -msgstr "El token expirará el %s" - -#: src/app/main/ui/settings/access_tokens.cljs:143 -msgid "dashboard.access-tokens.token-will-not-expire" -msgstr "El token no tiene fecha de expiración" - #: src/app/main/ui/dashboard/placeholder.cljs:41 msgid "dashboard.add-file" msgstr "Añadir archivo" @@ -2094,6 +2023,209 @@ msgstr "Valor resuelto:" msgid "inspect.tabs.styles.variants-panel" msgstr "Propiedades de las variantes" +#: src/app/main/ui/settings/integrations.cljs:189 +msgid "integrations.access-tokens.create" +msgstr "Crear nuevo token de acceso" + +#: src/app/main/ui/settings/integrations.cljs:286 +msgid "integrations.access-tokens.empty.add-one" +msgstr "Pulsa el botón \"Crear nuevo token de accesso\" para generar uno." + +#: src/app/main/ui/settings/integrations.cljs:285 +msgid "integrations.access-tokens.empty.no-access-tokens" +msgstr "Todavía no tienes ningún token." + +#: src/app/main/ui/settings/integrations.cljs:184 +msgid "integrations.access-tokens.personal" +msgstr "Tokens de acceso personales" + +#: src/app/main/ui/settings/integrations.cljs:185 +msgid "integrations.access-tokens.personal.description" +msgstr "" +"Los tokens de accesso personales funcionan como una alternativa a nuestro " +"sistema de autenticación usuario/password y se pueden usar para permitir a " +"otras aplicaciones acceso a la API interna de Penpot" + +#: src/app/main/ui/settings/integrations.cljs:152, src/app/main/ui/settings/integrations.cljs:158 +msgid "integrations.copy-token" +msgstr "Copiar token" + +#: src/app/main/ui/settings/integrations.cljs:432 +msgid "integrations.create-access-token.title" +msgstr "Crear token de accesso" + +#: src/app/main/ui/settings/integrations.cljs:433 +msgid "integrations.create-access-token.title.created" +msgstr "Token de acceso creado" + +#: src/app/main/ui/settings/integrations.cljs:290 +msgid "integrations.create-mcp-key.title" +msgstr "Crear nueva clave MCP" + +#: src/app/main/ui/settings/integrations.cljs:291 +msgid "integrations.create-mcp-key.title.created" +msgstr "Clave MCP creada" + +#: src/app/main/ui/settings/integrations.cljs:257 +msgid "integrations.delete-token.accept" +msgstr "Borrar token" + +#: src/app/main/ui/settings/integrations.cljs:256 +msgid "integrations.delete-token.message" +msgstr "¿Seguro que deseas borrar este token?" + +#: src/app/main/ui/settings/integrations.cljs:255 +msgid "integrations.delete-token.title" +msgstr "Borrar token" + +#: src/app/main/ui/settings/integrations.cljs:135 +msgid "integrations.expiration-180-days" +msgstr "180 días" + +#: src/app/main/ui/settings/integrations.cljs:132 +msgid "integrations.expiration-30-days" +msgstr "30 días" + +#: src/app/main/ui/settings/integrations.cljs:133 +msgid "integrations.expiration-60-days" +msgstr "60 días" + +#: src/app/main/ui/settings/integrations.cljs:134 +msgid "integrations.expiration-90-days" +msgstr "90 días" + +#: src/app/main/ui/settings/integrations.cljs:131 +msgid "integrations.expiration-never" +msgstr "Nunca" + +#: src/app/main/ui/settings/integrations.cljs:268 +msgid "integrations.expired-on" +msgstr "Expiró el %s" + +#: src/app/main/ui/settings/integrations.cljs:269 +msgid "integrations.expires-on" +msgstr "Expira el %s" + +#: src/app/main/ui/settings/integrations.cljs:267 +msgid "integrations.no-expiration" +msgstr "Sin fecha de expiración" + +#: src/app/main/ui/settings/integrations.cljs:130 +msgid "integrations.expiration-date.label" +msgstr "Fecha de expiración" + +#: src/app/main/ui/settings/integrations.cljs:131 +msgid "integrations.info.non-recuperable" +msgstr "Esta clave única no es recuperable. Si la pierdes, tendrás que crear una nueva." + +#: src/app/main/ui/settings/integrations.cljs:336 +msgid "integrations.mcp-server.title" +msgstr "Servidor MCP" + +#: src/app/main/ui/settings/integrations.cljs:336 +msgid "integrations.mcp-server.title.beta" +msgstr "Beta" + +#: src/app/main/ui/settings/integrations.cljs:347 +msgid "integrations.mcp-server.description" +msgstr "El servidor MCP de Penpot permite que los clientes MCP interactúen directamente con los archivos de diseño de Penpot." + +#: src/app/main/ui/settings/integrations.cljs:353 +msgid "integrations.mcp-server.status" +msgstr "Estado" + +#: src/app/main/ui/settings/integrations.cljs:370 +msgid "integrations.mcp-server.status.enabled" +msgstr "Habilitado" + +#: src/app/main/ui/settings/integrations.cljs:370 +msgid "integrations.mcp-server.status.disabled" +msgstr "Deshabilitado" + +#: src/app/main/ui/settings/integrations.cljs:363 +msgid "integrations.mcp-server.status.expired.0" +msgstr "La clave MCP utilizada para conectarse al servidor MCP ha expirado. Como resultado, no se puede establecer la conexión." + +#: src/app/main/ui/settings/integrations.cljs:368 +msgid "integrations.mcp-server.status.expired.1" +msgstr "Por favor, regenera la clave MCP y actualiza la configuración de tu cliente con la nueva clave." + +#: src/app/main/ui/settings/integrations.cljs:415 +msgid "integrations.mcp-server.mcp-keys.copy" +msgstr "Copiar enlace" + +#: src/app/main/ui/settings/integrations.cljs:422 +msgid "integrations.mcp-server.mcp-keys.help" +msgstr "Cómo configurar clientes MCP" + +#: src/app/main/ui/settings/integrations.cljs:405 +msgid "integrations.mcp-server.mcp-keys.info" +msgstr "Esta es la URL del servidor que necesitarás configurar en tu cliente MCP para conectarlo al servidor MCP de Penpot." + +#: src/app/main/ui/settings/integrations.cljs:387 +msgid "integrations.mcp-server.mcp-keys.regenerate" +msgstr "Regenerar clave MCP" + +#: src/app/main/ui/settings/integrations.cljs:381 +msgid "integrations.mcp-server.mcp-keys.title" +msgstr "Claves MCP" + +#: src/app/main/ui/settings/integrations.cljs:388 +msgid "integrations.mcp-server.mcp-keys.tootip" +msgstr "La clave MCP es necesaria para la configuración del cliente MCP" + +#: src/app/main/ui/settings/integrations.cljs:124 +msgid "integrations.name.label" +msgstr "Nombre" + +#: src/app/main/ui/settings/integrations.cljs:126 +msgid "integrations.name.placeholder" +msgstr "El nombre te pude ayudar a saber para qué se utiliza el token" + +#: src/app/main/ui/settings/integrations.cljs:103 +msgid "integrations.notification.success.copied" +msgstr "Token copiado" + +#: src/app/main/ui/settings/integrations.cljs:64 +msgid "integrations.notification.success.created" +msgstr "Token creado con éxito" + +#: src/app/main/ui/settings/integrations.cljs:327 +msgid "integrations.notification.success.copied-link" +msgstr "Enlace copiado al portapapeles" + +#: src/app/main/ui/settings/integrations.cljs:293 +msgid "integrations.notification.success.mcp-server-disabled" +msgstr "Servidor MCP deshabilitado" + +#: src/app/main/ui/settings/integrations.cljs:299 +msgid "integrations.notification.success.mcp-server-enabled" +msgstr "Servidor MCP habilitado" + +#: src/app/main/ui/settings/integrations.cljs:317 +msgid "integrations.regenerate-mcp-key.info" +msgstr "Regenerar la clave revocará inmediatamente la actual. Cualquier aplicación que la esté utilizando dejará de funcionar." + +#: src/app/main/ui/settings/integrations.cljs:317 +msgid "integrations.regenerate-mcp-key.title" +msgstr "Regenerar clave MCP" + +#: src/app/main/ui/settings/integrations.cljs:318 +msgid "integrations.regenerate-mcp-key.title.created" +msgstr "Clave MCP regenerada" + +#: src/app/main/ui/settings/integrations.cljs:480 +msgid "integrations.title" +msgstr "Integraciones" + +#: src/app/main/ui/settings/integrations.cljs:142 +msgid "integrations.token-will-expire" +msgstr "El token expirará el %s" + +#: src/app/main/ui/settings/integrations.cljs:143 +msgid "integrations.token-will-not-expire" +msgstr "El token no tiene fecha de expiración" + #: src/app/main/ui/dashboard/comments.cljs:96 msgid "label.mark-all-as-read" msgstr "Marcar todo como leído" @@ -2445,6 +2577,10 @@ msgstr "Información" msgid "labels.installed-fonts" msgstr "Fuentes instaladas" +#: src/app/main/ui/settings/sidebar.cljs:123 +msgid "labels.integrations" +msgstr "Integraciones" + #: src/app/main/ui/static.cljs:396 msgid "labels.internal-error.desc-message-first" msgstr "Ha ocurrido algo extraño." @@ -3101,30 +3237,6 @@ msgstr "Cambiar correo" msgid "modals.change-email.title" msgstr "Cambiar tu correo" -#: src/app/main/ui/settings/access_tokens.cljs:152, src/app/main/ui/settings/access_tokens.cljs:158 -msgid "modals.create-access-token.copy-token" -msgstr "Copiar token" - -#: src/app/main/ui/settings/access_tokens.cljs:130 -msgid "modals.create-access-token.expiration-date.label" -msgstr "Fecha de expiración" - -#: src/app/main/ui/settings/access_tokens.cljs:124 -msgid "modals.create-access-token.name.label" -msgstr "Nombre" - -#: src/app/main/ui/settings/access_tokens.cljs:126 -msgid "modals.create-access-token.name.placeholder" -msgstr "El nombre te pude ayudar a saber para qué se utiliza el token" - -#: src/app/main/ui/settings/access_tokens.cljs:178 -msgid "modals.create-access-token.submit-label" -msgstr "Crear token" - -#: src/app/main/ui/settings/access_tokens.cljs:111 -msgid "modals.create-access-token.title" -msgstr "Generar access token" - #: src/app/main/ui/dashboard/team.cljs:1127 msgid "modals.create-webhook.submit-label" msgstr "Crear webhook" @@ -3141,18 +3253,6 @@ msgstr "Payload URL" msgid "modals.create-webhook.url.placeholder" msgstr "https://example.com/postreceive" -#: src/app/main/ui/settings/access_tokens.cljs:257 -msgid "modals.delete-acces-token.accept" -msgstr "Borrar token" - -#: src/app/main/ui/settings/access_tokens.cljs:256 -msgid "modals.delete-acces-token.message" -msgstr "¿Seguro que deseas borrar este token?" - -#: src/app/main/ui/settings/access_tokens.cljs:255 -msgid "modals.delete-acces-token.title" -msgstr "Borrar token" - #: src/app/main/ui/settings/delete_account.cljs:56 msgid "modals.delete-account.cancel" msgstr "Cancelar y mantener mi cuenta" @@ -5062,14 +5162,14 @@ msgstr "Bibliotecas Compartidas - %s - Penpot" msgid "title.default" msgstr "Penpot - Diseño Libre para Equipos" -#: src/app/main/ui/settings/access_tokens.cljs:278 -msgid "title.settings.access-tokens" -msgstr "Perfil - Access tokens" - #: src/app/main/ui/settings/feedback.cljs:161 msgid "title.settings.feedback" msgstr "Danos tu opinión - Penpot" +#: src/app/main/ui/settings/integrations.cljs:278 +msgid "title.settings.integrations" +msgstr "Integraciones - Penpot" + #: src/app/main/ui/settings/notifications.cljs:45 msgid "title.settings.notifications" msgstr "Notificaciones - Penpot"