diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 540a8306..423b5af2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,9 @@ jobs: - name: Setup CI Environment uses: yetanalytics/action-setup-env@v2 + - name: Install Chromium + uses: browser-actions/setup-chrome@v1 + - name: Cache Deps uses: actions/cache@v4 with: diff --git a/resources/sass/global.scss b/resources/sass/global.scss index b5d28eab..3a7ba398 100644 --- a/resources/sass/global.scss +++ b/resources/sass/global.scss @@ -905,15 +905,15 @@ span.close-alert img { } @media only screen and (max-width: 1124px) { - .accordion .accordion-container .accordion-header > div:nth-child(2) { + .accordion .accordion-container > div:nth-child(2) { padding-left: 20px; } } - /************************************************** # Action Icon List **************************************************/ + .action-row { display: table; width: 100%; @@ -939,7 +939,7 @@ span.close-alert img { .action-icon-list li { display: inline-block; vertical-align: top; - margin: 0 0 18px 24px; + margin: 0 12px 18px 0; padding: 0; } @@ -1075,7 +1075,7 @@ a.icon-copy:hover { } .api-key-col { - @extend .col-sm-12, .col-lg-6; + @extend .col-sm-8, .col-lg-4; } .api-key-col-header { @@ -1108,6 +1108,30 @@ input.key-display { vertical-align: top; } +// Credential Label Editor + +input.label-editor { + margin-bottom: 8px; +} + +// Credential Roles Editor + +ul.role-select { + padding-left: 0; + margin-bottom: 8px; +} + +ul.role-select li.role-checkbox { + vertical-align: top; + margin: 0px; + padding: 0px; + list-style-type: none; +} + +.role-checkbox label { + padding-left: 4px; +} + /************************************************** # Accounts ***************************************************/ @@ -1125,7 +1149,7 @@ input.key-display { } .account-col { - @extend .api-key-col; + @extend .col-sm-12, .col-lg-6; } .account-col-header { @@ -1251,27 +1275,6 @@ div.browser-filters { white-space: nowrap; } -/************************************************** -# Roles -**************************************************/ - -ul.role-select { - margin: 0px; - padding: 0px; -} - -ul.role-select li.role-checkbox { - vertical-align: top; - margin: 0px; - padding: 0px; - list-style-type: none; -} - -.role-checkbox label { - padding-left: 4px; -} - - /************************************************** # Tooltip **************************************************/ diff --git a/src/com/yetanalytics/lrs_admin_ui/db.cljs b/src/com/yetanalytics/lrs_admin_ui/db.cljs index d6246bc1..6f970093 100644 --- a/src/com/yetanalytics/lrs_admin_ui/db.cljs +++ b/src/com/yetanalytics/lrs_admin_ui/db.cljs @@ -26,6 +26,7 @@ (s/def :credential/api-key string?) (s/def :credential/secret-key string?) +(s/def :credential/label (s/nilable (s/and string? not-empty))) (s/def :credential/scope string?) (s/def :credential/scopes (s/every :credential/scope)) @@ -33,6 +34,7 @@ (s/def ::credential (s/keys :req-un [:credential/api-key :credential/secret-key + :credential/label :credential/scopes])) (s/def ::credentials diff --git a/src/com/yetanalytics/lrs_admin_ui/language.cljs b/src/com/yetanalytics/lrs_admin_ui/language.cljs index 6224ff3c..5636b00f 100644 --- a/src/com/yetanalytics/lrs_admin_ui/language.cljs +++ b/src/com/yetanalytics/lrs_admin_ui/language.cljs @@ -32,12 +32,15 @@ :credentials.tenant.number {:en-US "Number of Credentials: "} :credentials.tenant.key {:en-US "Api Key"} :credentials.key.aria {:en-US "Show/Hide Api Key Details"} - :credentials.key.permissions {:en-US "Permissions "} + :credentials.key.label {:en-US "Label:"} + :credentials.key.permissions {:en-US "Permissions:"} :credentials.key.secret {:en-US "API Key Secret"} :credentials.key.hide {:en-US "Hide"} :credentials.key.show {:en-US "Show Secret Key"} :credentials.key.permissions.save {:en-US "Save"} :credentials.key.permissions.cancel {:en-US "Cancel"} + :credentials.key.actions {:en-US "Actions"} + :credentials.key.edit-credential {:en-US "Edit Credential"} :credentials.key.edit {:en-US "Edit"} :credentials.key.delete {:en-US "Delete"} :credentials.key.delete.confirm {:en-US "Are you sure?"} diff --git a/src/com/yetanalytics/lrs_admin_ui/views/accounts.cljs b/src/com/yetanalytics/lrs_admin_ui/views/accounts.cljs index c4092d1f..25849143 100644 --- a/src/com/yetanalytics/lrs_admin_ui/views/accounts.cljs +++ b/src/com/yetanalytics/lrs_admin_ui/views/accounts.cljs @@ -27,11 +27,11 @@ :on-click #(do (dispatch [:accounts/delete-account account]) (swap! delete-confirm not)) :class "confirm-delete"} - "Yes"] + "Yes"] ; TODO: :lang/get [:a {:href "#!" :on-click #(swap! delete-confirm not) :class "confirm-delete"} - "No"]] + "No"]] ; TODO: :lang/get [:li [:a {:href "#!" :on-click #(swap! delete-confirm not) diff --git a/src/com/yetanalytics/lrs_admin_ui/views/credentials/api_key.cljs b/src/com/yetanalytics/lrs_admin_ui/views/credentials/api_key.cljs index a94ebca4..a15d376c 100644 --- a/src/com/yetanalytics/lrs_admin_ui/views/credentials/api_key.cljs +++ b/src/com/yetanalytics/lrs_admin_ui/views/credentials/api_key.cljs @@ -1,126 +1,166 @@ (ns com.yetanalytics.lrs-admin-ui.views.credentials.api-key (:require + [clojure.string :as cstr] [reagent.core :as r] [re-frame.core :as re-frame :refer [dispatch subscribe]] [com.yetanalytics.lrs-admin-ui.functions.copy :refer [copy-text]] [com.yetanalytics.lrs-admin-ui.functions.scopes :refer [scope-list has-scope? toggle-scope]] - [com.yetanalytics.lrs-admin-ui.functions :refer [ps-event]])) + [com.yetanalytics.lrs-admin-ui.functions :refer [ps-event ps-event-val]])) + +(defn- api-key-row + [{:keys [api-key label scopes] :as _credential} expanded] + [:div {:class "api-key-row" + :aria-label @(subscribe [:lang/get :notification.key.aria]) + :on-click #(swap! expanded not)} + ;; API Key View + [:div {:class "api-key-col"} + [:span {:class (str "collapse-sign" + (when @expanded " expanded"))} + [:input {:value api-key + :class "key-display" + :read-only true}] + [copy-text + {:text api-key + :on-copy #(dispatch [:notification/notify false + @(subscribe [:lang/get :notification.credentials.key-copied])])} + [:a {:class "icon-copy" + :on-click #(ps-event %)}]]]] + ;; Label + [:div {:class "api-key-col"} + (str + @(subscribe [:lang/get :credentials.key.label]) + " " + (or label "(None)"))] + ;; Permissions + [:div {:class "api-key-col"} + (str + @(subscribe [:lang/get :credentials.key.permissions]) + " " + (cstr/join ", " scopes))]]) + +(defn- api-key-expand-edit + [idx {:keys [label scopes] :as credential} edit] + [:div {:class "action-row"} + ;; Label editor + [:input + {:class "label-editor round" + :value label + :placeholder "Add Label" + :on-change (fn [e] + (let [new-label (not-empty (ps-event-val e)) + new-cred (assoc credential :label new-label)] + (dispatch [:credentials/update-credential idx new-cred])))}] + ;; Scope selector + [:ul {:class "role-select"} + (map (fn [scope] + [:li {:class "role-checkbox" + :key (str "permission-check-" + idx "-" scope)} + [:input {:type "checkbox", + :name "scopes", + :value scope + :checked (has-scope? scopes scope) + :on-change #(dispatch [:credentials/update-credential + idx + (assoc credential :scopes + (toggle-scope scopes scope))])}] + [:label {:for "scopes"} (str " " scope)]]) + scope-list)] + [:ul {:class "action-icon-list"} + ;; Save button + [:li + [:a {:href "#!", + :on-click (fn [] + (swap! edit not) + (dispatch [:credentials/save-credential credential])) + :class "icon-save"} + @(subscribe [:lang/get :credentials.key.permissions.save])]] + ;; Cancel button + [:li + [:a {:href "#!", + :on-click (fn [] + (swap! edit not) + (dispatch [:credentials/load-credentials])) + :class "icon-close"} + @(subscribe [:lang/get :credentials.key.permissions.cancel])]]]]) + +(defn- api-key-expand-view + [_credential edit] + (let [delete-confirm (r/atom false)] + (fn [credential _edit] + [:div {:class "action-row"} + [:ul {:class "action-icon-list"} + [:li + [:a {:href "#!", + :on-click #(swap! edit not) + :class "icon-edit"} @(subscribe [:lang/get :credentials.key.edit])]] + (if @delete-confirm + [:li + [:span @(subscribe [:lang/get :credentials.key.delete.confirm])] + [:a {:href "#!", + :on-click #(do (dispatch [:credentials/delete-credential credential]) + (swap! delete-confirm not)) + :class "confirm-delete"} + "Yes"] ; TODO: :lang/get + [:a {:href "#!" + :on-click #(swap! delete-confirm not) + :class "confirm-delete"} + "No"]] ; TODO: :lang/get + [:li + [:a {:href "#!" + :on-click #(swap! delete-confirm not) + :class "icon-delete"} + @(subscribe [:lang/get :credentials.key.delete])]])]]))) + +(defn- api-key-expand-secret-key + [{:keys [secret-key] :as _credential}] + [:div {:class "action-label-wide"} + [:span + [:input {:value secret-key + :class "key-display" + :read-only true}] + [copy-text + {:text secret-key + :on-copy #(dispatch [:notification/notify false + @(subscribe [:lang/get :notification.credentials.secret-copied])])} + [:a {:class "icon-copy pointer"}]]]]) + +(defn- api-key-expand + [_idx _credential] + (let [edit (r/atom false) + show-secret (r/atom false)] + (fn [idx credential] + [:div {:class "api-key-expand"} + [:div {:class "api-key-col"} + [:p {:class "api-key-col-header"} + @(subscribe [:lang/get :credentials.key.secret])] + [:div {:class "action-row"} + (when @show-secret + [api-key-expand-secret-key credential]) + [:ul {:class "action-icon-list"} + [:li + [:a {:href "#!", + :class "icon-secret" + :on-click #(swap! show-secret not)} + (str (cond + @show-secret @(subscribe [:lang/get :credentials.key.hide]) + :else @(subscribe [:lang/get :credentials.key.show])))]]]]] + [:div {:class "api-key-col"} + [:p {:class "api-key-col-header"} + (if @edit + @(subscribe [:lang/get :credentials.key.edit-credential]) + @(subscribe [:lang/get :credentials.key.actions]))] + (if @edit + [api-key-expand-edit idx credential edit] + [api-key-expand-view credential edit])]]))) (defn api-key - [{:keys [idx]}] - (let [expanded (r/atom false) - show-secret (r/atom false) - edit (r/atom false) - delete-confirm (r/atom false)] - (fn [] - (let [credential @(subscribe [:credentials/get-credential idx]) - scopes (:scopes credential) - scope-display (map-indexed (fn [idx scope] - [:span {:key (str "scope-display-" idx)} - (str (when (> idx 0) - ", ") - scope)]) - scopes)] + [_props] + (let [expanded (r/atom false)] + (fn [{:keys [idx]}] + (let [credential @(subscribe [:credentials/get-credential idx])] [:li {:class "mb-2"} [:div {:class "accordion-container"} - [:div {:class "api-key-row" - :aria-label @(subscribe [:lang/get :notification.key.aria]) - :on-click #(swap! expanded not)} - [:div {:class "api-key-col"} - [:span {:class (str "collapse-sign" - (when @expanded " expanded"))} - [:input {:value (:api-key credential) - :class "key-display" - :read-only true}] - [copy-text - {:text (:api-key credential) - :on-copy #(dispatch [:notification/notify false - @(subscribe [:lang/get :notification.credentials.key-copied])])} - [:a {:class "icon-copy" - :on-click #(ps-event %)}]]]] - [:div {:class "api-key-col"} @(subscribe [:lang/get :credentials.key.permissions]) scope-display]] + [api-key-row credential expanded] (when @expanded - [:div {:class "api-key-expand"} - [:div {:class "api-key-col"} - [:p {:class "api-key-col-header"} - @(subscribe [:lang/get :credentials.key.secret])] - [:div {:class "action-row"} - (when @show-secret - [:div {:class "action-label-wide"} - [:span - [:input {:value (:secret-key credential) - :class "key-display" - :read-only true}] - [copy-text - {:text (:secret-key credential) - :on-copy #(dispatch [:notification/notify false - @(subscribe [:lang/get :notification.credentials.secret-copied])])} - [:a {:class "icon-copy pointer"}]]]]) - [:ul {:class "action-icon-list"} - [:li - [:a {:href "#!", - :class "icon-secret" - :on-click #(swap! show-secret not)} - (str (cond - @show-secret @(subscribe [:lang/get :credentials.key.hide]) - :else @(subscribe [:lang/get :credentials.key.show])))]]]]] - [:div {:class "api-key-col"} - [:p {:class "api-key-col-header"} - @(subscribe [:lang/get :credentials.key.permissions])] - (cond - @edit - [:div {:class "action-row"} - [:ul {:class "role-select"} - (map (fn [scope] - [:li {:class "role-checkbox" - :key (str "permission-check-" - idx "-" scope)} - [:input {:type "checkbox", - :name "scopes", - :value scope - :checked (has-scope? scopes scope) - :on-change #(dispatch [:credentials/update-credential - idx - (assoc credential :scopes - (toggle-scope scopes scope))])}] - [:label {:for "scopes"} (str " " scope)]]) - scope-list)] - [:ul {:class "action-icon-list"} - [:li - [:a {:href "#!", - :on-click (fn [] - (swap! edit not) - (dispatch [:credentials/save-credential credential])) - :class "icon-save"} @(subscribe [:lang/get :credentials.key.permissions.save])]] - [:li - [:a {:href "#!", - :on-click (fn [] - (swap! edit not) - (dispatch [:credentials/load-credentials])) - :class "icon-close"} @(subscribe [:lang/get :credentials.key.permissions.cancel])]]]] - :else - [:div {:class "action-row"} - [:div {:class "action-label"} - scope-display] - [:ul {:class "action-icon-list"} - [:li - [:a {:href "#!", - :on-click #(swap! edit not) - :class "icon-edit"} @(subscribe [:lang/get :credentials.key.edit])]] - (if @delete-confirm - [:li - [:span @(subscribe [:lang/get :credentials.key.delete.confirm])] - [:a {:href "#!", - :on-click #(do (dispatch [:credentials/delete-credential credential]) - (swap! delete-confirm not)) - :class "confirm-delete"} - "Yes"] - [:a {:href "#!" - :on-click #(swap! delete-confirm not) - :class "confirm-delete"} - "No"]] - [:li - [:a {:href "#!" - :on-click #(swap! delete-confirm not) - :class "icon-delete"} - @(subscribe [:lang/get :credentials.key.delete])]])]])]])]])))) + [api-key-expand idx credential])]]))))