From 9526e33c3546833cc83cd0fcee1e9596f7c86d4d Mon Sep 17 00:00:00 2001 From: khaled basbous Date: Thu, 3 Jul 2025 16:55:26 +0200 Subject: [PATCH 01/10] fix(Groups-page): Show hierarchy next to group edition --- code/resources/public/ui/css/nuvla-ui.css | 4 + .../sixsq/nuvla/ui/pages/groups/views.cljs | 76 ++++++++++--------- .../src/cljs/sixsq/nuvla/ui/session/subs.cljs | 12 ++- 3 files changed, 55 insertions(+), 37 deletions(-) diff --git a/code/resources/public/ui/css/nuvla-ui.css b/code/resources/public/ui/css/nuvla-ui.css index cb65fec1d..4b75be54b 100644 --- a/code/resources/public/ui/css/nuvla-ui.css +++ b/code/resources/public/ui/css/nuvla-ui.css @@ -1253,3 +1253,7 @@ table.ui i.icon.sort.ascending, table.ui i.icon.sort.descending { opacity: 0.8; } + +.nuvla-group-item:hover { + background: rgba(0,0,0,0.05); +} \ No newline at end of file diff --git a/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs b/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs index a49c1b362..4c2c6445f 100644 --- a/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs +++ b/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs @@ -5,6 +5,7 @@ [reagent.core :as r] [sixsq.nuvla.ui.common-components.acl.views :as acl-views] [sixsq.nuvla.ui.common-components.i18n.subs :as i18n-subs] + [sixsq.nuvla.ui.common-components.plugins.full-text-search :as full-text-search-plugin] [sixsq.nuvla.ui.main.events :as main-events] [sixsq.nuvla.ui.pages.profile.events :as events] [sixsq.nuvla.ui.pages.profile.subs :as subs] @@ -166,53 +167,60 @@ ^{:key (random-uuid)} [GroupMembers @group]]))) +(def selected-group (r/atom nil)) + (defn Group [] (let [collapsed (r/atom true)] - (fn [{:keys [id name description children] :as _group}] - [ui/ListItem {:on-click #(do (swap! collapsed not) - (.stopPropagation %))} - [ui/ListIcon {:name "group"}] - [ui/ListContent - [ui/ListHeader (or name id)] - (when description [ui/ListDescription description]) + (fn [{:keys [id name children] :as _group}] + [ui/ListItem {:active true + :on-click #(do + (reset! selected-group id) + (.stopPropagation %)) + :style {:cursor :pointer}} + [ui/ListIcon {:style (cond-> {:padding 5 + :min-width "17px"} + (seq children) (assoc :cursor :pointer)) + :on-click #(do (swap! collapsed not) + (.stopPropagation %)) + :name (if (seq children) + (if @collapsed "angle right" "angle down") + "")}] + [ui/ListContent {:className "nuvla-group-item" + :style (cond-> {:padding 5 + :border-radius 5} + (= @selected-group id) (assoc :background-color "lightgray"))} + [ui/ListHeader (when (not= @selected-group id) {:style {:font-weight 400}}) + (or name id)] (when (and (not @collapsed) (seq children)) [ui/ListList - (for [child children] + (for [child (sort-by (juxt :id :name) children)] ^{:key (:id child)} [Group child])])]]))) (defn GroupHierarchySegment [] (let [groups-hierarchy @(subscribe [::session-subs/groups-hierarchies])] - [ui/Segment {:padded true - :color "purple"} - [ui/Header {:as :h2 :dividing true} "Group Hierarchy"] - [ui/ListSA {:celled true - :style {:cursor :pointer}} - (for [group-hierarchy groups-hierarchy] + [ui/Segment {:raised true :style {:min-height "100%"}} + [ui/Header {:as :h3} "Groups"] + [full-text-search-plugin/FullTextSearch + {:db-path [::deployments-search] + :change-event [:a] + :style {:width "100%"}}] + [ui/ListSA + (for [group-hierarchy (sort-by (juxt :id :name) groups-hierarchy)] ^{:key (:id group-hierarchy)} [Group group-hierarchy])]])) (defn GroupsViewPage [] - (let [tr (subscribe [::i18n-subs/tr]) - groups (subscribe [::session-subs/groups]) - is-group? (subscribe [::session-subs/is-group?]) - is-admin? (subscribe [::session-subs/is-admin?])] - (fn [] - (let [remove-groups #{"group/nuvla-nuvlabox" "group/nuvla-anon" "group/nuvla-user" - (when-not @is-admin? "group/nuvla-admin")} - sorted-groups (->> @groups - (remove (comp remove-groups :id)) - (sort-by :id))] - [:<> - (when @is-group? - [ui/GridColumn - [GroupMembersSegment]]) - [GroupHierarchySegment] - [ui/Segment {:padded true, :color "blue"} - [ui/Header {:as :h2} (str/capitalize (@tr [:groups]))] - (for [group sorted-groups] - ^{:key (str "group-" group)} - [GroupMembers group])]])))) + [ui/Grid {:columns 2} + [ui/GridColumn {:width 4 :stretched true :style {:background-color "light-gray" + :padding-right 0}} + [GroupHierarchySegment]] + [ui/GridColumn {:width 12 :stretched true :style {:background-color "light-gray" + :padding-right 0}} + [ui/Segment {:style {:min-height "100%"}} + (if @selected-group + [GroupMembers @(subscribe [::session-subs/group @selected-group])] + [:i "Select a group"])]]]) diff --git a/code/src/cljs/sixsq/nuvla/ui/session/subs.cljs b/code/src/cljs/sixsq/nuvla/ui/session/subs.cljs index 18299297f..b17e3cf49 100644 --- a/code/src/cljs/sixsq/nuvla/ui/session/subs.cljs +++ b/code/src/cljs/sixsq/nuvla/ui/session/subs.cljs @@ -219,14 +219,20 @@ :<- [::groups] (fn [groups] (->> groups - (map (juxt :id :name)) + (map (juxt :id identity)) (into {})))) +(reg-sub + ::group + :<- [::groups-mapping] + (fn [groups-mapping [_ id]] + (get groups-mapping id))) + (reg-sub ::groups-options :<- [::groups-mapping] (fn [groups-mapping] - (map (fn [[id name]] {:key id, :value id, :text (or name (utils/remove-group-prefix id))}) groups-mapping))) + (map (fn [[id {:keys [name]}]] {:key id, :value id, :text (or name (utils/remove-group-prefix id))}) groups-mapping))) (reg-sub ::resolve-principal @@ -237,7 +243,7 @@ (fn [[current-user-id identifier peers groups] [_ id]] (if (string? id) (if (str/starts-with? id "group/") - (or (get groups id) (utils/remove-group-prefix id)) + (or (get-in groups [id :name]) (utils/remove-group-prefix id)) (utils/resolve-user current-user-id identifier peers id)) id))) From 2a50310032001beb9f8cac24e5b8d75c39b2b8fe Mon Sep 17 00:00:00 2001 From: khaled basbous Date: Fri, 4 Jul 2025 13:57:22 +0200 Subject: [PATCH 02/10] wip members list --- .../sixsq/nuvla/ui/pages/groups/views.cljs | 222 ++++++++++-------- .../sixsq/nuvla/ui/utils/semantic_ui.cljs | 2 +- 2 files changed, 128 insertions(+), 96 deletions(-) diff --git a/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs b/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs index 4c2c6445f..8da587e39 100644 --- a/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs +++ b/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs @@ -29,20 +29,19 @@ [id principal members editable?] (let [principal-name (subscribe [::session-subs/resolve-principal principal])] [ui/ListItem + [ui/ListContent - [ui/ListHeader - [acl-views/PrincipalIcon principal] - utils-general/nbsp - @principal-name - utils-general/nbsp - (when editable? - [icons/CloseIcon {:link true - :size "small" - :color "red" - :on-click (fn [] - (reset! members (-> @members set (disj principal) vec)) - (dispatch [::main-events/changes-protection? true]) - (set-group-changed! id))}])]]])) + [ui/ListIcon {:name icons/i-user :size "large" :verticalAlign "middle"}] + @principal-name + utils-general/nbsp + (when editable? + [icons/CloseIcon {:link true + :size "small" + :color "red" + :on-click (fn [] + (reset! members (-> @members set (disj principal) vec)) + (dispatch [::main-events/changes-protection? true]) + (set-group-changed! id))}])]])) (defn DropdownPrincipals [_add-user _opts _members] @@ -82,75 +81,96 @@ add-user (r/atom nil)] (fn [group] (let [{:keys [id name description]} group] - [ui/Table {:columns 4} - [ui/TableHeader {:fullWidth true} - [ui/TableRow - [ui/TableHeaderCell - [ui/HeaderSubheader {:as :h3} - name " (" id ")"] - (when description [:p description])] - (when (and @acl editable?) - [ui/TableHeaderCell - [acl-views/AclButtonOnly {:default-value @acl - :read-only (not editable?) - :active? show-acl?}]])] - (when @show-acl? - [ui/TableRow - [ui/TableCell {:colSpan 4} - [acl-views/AclSection {:default-value @acl - :read-only (not editable?) - :active? show-acl? - :on-change #(do - (reset! acl %) - (set-group-changed! id) - (dispatch [::main-events/changes-protection? true]))}]]])] - [ui/TableBody - [ui/TableRow - [ui/TableCell - (if (empty? @members) - [uix/MsgNoItemsToShow [uix/TR (if editable? :empty-group-message - :empty-group-or-no-access-message)]] - [ui/ListSA - (for [m @members] - ^{:key m} - [GroupMember id m members editable?])])]] - (when editable? - [ui/TableRow - [ui/TableCell - [:div {:style {:display "flex"}} - [DropdownPrincipals - add-user - {:placeholder (@tr [:add-group-members]) - :fluid true} @members] - [:span utils-general/nbsp] - [uix/Button {:text (@tr [:add]) - :icon "add user" - :disabled (str/blank? @add-user) - :on-click #(do - (swap! members conj @add-user) - (reset! add-user nil) - (set-group-changed! id) - (dispatch [::main-events/changes-protection? true]))}] - [:span utils-general/nbsp] - [:span utils-general/nbsp] - [ui/Input {:placeholder (@tr [:invite-by-email]) - :style {:width "250px"} - :value (or @invite-user "") - :on-change (ui-callback/value #(reset! invite-user %))}] - [:span utils-general/nbsp] - [uix/Button {:text (@tr [:send]) - :icon "send" - :disabled (str/blank? @invite-user) - :on-click #(do - (dispatch [::events/invite-to-group id @invite-user]) - (reset! invite-user nil))}]]] - [ui/TableCell {:textAlign "right"} - [uix/Button {:primary true - :text (@tr [:save]) - :icon "save" - :disabled (not @changed?) - :on-click #(do (dispatch [::events/edit-group (assoc group :users @members, :acl @acl)]) - (disable-changes-protection! id))}]]])]])))) + [:<> + [ui/Header {:as :h3} + [icons/UserGroupIcon] + [ui/HeaderContent + (or name id) + [ui/HeaderSubheader description " (" id ")"]]] + [:div + + [ui/Button {:basic true :floated "right"} "Add Subgroup"]] + [ui/Header {:as :h3 :dividing true} "Members"] + (if (empty? @members) + [uix/MsgNoItemsToShow [uix/TR (if editable? :empty-group-message + :empty-group-or-no-access-message)]] + [ui/ListSA {:relaxed "true" :vertical-align "middle"} + (for [m @members] + ^{:key m} + [GroupMember id m members editable?])]) + [ui/Input {:placeholder (@tr [:invite-by-email]) + :style {:width "250px"} + :value (or @invite-user "") + :on-change (ui-callback/value #(reset! invite-user %))}] + + [ui/Table {:columns 4} + [ui/TableHeader {:fullWidth true} + [ui/TableRow + [ui/TableHeaderCell + [ui/HeaderSubheader {:as :h3} name]] + (when description [:p description]) + (when (and @acl editable?) + [ui/TableHeaderCell + [acl-views/AclButtonOnly {:default-value @acl + :read-only (not editable?) + :active? show-acl?}]])] + (when @show-acl? + [ui/TableRow + [ui/TableCell {:colSpan 4} + [acl-views/AclSection {:default-value @acl + :read-only (not editable?) + :active? show-acl? + :on-change #(do + (reset! acl %) + (set-group-changed! id) + (dispatch [::main-events/changes-protection? true]))}]]])] + [ui/TableBody + [ui/TableRow + [ui/TableCell + (if (empty? @members) + [uix/MsgNoItemsToShow [uix/TR (if editable? :empty-group-message + :empty-group-or-no-access-message)]] + [ui/ListSA + (for [m @members] + ^{:key m} + [GroupMember id m members editable?])])]] + (when editable? + [ui/TableRow + [ui/TableCell + [:div {:style {:display "flex"}} + [DropdownPrincipals + add-user + {:placeholder (@tr [:add-group-members]) + :fluid true} @members] + [:span utils-general/nbsp] + [uix/Button {:text (@tr [:add]) + :icon "add user" + :disabled (str/blank? @add-user) + :on-click #(do + (swap! members conj @add-user) + (reset! add-user nil) + (set-group-changed! id) + (dispatch [::main-events/changes-protection? true]))}] + [:span utils-general/nbsp] + [:span utils-general/nbsp] + [ui/Input {:placeholder (@tr [:invite-by-email]) + :style {:width "250px"} + :value (or @invite-user "") + :on-change (ui-callback/value #(reset! invite-user %))}] + [:span utils-general/nbsp] + [uix/Button {:text (@tr [:send]) + :icon "send" + :disabled (str/blank? @invite-user) + :on-click #(do + (dispatch [::events/invite-to-group id @invite-user]) + (reset! invite-user nil))}]]] + [ui/TableCell {:textAlign "right"} + [uix/Button {:primary true + :text (@tr [:save]) + :icon "save" + :disabled (not @changed?) + :on-click #(do (dispatch [::events/edit-group (assoc group :users @members, :acl @acl)]) + (disable-changes-protection! id))}]]])]]])))) (defn GroupMembersSegment [] @@ -173,7 +193,7 @@ [] (let [collapsed (r/atom true)] (fn [{:keys [id name children] :as _group}] - [ui/ListItem {:active true + [ui/ListItem {:active true :on-click #(do (reset! selected-group id) (.stopPropagation %)) @@ -186,11 +206,13 @@ :name (if (seq children) (if @collapsed "angle right" "angle down") "")}] - [ui/ListContent {:className "nuvla-group-item" - :style (cond-> {:padding 5 - :border-radius 5} - (= @selected-group id) (assoc :background-color "lightgray"))} - [ui/ListHeader (when (not= @selected-group id) {:style {:font-weight 400}}) + [ui/ListContent + [ui/ListHeader + {:className "nuvla-group-item" + :style (cond-> {:padding 5 + :border-radius 5} + (= @selected-group id) (assoc :background-color "lightgray") + (not= @selected-group id) (assoc :font-weight 400))} (or name id)] (when (and (not @collapsed) (seq children)) [ui/ListList @@ -201,7 +223,8 @@ (defn GroupHierarchySegment [] (let [groups-hierarchy @(subscribe [::session-subs/groups-hierarchies])] - [ui/Segment {:raised true :style {:min-height "100%"}} + [ui/Segment {:raised true :style {:overflow-x :auto + :min-height "100%"}} [ui/Header {:as :h3} "Groups"] [full-text-search-plugin/FullTextSearch {:db-path [::deployments-search] @@ -214,13 +237,22 @@ (defn GroupsViewPage [] - [ui/Grid {:columns 2} - [ui/GridColumn {:width 4 :stretched true :style {:background-color "light-gray" - :padding-right 0}} + [ui/Grid {:stackable false} + [ui/GridColumn {:stretched true + :computer 4 + :tablet 6 + :mobile 8 + :style {:background-color "light-gray" + :padding-right 0}} [GroupHierarchySegment]] - [ui/GridColumn {:width 12 :stretched true :style {:background-color "light-gray" - :padding-right 0}} - [ui/Segment {:style {:min-height "100%"}} + [ui/GridColumn {:stretched true + :tablet 10 + :computer 12 + :mobile 8 + :style {:background-color "light-gray" + :padding-right 0}} + [ui/Segment {:style {:min-height "100%" + :overflow-x :auto}} (if @selected-group [GroupMembers @(subscribe [::session-subs/group @selected-group])] [:i "Select a group"])]]]) diff --git a/code/src/cljs/sixsq/nuvla/ui/utils/semantic_ui.cljs b/code/src/cljs/sixsq/nuvla/ui/utils/semantic_ui.cljs index 8180e9f3f..b9603dbe8 100644 --- a/code/src/cljs/sixsq/nuvla/ui/utils/semantic_ui.cljs +++ b/code/src/cljs/sixsq/nuvla/ui/utils/semantic_ui.cljs @@ -96,7 +96,7 @@ (def Input (r/adapt-react-class semantic/Input)) (def Header (r/adapt-react-class semantic/Header)) -;;(def HeaderContent (r/adapt-react-class semantic/HeaderContent)) +(def HeaderContent (r/adapt-react-class semantic/HeaderContent)) (def HeaderSubheader (r/adapt-react-class semantic/HeaderSubheader)) (def Label (r/adapt-react-class semantic/Label)) From bad6c5c3d2be056009288bd633bc63f091b329a3 Mon Sep 17 00:00:00 2001 From: khaled basbous Date: Fri, 4 Jul 2025 14:20:15 +0200 Subject: [PATCH 03/10] hierarchy selection without css --- code/resources/public/ui/css/nuvla-ui.css | 4 -- .../sixsq/nuvla/ui/pages/groups/views.cljs | 52 +++++++++---------- 2 files changed, 26 insertions(+), 30 deletions(-) diff --git a/code/resources/public/ui/css/nuvla-ui.css b/code/resources/public/ui/css/nuvla-ui.css index 4b75be54b..cb65fec1d 100644 --- a/code/resources/public/ui/css/nuvla-ui.css +++ b/code/resources/public/ui/css/nuvla-ui.css @@ -1253,7 +1253,3 @@ table.ui i.icon.sort.ascending, table.ui i.icon.sort.descending { opacity: 0.8; } - -.nuvla-group-item:hover { - background: rgba(0,0,0,0.05); -} \ No newline at end of file diff --git a/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs b/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs index 8da587e39..b10285077 100644 --- a/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs +++ b/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs @@ -193,32 +193,32 @@ [] (let [collapsed (r/atom true)] (fn [{:keys [id name children] :as _group}] - [ui/ListItem {:active true - :on-click #(do - (reset! selected-group id) - (.stopPropagation %)) - :style {:cursor :pointer}} - [ui/ListIcon {:style (cond-> {:padding 5 - :min-width "17px"} - (seq children) (assoc :cursor :pointer)) - :on-click #(do (swap! collapsed not) + (let [selected? (= @selected-group id) + children? (boolean (seq children))] + [ui/ListItem {:on-click #(do + (reset! selected-group id) + (.stopPropagation %))} + [ui/ListIcon {:style {:padding 5 + :min-width "17px"} + :on-click #(when children? + (swap! collapsed not) (.stopPropagation %)) - :name (if (seq children) - (if @collapsed "angle right" "angle down") - "")}] - [ui/ListContent - [ui/ListHeader - {:className "nuvla-group-item" - :style (cond-> {:padding 5 - :border-radius 5} - (= @selected-group id) (assoc :background-color "lightgray") - (not= @selected-group id) (assoc :font-weight 400))} - (or name id)] - (when (and (not @collapsed) (seq children)) - [ui/ListList - (for [child (sort-by (juxt :id :name) children)] - ^{:key (:id child)} - [Group child])])]]))) + :name (if (seq children) + (if @collapsed "angle right" "angle down") + "")}] + [ui/ListContent + [ui/ListHeader + {:className "nuvla-group-item" + :style (cond-> {:padding 5 + :border-radius 5} + selected? (assoc :background-color "lightgray") + (not selected?) (assoc :font-weight 400))} + (or name id)] + (when (and (not @collapsed) (seq children)) + [ui/ListList + (for [child (sort-by (juxt :id :name) children)] + ^{:key (:id child)} + [Group child])])]])))) (defn GroupHierarchySegment [] @@ -230,7 +230,7 @@ {:db-path [::deployments-search] :change-event [:a] :style {:width "100%"}}] - [ui/ListSA + [ui/ListSA {:selection true} (for [group-hierarchy (sort-by (juxt :id :name) groups-hierarchy)] ^{:key (:id group-hierarchy)} [Group group-hierarchy])]])) From b41bdfe372042f40fe2be5fc5519a85eb3f9b4c2 Mon Sep 17 00:00:00 2001 From: khaled basbous Date: Fri, 4 Jul 2025 16:20:34 +0200 Subject: [PATCH 04/10] wip remove member --- .../sixsq/nuvla/ui/pages/groups/views.cljs | 268 ++++++++++-------- code/src/cljs/sixsq/nuvla/ui/utils/icons.cljs | 10 + 2 files changed, 154 insertions(+), 124 deletions(-) diff --git a/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs b/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs index b10285077..34bd76b02 100644 --- a/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs +++ b/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs @@ -10,6 +10,7 @@ [sixsq.nuvla.ui.pages.profile.events :as events] [sixsq.nuvla.ui.pages.profile.subs :as subs] [sixsq.nuvla.ui.session.subs :as session-subs] + [sixsq.nuvla.ui.utils.forms :as forms] [sixsq.nuvla.ui.utils.general :as utils-general] [sixsq.nuvla.ui.utils.icons :as icons] [sixsq.nuvla.ui.utils.semantic-ui :as ui] @@ -26,22 +27,26 @@ (dispatch [::main-events/reset-changes-protection]))) (defn GroupMember - [id principal members editable?] - (let [principal-name (subscribe [::session-subs/resolve-principal principal])] + [id group-name principal members editable?] + (let [tr (subscribe [::i18n-subs/tr]) + principal-name (subscribe [::session-subs/resolve-principal principal])] [ui/ListItem [ui/ListContent - [ui/ListIcon {:name icons/i-user :size "large" :verticalAlign "middle"}] + [ui/ListIcon {:className icons/i-user :size "large" :verticalAlign "middle"}] @principal-name utils-general/nbsp (when editable? - [icons/CloseIcon {:link true - :size "small" - :color "red" - :on-click (fn [] - (reset! members (-> @members set (disj principal) vec)) - (dispatch [::main-events/changes-protection? true]) - (set-group-changed! id))}])]])) + [uix/ModalDanger + {:button-text (@tr [:yes]) + :on-confirm (fn [] + (reset! members (-> @members set (disj principal) vec)) + (dispatch [::main-events/changes-protection? true]) + (set-group-changed! id)) + :trigger (r/as-element [ui/MenuItem + [icons/TrashIcon]]) + :header "Remove member" + :content [:span "Do you want to remove " [:b @principal-name] " from " [:b group-name] " group?"]}])]])) (defn DropdownPrincipals [_add-user _opts _members] @@ -79,113 +84,125 @@ show-acl? (r/atom false) invite-user (r/atom nil) add-user (r/atom nil)] - (fn [group] - (let [{:keys [id name description]} group] + (fn [{:keys [id name description]}] + (let [invite-fn #(do + (when-not (str/blank? @invite-user) + (dispatch [::events/invite-to-group id @invite-user]) + (reset! invite-user nil))) + group-name (or name id)] [:<> - [ui/Header {:as :h3} - [icons/UserGroupIcon] - [ui/HeaderContent - (or name id) - [ui/HeaderSubheader description " (" id ")"]]] - [:div - - [ui/Button {:basic true :floated "right"} "Add Subgroup"]] + [ui/Grid {:columns 2} + [ui/GridColumn {:floated :left} + [ui/Header {:as :h3} + [icons/UserGroupIcon] + [ui/HeaderContent + group-name + [ui/HeaderSubheader description " (" id ")"]]]] + [ui/GridColumn {:floated :right} + [ui/Button {:basic true :floated :right} [:b "Add Subgroup"]]]] [ui/Header {:as :h3 :dividing true} "Members"] (if (empty? @members) [uix/MsgNoItemsToShow [uix/TR (if editable? :empty-group-message :empty-group-or-no-access-message)]] - [ui/ListSA {:relaxed "true" :vertical-align "middle"} + [ui/ListSA {:relaxed true :vertical-align "middle"} (for [m @members] ^{:key m} - [GroupMember id m members editable?])]) - [ui/Input {:placeholder (@tr [:invite-by-email]) - :style {:width "250px"} - :value (or @invite-user "") - :on-change (ui-callback/value #(reset! invite-user %))}] + [GroupMember id group-name m members editable?])]) + (when (utils-general/can-operation? "invite" group) + [ui/Input {:placeholder (@tr [:invite-by-email]) + :icon (r/as-element + [icons/PaperPlaneIcon {:style {:cursor :pointer :font-size "unset"} + :link true + :circular true + :onClick invite-fn}]) + :style {:width "280px" :cursor :pointer} + :on-key-press (partial forms/on-return-key invite-fn) + :value (or @invite-user "") + :on-change (ui-callback/value #(reset! invite-user %))}]) - [ui/Table {:columns 4} - [ui/TableHeader {:fullWidth true} - [ui/TableRow - [ui/TableHeaderCell - [ui/HeaderSubheader {:as :h3} name]] - (when description [:p description]) - (when (and @acl editable?) - [ui/TableHeaderCell - [acl-views/AclButtonOnly {:default-value @acl - :read-only (not editable?) - :active? show-acl?}]])] - (when @show-acl? + #_[ui/Table {:columns 4} + [ui/TableHeader {:fullWidth true} [ui/TableRow - [ui/TableCell {:colSpan 4} - [acl-views/AclSection {:default-value @acl - :read-only (not editable?) - :active? show-acl? - :on-change #(do - (reset! acl %) - (set-group-changed! id) - (dispatch [::main-events/changes-protection? true]))}]]])] - [ui/TableBody - [ui/TableRow - [ui/TableCell - (if (empty? @members) - [uix/MsgNoItemsToShow [uix/TR (if editable? :empty-group-message - :empty-group-or-no-access-message)]] - [ui/ListSA - (for [m @members] - ^{:key m} - [GroupMember id m members editable?])])]] - (when editable? + [ui/TableHeaderCell + [ui/HeaderSubheader {:as :h3} name]] + (when description [:p description]) + (when (and @acl editable?) + [ui/TableHeaderCell + [acl-views/AclButtonOnly {:default-value @acl + :read-only (not editable?) + :active? show-acl?}]])] + (when @show-acl? + [ui/TableRow + [ui/TableCell {:colSpan 4} + [acl-views/AclSection {:default-value @acl + :read-only (not editable?) + :active? show-acl? + :on-change #(do + (reset! acl %) + (set-group-changed! id) + (dispatch [::main-events/changes-protection? true]))}]]])] + [ui/TableBody [ui/TableRow [ui/TableCell - [:div {:style {:display "flex"}} - [DropdownPrincipals - add-user - {:placeholder (@tr [:add-group-members]) - :fluid true} @members] - [:span utils-general/nbsp] - [uix/Button {:text (@tr [:add]) - :icon "add user" - :disabled (str/blank? @add-user) - :on-click #(do - (swap! members conj @add-user) - (reset! add-user nil) - (set-group-changed! id) - (dispatch [::main-events/changes-protection? true]))}] - [:span utils-general/nbsp] - [:span utils-general/nbsp] - [ui/Input {:placeholder (@tr [:invite-by-email]) - :style {:width "250px"} - :value (or @invite-user "") - :on-change (ui-callback/value #(reset! invite-user %))}] - [:span utils-general/nbsp] - [uix/Button {:text (@tr [:send]) - :icon "send" - :disabled (str/blank? @invite-user) - :on-click #(do - (dispatch [::events/invite-to-group id @invite-user]) - (reset! invite-user nil))}]]] - [ui/TableCell {:textAlign "right"} - [uix/Button {:primary true - :text (@tr [:save]) - :icon "save" - :disabled (not @changed?) - :on-click #(do (dispatch [::events/edit-group (assoc group :users @members, :acl @acl)]) - (disable-changes-protection! id))}]]])]]])))) + (if (empty? @members) + [uix/MsgNoItemsToShow [uix/TR (if editable? :empty-group-message + :empty-group-or-no-access-message)]] + [ui/ListSA + (for [m @members] + ^{:key m} + [GroupMember id m members editable?])])]] + (when editable? + [ui/TableRow + [ui/TableCell + [:div {:style {:display "flex"}} + [DropdownPrincipals + add-user + {:placeholder (@tr [:add-group-members]) + :fluid true} @members] + [:span utils-general/nbsp] + [uix/Button {:text (@tr [:add]) + :icon "add user" + :disabled (str/blank? @add-user) + :on-click #(do + (swap! members conj @add-user) + (reset! add-user nil) + (set-group-changed! id) + (dispatch [::main-events/changes-protection? true]))}] + [:span utils-general/nbsp] + [:span utils-general/nbsp] + [ui/Input {:placeholder (@tr [:invite-by-email]) + :style {:width "250px"} + :value (or @invite-user "") + :on-change (ui-callback/value #(reset! invite-user %))}] + [:span utils-general/nbsp] + [uix/Button {:text (@tr [:send]) + :icon "send" + :disabled (str/blank? @invite-user) + :on-click #(do + (dispatch [::events/invite-to-group id @invite-user]) + (reset! invite-user nil))}]]] + [ui/TableCell {:textAlign "right"} + [uix/Button {:primary true + :text (@tr [:save]) + :icon "save" + :disabled (not @changed?) + :on-click #(do (dispatch [::events/edit-group (assoc group :users @members, :acl @acl)]) + (disable-changes-protection! id))}]]])]]])))) -(defn GroupMembersSegment - [] - (let [tr (subscribe [::i18n-subs/tr]) - loading? (subscribe [::subs/loading? :group]) - group (subscribe [::subs/group])] - (dispatch [::events/get-group]) - (fn [] - [ui/Segment {:padded true - :color "green" - :loading @loading? - :style {:height "100%"}} - [ui/Header {:as :h2 :dividing true} (@tr [:group-members])] - ^{:key (random-uuid)} - [GroupMembers @group]]))) +;(defn GroupMembersSegment +; [] +; (let [tr (subscribe [::i18n-subs/tr]) +; loading? (subscribe [::subs/loading? :group]) +; group (subscribe [::subs/group])] +; (dispatch [::events/get-group]) +; (fn [] +; [ui/Segment {:padded true +; :color "green" +; :loading @loading? +; :style {:height "100%"}} +; [ui/Header {:as :h2 :dividing true} (@tr [:group-members])] +; ^{:key (random-uuid)} +; [GroupMembers @group]]))) (def selected-group (r/atom nil)) @@ -237,22 +254,25 @@ (defn GroupsViewPage [] - [ui/Grid {:stackable false} - [ui/GridColumn {:stretched true - :computer 4 - :tablet 6 - :mobile 8 - :style {:background-color "light-gray" - :padding-right 0}} - [GroupHierarchySegment]] - [ui/GridColumn {:stretched true - :tablet 10 - :computer 12 - :mobile 8 - :style {:background-color "light-gray" - :padding-right 0}} - [ui/Segment {:style {:min-height "100%" - :overflow-x :auto}} - (if @selected-group - [GroupMembers @(subscribe [::session-subs/group @selected-group])] - [:i "Select a group"])]]]) + (let [group (when @selected-group + @(subscribe [::session-subs/group @selected-group]))] + [ui/Grid {:stackable false} + [ui/GridColumn {:stretched true + :computer 4 + :tablet 6 + :mobile 8 + :style {:background-color "light-gray" + :padding-right 0}} + [GroupHierarchySegment]] + [ui/GridColumn {:stretched true + :tablet 10 + :computer 12 + :mobile 8 + :style {:background-color "light-gray" + :padding-right 0}} + [ui/Segment {:style {:min-height "100%" + :overflow-x :auto}} + (if @selected-group + ^{:key group} + [GroupMembers group] + [uix/MsgNoItemsToShow [uix/TR "Select a Group"]])]]])) diff --git a/code/src/cljs/sixsq/nuvla/ui/utils/icons.cljs b/code/src/cljs/sixsq/nuvla/ui/utils/icons.cljs index 6d5573704..b3619758b 100644 --- a/code/src/cljs/sixsq/nuvla/ui/utils/icons.cljs +++ b/code/src/cljs/sixsq/nuvla/ui/utils/icons.cljs @@ -717,3 +717,13 @@ (def i-cloud-download "fal fa-cloud-download") (def i-cloud-upload "fal fa-cloud-upload") + +(def i-ellipsis "fas fa-ellipsis") +(defn EllipsisIcon + [opts] + [I opts i-ellipsis]) + +(def i-paper-plane "fal fa-paper-plane") +(defn PaperPlaneIcon + [opts] + [I opts i-paper-plane]) From ad63203266bb253f2810e9a3ca92470ce57b7b52 Mon Sep 17 00:00:00 2001 From: khaled basbous Date: Mon, 14 Jul 2025 13:37:48 +0200 Subject: [PATCH 05/10] Logic over buttons and send invitation --- .../sixsq/nuvla/ui/pages/groups/views.cljs | 82 +++++++++++++------ 1 file changed, 58 insertions(+), 24 deletions(-) diff --git a/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs b/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs index 34bd76b02..0b7535d65 100644 --- a/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs +++ b/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs @@ -1,5 +1,6 @@ (ns sixsq.nuvla.ui.pages.groups.views (:require ["@stripe/react-stripe-js" :as react-stripe] + [clojure.set :as set] [clojure.string :as str] [re-frame.core :refer [dispatch subscribe]] [reagent.core :as r] @@ -27,26 +28,56 @@ (dispatch [::main-events/reset-changes-protection]))) (defn GroupMember - [id group-name principal members editable?] - (let [tr (subscribe [::i18n-subs/tr]) - principal-name (subscribe [::session-subs/resolve-principal principal])] + [id group-name principal members editable? {:keys [owners manage view-data view-acl] :as acl}] + (let [tr (subscribe [::i18n-subs/tr]) + principal-name (subscribe [::session-subs/resolve-principal principal]) + manager? (boolean ((set (concat owners manage)) principal)) + can-view-members? (boolean ((set (concat owners view-data view-acl)) principal))] [ui/ListItem - - [ui/ListContent - [ui/ListIcon {:className icons/i-user :size "large" :verticalAlign "middle"}] - @principal-name - utils-general/nbsp - (when editable? + (when editable? + [ui/ListContent {:floated :right} + (if manager? + [ui/Popup {:content "Remove manager" + :trigger (r/as-element + [ui/Button {:icon true :basic true} + [ui/IconGroup + [icons/Icon {:name "fal fa-crown"}] + [icons/Icon {:name "fal fa-slash"}]]])}] + [:<> + (if can-view-members? + [ui/Popup {:content "Limit member’s view to only the group name and description" + :trigger (r/as-element + [ui/Button {:icon true :basic true} + [icons/Icon {:name "far fa-eye-slash"}]])}] + [ui/Popup {:content "Extend user view to member's list" + :trigger (r/as-element + [ui/Button {:icon true :basic true} + [icons/Icon {:name "far fa-eye"}]])}]) + [ui/Popup {:content "Make manager" + :trigger (r/as-element + [ui/Button {:icon true :basic true} [icons/Icon {:name "fal fa-crown"}]])}]]) [uix/ModalDanger {:button-text (@tr [:yes]) :on-confirm (fn [] (reset! members (-> @members set (disj principal) vec)) (dispatch [::main-events/changes-protection? true]) (set-group-changed! id)) - :trigger (r/as-element [ui/MenuItem - [icons/TrashIcon]]) + :trigger (r/as-element + [:span [ui/Popup {:content "Remove member" + :trigger (r/as-element + [ui/Button {:icon true + :basic true} + [icons/TrashIcon]])}]]) :header "Remove member" - :content [:span "Do you want to remove " [:b @principal-name] " from " [:b group-name] " group?"]}])]])) + :content [:span "Do you want to remove " [:b @principal-name] " from " [:b group-name] " group?"]}]]) + + + [ui/ListContent {:style {:display :flex :align-items :flex-end}} + [ui/IconGroup + [ui/Icon {:className icons/i-user :size "large"}] + (when manager? [ui/Icon {:className "fa-solid fa-crown" :corner true}])] + @principal-name] + ])) (defn DropdownPrincipals [_add-user _opts _members] @@ -84,35 +115,38 @@ show-acl? (r/atom false) invite-user (r/atom nil) add-user (r/atom nil)] - (fn [{:keys [id name description]}] - (let [invite-fn #(do - (when-not (str/blank? @invite-user) - (dispatch [::events/invite-to-group id @invite-user]) - (reset! invite-user nil))) + (fn [{:keys [id name description acl]}] + (let [invite-fn #(do + (when-not (str/blank? @invite-user) + (dispatch [::events/invite-to-group id @invite-user]) + (reset! invite-user nil))) group-name (or name id)] [:<> - [ui/Grid {:columns 2} - [ui/GridColumn {:floated :left} + [ui/Grid {:columns 2 :stackable true} + [ui/GridColumn {:floated :left :width 13} [ui/Header {:as :h3} [icons/UserGroupIcon] [ui/HeaderContent group-name [ui/HeaderSubheader description " (" id ")"]]]] - [ui/GridColumn {:floated :right} + [ui/GridColumn {:floated :right :width 3} [ui/Button {:basic true :floated :right} [:b "Add Subgroup"]]]] [ui/Header {:as :h3 :dividing true} "Members"] (if (empty? @members) [uix/MsgNoItemsToShow [uix/TR (if editable? :empty-group-message :empty-group-or-no-access-message)]] - [ui/ListSA {:relaxed true :vertical-align "middle"} + + [ui/ListSA {:divided true :vertical-align "middle"} (for [m @members] ^{:key m} - [GroupMember id group-name m members editable?])]) + [GroupMember id group-name m members editable? acl])]) (when (utils-general/can-operation? "invite" group) [ui/Input {:placeholder (@tr [:invite-by-email]) + :type :email :icon (r/as-element - [icons/PaperPlaneIcon {:style {:cursor :pointer :font-size "unset"} - :link true + [icons/PaperPlaneIcon {:style {:font-size "unset"} + :link (not (str/blank? @invite-user)) + :color (when (not (str/blank? @invite-user)) "blue") :circular true :onClick invite-fn}]) :style {:width "280px" :cursor :pointer} From 55883b495cba58802650c7820588dfdacf8c8dd0 Mon Sep 17 00:00:00 2001 From: khaled basbous Date: Tue, 15 Jul 2025 13:29:16 +0300 Subject: [PATCH 06/10] implementation of action --- .../sixsq/nuvla/ui/pages/groups/views.cljs | 279 ++++++++---------- .../cljs/sixsq/nuvla/ui/utils/general.cljs | 20 ++ 2 files changed, 140 insertions(+), 159 deletions(-) diff --git a/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs b/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs index 0b7535d65..9c967a827 100644 --- a/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs +++ b/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs @@ -1,15 +1,11 @@ (ns sixsq.nuvla.ui.pages.groups.views (:require ["@stripe/react-stripe-js" :as react-stripe] - [clojure.set :as set] [clojure.string :as str] [re-frame.core :refer [dispatch subscribe]] [reagent.core :as r] - [sixsq.nuvla.ui.common-components.acl.views :as acl-views] [sixsq.nuvla.ui.common-components.i18n.subs :as i18n-subs] [sixsq.nuvla.ui.common-components.plugins.full-text-search :as full-text-search-plugin] - [sixsq.nuvla.ui.main.events :as main-events] [sixsq.nuvla.ui.pages.profile.events :as events] - [sixsq.nuvla.ui.pages.profile.subs :as subs] [sixsq.nuvla.ui.session.subs :as session-subs] [sixsq.nuvla.ui.utils.forms :as forms] [sixsq.nuvla.ui.utils.general :as utils-general] @@ -18,17 +14,93 @@ [sixsq.nuvla.ui.utils.semantic-ui-extensions :as uix] [sixsq.nuvla.ui.utils.ui-callback :as ui-callback])) +(def selected-group (r/atom nil)) + +(defn ConfirmActionModal + [{:keys [on-confirm header Content Icon]}] + (let [tr (subscribe [::i18n-subs/tr])] + [uix/ModalDanger + {:button-text (@tr [:yes]) + :on-confirm on-confirm + :trigger (r/as-element + [:span [ui/Popup {:content header + :trigger (r/as-element + [ui/Button {:icon true :basic true} + Icon])}]]) + :header header + :content Content}])) + +(defn RemoveManagerButton + [group principal principal-name group-name] + [ConfirmActionModal {:on-confirm (fn [] + (dispatch [::events/edit-group + (-> group + (utils-general/acl-append-resource :owners "group/nuvla-admin") + (utils-general/acl-remove-resource :owners principal) + (utils-general/acl-remove-resource :edit-meta principal) + (utils-general/acl-remove-resource :edit-data principal) + (utils-general/acl-remove-resource :edit-acl principal) + (utils-general/acl-remove-resource :manage principal))])) + :header "Remove manager" + :Content [:span "Do you want to remove " [:b @principal-name] " from manager's of group " [:b group-name] "?"] + :Icon [ui/IconGroup + [icons/Icon {:name "fal fa-crown"}] + [icons/Icon {:name "fal fa-slash"}]]}]) + +(defn MakeManagerButton + [group principal principal-name group-name] + [ConfirmActionModal {:on-confirm #(dispatch [::events/edit-group + (-> group + (utils-general/acl-append-resource :edit-acl principal) + (utils-general/acl-append-resource :manage principal))]) + :header "Make manager" + :Content [:span "Do you want to make " [:b @principal-name] " a manager of group " [:b group-name] "?"] + :Icon [icons/Icon {:name "fal fa-crown"}]}]) -(def group-changed! (r/atom {})) -(defn set-group-changed! [id] (swap! group-changed! assoc id true)) -(defn disable-changes-protection! - [id] - (swap! group-changed! assoc id false) - (when-not (some true? (vals @group-changed!)) - (dispatch [::main-events/reset-changes-protection]))) +(defn RemoveMemberButton + [group principal principal-name group-name] + [ConfirmActionModal {:on-confirm (fn [] + (dispatch [::events/edit-group + (-> group + (update :users (partial remove #{principal})) + (utils-general/acl-append-resource :owners "group/nuvla-admin") + (utils-general/acl-remove-resource :edit-acl principal) + (utils-general/acl-remove-resource :edit-data principal) + (utils-general/acl-remove-resource :edit-meta principal) + (utils-general/acl-remove-resource :view-acl principal) + (utils-general/acl-remove-resource :view-data principal) + (utils-general/acl-remove-resource :view-meta principal) + (utils-general/acl-remove-resource :manage principal))])) + :header "Remove member" + :Content [:span "Do you want to remove " [:b @principal-name] " from " [:b group-name] " group?"] + :Icon [icons/TrashIcon]}]) + +(defn LimitMemberViewButton + [group principal] + [ConfirmActionModal {:on-confirm (fn [] + (dispatch [::events/edit-group + (-> group + (utils-general/acl-remove-resource :edit-meta principal) + (utils-general/acl-remove-resource :edit-data principal) + (utils-general/acl-remove-resource :edit-acl principal) + (utils-general/acl-remove-resource :view-acl principal) + (utils-general/acl-remove-resource :view-data principal) + )])) + :header "Limit member’s view" + :Content "Limit member’s view to only the group name and description" + :Icon [icons/Icon {:name "far fa-eye-slash"}]}]) + +(defn ExtendMemberViewButton + [group principal] + [ConfirmActionModal {:on-confirm (fn [] + (dispatch [::events/edit-group + (utils-general/acl-append-resource group :view-acl principal)])) + :header "Extend user view" + :Content "Extend user view to member's list" + :Icon [icons/Icon {:name "far fa-eye"}]}]) (defn GroupMember - [id group-name principal members editable? {:keys [owners manage view-data view-acl] :as acl}] + [id group-name principal editable? {{:keys [owners manage view-data view-acl] :as acl} :acl :as group}] (let [tr (subscribe [::i18n-subs/tr]) principal-name (subscribe [::session-subs/resolve-principal principal]) manager? (boolean ((set (concat owners manage)) principal)) @@ -37,39 +109,13 @@ (when editable? [ui/ListContent {:floated :right} (if manager? - [ui/Popup {:content "Remove manager" - :trigger (r/as-element - [ui/Button {:icon true :basic true} - [ui/IconGroup - [icons/Icon {:name "fal fa-crown"}] - [icons/Icon {:name "fal fa-slash"}]]])}] + [RemoveManagerButton group principal @principal-name group-name] [:<> (if can-view-members? - [ui/Popup {:content "Limit member’s view to only the group name and description" - :trigger (r/as-element - [ui/Button {:icon true :basic true} - [icons/Icon {:name "far fa-eye-slash"}]])}] - [ui/Popup {:content "Extend user view to member's list" - :trigger (r/as-element - [ui/Button {:icon true :basic true} - [icons/Icon {:name "far fa-eye"}]])}]) - [ui/Popup {:content "Make manager" - :trigger (r/as-element - [ui/Button {:icon true :basic true} [icons/Icon {:name "fal fa-crown"}]])}]]) - [uix/ModalDanger - {:button-text (@tr [:yes]) - :on-confirm (fn [] - (reset! members (-> @members set (disj principal) vec)) - (dispatch [::main-events/changes-protection? true]) - (set-group-changed! id)) - :trigger (r/as-element - [:span [ui/Popup {:content "Remove member" - :trigger (r/as-element - [ui/Button {:icon true - :basic true} - [icons/TrashIcon]])}]]) - :header "Remove member" - :content [:span "Do you want to remove " [:b @principal-name] " from " [:b group-name] " group?"]}]]) + [LimitMemberViewButton group principal] + [ExtendMemberViewButton group principal]) + [MakeManagerButton group principal principal-name group-name]]) + [RemoveMemberButton group principal principal-name group-name]]) [ui/ListContent {:style {:display :flex :align-items :flex-end}} @@ -104,23 +150,34 @@ :style {:width "250px"} :upward false}])))) -(defn GroupMembers - [group] +(defn InviteInput + [{:keys [id] :as _group}] (let [tr (subscribe [::i18n-subs/tr]) - editable? (utils-general/editable? group false) - users (:users group) - members (r/atom users) - acl (r/atom (:acl group)) - changed? (r/cursor group-changed! [(:id group)]) - show-acl? (r/atom false) invite-user (r/atom nil) - add-user (r/atom nil)] - (fn [{:keys [id name description acl]}] - (let [invite-fn #(do - (when-not (str/blank? @invite-user) - (dispatch [::events/invite-to-group id @invite-user]) - (reset! invite-user nil))) - group-name (or name id)] + invite-fn #(do + (when-not (str/blank? @invite-user) + (dispatch [::events/invite-to-group id @invite-user]) + (reset! invite-user nil)))] + (fn [group] + (when (utils-general/can-operation? "invite" group) + [ui/Input {:placeholder (@tr [:invite-by-email]) + :type :email + :icon (r/as-element + [icons/PaperPlaneIcon {:style {:font-size "unset"} + :link (not (str/blank? @invite-user)) + :color (when (not (str/blank? @invite-user)) "blue") + :circular true + :onClick invite-fn}]) + :style {:width "280px" :cursor :pointer} + :on-key-press (partial forms/on-return-key invite-fn) + :value (or @invite-user "") + :on-change (ui-callback/value #(reset! invite-user %))}])))) + +(defn GroupMembers + [group] + (let [editable? (utils-general/editable? group false)] + (fn [{:keys [id name description users] :as group}] + (let [group-name (or name id)] [:<> [ui/Grid {:columns 2 :stackable true} [ui/GridColumn {:floated :left :width 13} @@ -132,113 +189,17 @@ [ui/GridColumn {:floated :right :width 3} [ui/Button {:basic true :floated :right} [:b "Add Subgroup"]]]] [ui/Header {:as :h3 :dividing true} "Members"] - (if (empty? @members) + (if (empty? users) [uix/MsgNoItemsToShow [uix/TR (if editable? :empty-group-message :empty-group-or-no-access-message)]] [ui/ListSA {:divided true :vertical-align "middle"} - (for [m @members] + (for [m users] ^{:key m} - [GroupMember id group-name m members editable? acl])]) - (when (utils-general/can-operation? "invite" group) - [ui/Input {:placeholder (@tr [:invite-by-email]) - :type :email - :icon (r/as-element - [icons/PaperPlaneIcon {:style {:font-size "unset"} - :link (not (str/blank? @invite-user)) - :color (when (not (str/blank? @invite-user)) "blue") - :circular true - :onClick invite-fn}]) - :style {:width "280px" :cursor :pointer} - :on-key-press (partial forms/on-return-key invite-fn) - :value (or @invite-user "") - :on-change (ui-callback/value #(reset! invite-user %))}]) + [GroupMember id group-name m editable? group])]) + [InviteInput group] - #_[ui/Table {:columns 4} - [ui/TableHeader {:fullWidth true} - [ui/TableRow - [ui/TableHeaderCell - [ui/HeaderSubheader {:as :h3} name]] - (when description [:p description]) - (when (and @acl editable?) - [ui/TableHeaderCell - [acl-views/AclButtonOnly {:default-value @acl - :read-only (not editable?) - :active? show-acl?}]])] - (when @show-acl? - [ui/TableRow - [ui/TableCell {:colSpan 4} - [acl-views/AclSection {:default-value @acl - :read-only (not editable?) - :active? show-acl? - :on-change #(do - (reset! acl %) - (set-group-changed! id) - (dispatch [::main-events/changes-protection? true]))}]]])] - [ui/TableBody - [ui/TableRow - [ui/TableCell - (if (empty? @members) - [uix/MsgNoItemsToShow [uix/TR (if editable? :empty-group-message - :empty-group-or-no-access-message)]] - [ui/ListSA - (for [m @members] - ^{:key m} - [GroupMember id m members editable?])])]] - (when editable? - [ui/TableRow - [ui/TableCell - [:div {:style {:display "flex"}} - [DropdownPrincipals - add-user - {:placeholder (@tr [:add-group-members]) - :fluid true} @members] - [:span utils-general/nbsp] - [uix/Button {:text (@tr [:add]) - :icon "add user" - :disabled (str/blank? @add-user) - :on-click #(do - (swap! members conj @add-user) - (reset! add-user nil) - (set-group-changed! id) - (dispatch [::main-events/changes-protection? true]))}] - [:span utils-general/nbsp] - [:span utils-general/nbsp] - [ui/Input {:placeholder (@tr [:invite-by-email]) - :style {:width "250px"} - :value (or @invite-user "") - :on-change (ui-callback/value #(reset! invite-user %))}] - [:span utils-general/nbsp] - [uix/Button {:text (@tr [:send]) - :icon "send" - :disabled (str/blank? @invite-user) - :on-click #(do - (dispatch [::events/invite-to-group id @invite-user]) - (reset! invite-user nil))}]]] - [ui/TableCell {:textAlign "right"} - [uix/Button {:primary true - :text (@tr [:save]) - :icon "save" - :disabled (not @changed?) - :on-click #(do (dispatch [::events/edit-group (assoc group :users @members, :acl @acl)]) - (disable-changes-protection! id))}]]])]]])))) - -;(defn GroupMembersSegment -; [] -; (let [tr (subscribe [::i18n-subs/tr]) -; loading? (subscribe [::subs/loading? :group]) -; group (subscribe [::subs/group])] -; (dispatch [::events/get-group]) -; (fn [] -; [ui/Segment {:padded true -; :color "green" -; :loading @loading? -; :style {:height "100%"}} -; [ui/Header {:as :h2 :dividing true} (@tr [:group-members])] -; ^{:key (random-uuid)} -; [GroupMembers @group]]))) - -(def selected-group (r/atom nil)) + ])))) (defn Group [] diff --git a/code/src/cljs/sixsq/nuvla/ui/utils/general.cljs b/code/src/cljs/sixsq/nuvla/ui/utils/general.cljs index ecbcf7d10..9d350b109 100644 --- a/code/src/cljs/sixsq/nuvla/ui/utils/general.cljs +++ b/code/src/cljs/sixsq/nuvla/ui/utils/general.cljs @@ -482,3 +482,23 @@ (if (not= c 0) c (recur (rest rest-orders))))))) + +(defn acl-append + [acl right-kw user-id] + (if user-id + (update acl right-kw (comp vec set conj) user-id) + acl)) + +(defn acl-remove + [acl right-kw user-id] + (if user-id + (update acl right-kw (fn [user-ids] (vec (remove #{user-id} user-ids)))) + acl)) + +(defn acl-append-resource + [resource right-kw user-id] + (update resource :acl acl-append right-kw user-id)) + +(defn acl-remove-resource + [resource right-kw user-id] + (update resource :acl acl-remove right-kw user-id)) From 061d19c8b13265b401d02fd0464f0b25e3b35cc4 Mon Sep 17 00:00:00 2001 From: khaled basbous Date: Tue, 15 Jul 2025 16:16:39 +0300 Subject: [PATCH 07/10] navigation by url and remove local atoms for selected group --- .../sixsq/nuvla/ui/pages/groups/views.cljs | 41 ++++++++++--------- .../sixsq/nuvla/ui/pages/profile/events.cljs | 10 +++-- .../cljs/sixsq/nuvla/ui/routing/router.cljs | 23 ++++++++--- .../cljs/sixsq/nuvla/ui/routing/routes.cljs | 1 + .../cljs/sixsq/nuvla/ui/routing/utils.cljs | 3 +- .../cljs/sixsq/nuvla/ui/session/events.cljs | 2 +- 6 files changed, 49 insertions(+), 31 deletions(-) diff --git a/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs b/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs index 9c967a827..7c1a2c66e 100644 --- a/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs +++ b/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs @@ -6,6 +6,8 @@ [sixsq.nuvla.ui.common-components.i18n.subs :as i18n-subs] [sixsq.nuvla.ui.common-components.plugins.full-text-search :as full-text-search-plugin] [sixsq.nuvla.ui.pages.profile.events :as events] + [sixsq.nuvla.ui.routing.routes :as routes] + [sixsq.nuvla.ui.routing.events :as routing-events] [sixsq.nuvla.ui.session.subs :as session-subs] [sixsq.nuvla.ui.utils.forms :as forms] [sixsq.nuvla.ui.utils.general :as utils-general] @@ -14,8 +16,6 @@ [sixsq.nuvla.ui.utils.semantic-ui-extensions :as uix] [sixsq.nuvla.ui.utils.ui-callback :as ui-callback])) -(def selected-group (r/atom nil)) - (defn ConfirmActionModal [{:keys [on-confirm header Content Icon]}] (let [tr (subscribe [::i18n-subs/tr])] @@ -88,7 +88,7 @@ )])) :header "Limit member’s view" :Content "Limit member’s view to only the group name and description" - :Icon [icons/Icon {:name "far fa-eye-slash"}]}]) + :Icon [icons/Icon {:className "far fa-eye-slash"}]}]) (defn ExtendMemberViewButton [group principal] @@ -97,7 +97,7 @@ (utils-general/acl-append-resource group :view-acl principal)])) :header "Extend user view" :Content "Extend user view to member's list" - :Icon [icons/Icon {:name "far fa-eye"}]}]) + :Icon [icons/Icon {:className "far fa-eye"}]}]) (defn GroupMember [id group-name principal editable? {{:keys [owners manage view-data view-acl] :as acl} :acl :as group}] @@ -109,7 +109,7 @@ (when editable? [ui/ListContent {:floated :right} (if manager? - [RemoveManagerButton group principal @principal-name group-name] + [RemoveManagerButton group principal principal-name group-name] [:<> (if can-view-members? [LimitMemberViewButton group principal] @@ -202,13 +202,13 @@ ])))) (defn Group - [] - (let [collapsed (r/atom true)] - (fn [{:keys [id name children] :as _group}] - (let [selected? (= @selected-group id) + [{:keys [id] :as _group} {:keys [parents] :as _selected-group}] + (let [collapsed (r/atom (not ((set parents) id)))] + (fn [{:keys [id name children] :as _group} selected-group] + (let [selected? (= (:id selected-group) id) children? (boolean (seq children))] [ui/ListItem {:on-click #(do - (reset! selected-group id) + (dispatch [::routing-events/navigate routes/groups-details {:uuid (utils-general/id->uuid id)}]) (.stopPropagation %))} [ui/ListIcon {:style {:padding 5 :min-width "17px"} @@ -230,10 +230,10 @@ [ui/ListList (for [child (sort-by (juxt :id :name) children)] ^{:key (:id child)} - [Group child])])]])))) + [Group child selected-group])])]])))) (defn GroupHierarchySegment - [] + [selected-group] (let [groups-hierarchy @(subscribe [::session-subs/groups-hierarchies])] [ui/Segment {:raised true :style {:overflow-x :auto :min-height "100%"}} @@ -245,12 +245,13 @@ [ui/ListSA {:selection true} (for [group-hierarchy (sort-by (juxt :id :name) groups-hierarchy)] ^{:key (:id group-hierarchy)} - [Group group-hierarchy])]])) + [Group group-hierarchy selected-group])]])) (defn GroupsViewPage - [] - (let [group (when @selected-group - @(subscribe [::session-subs/group @selected-group]))] + [{path :path}] + (let [[_ uuid] path + selected-group (when uuid + @(subscribe [::session-subs/group (str "group/" uuid)]))] [ui/Grid {:stackable false} [ui/GridColumn {:stretched true :computer 4 @@ -258,7 +259,7 @@ :mobile 8 :style {:background-color "light-gray" :padding-right 0}} - [GroupHierarchySegment]] + [GroupHierarchySegment selected-group]] [ui/GridColumn {:stretched true :tablet 10 :computer 12 @@ -267,7 +268,7 @@ :padding-right 0}} [ui/Segment {:style {:min-height "100%" :overflow-x :auto}} - (if @selected-group - ^{:key group} - [GroupMembers group] + (if selected-group + ^{:key selected-group} + [GroupMembers selected-group] [uix/MsgNoItemsToShow [uix/TR "Select a Group"]])]]])) diff --git a/code/src/cljs/sixsq/nuvla/ui/pages/profile/events.cljs b/code/src/cljs/sixsq/nuvla/ui/pages/profile/events.cljs index 6a788814f..f73b37750 100644 --- a/code/src/cljs/sixsq/nuvla/ui/pages/profile/events.cljs +++ b/code/src/cljs/sixsq/nuvla/ui/pages/profile/events.cljs @@ -98,10 +98,12 @@ status (str " (" status ")")) :content message :type :error}])) - (dispatch [::messages-events/add - {:header "Group updated" - :content "Group updated successfully." - :type :info}]))]}))) + (do + (dispatch [::session-events/search-groups]) + (dispatch [::messages-events/add + {:header "Group updated" + :content "Group updated successfully." + :type :info}])))]}))) (reg-event-fx ::invite-to-group diff --git a/code/src/cljs/sixsq/nuvla/ui/routing/router.cljs b/code/src/cljs/sixsq/nuvla/ui/routing/router.cljs index 44c01b13a..d84bb8da2 100644 --- a/code/src/cljs/sixsq/nuvla/ui/routing/router.cljs +++ b/code/src/cljs/sixsq/nuvla/ui/routing/router.cljs @@ -61,6 +61,22 @@ :view #'views-cluster/ClusterViewPage}]]) (utils/canonical->all-page-names "edges"))) +(def groups-routes + (mapv (fn [page-alias] + [page-alias + {:name (create-route-name page-alias) + :layout #'LayoutPage + :view #'GroupsViewPage + :protected? true + :dict-key :groups} + [""] + ["/" (create-route-name page-alias "-slashed")] + ["/:uuid" + {:name (create-route-name page-alias "-details") + :layout #'LayoutPage + :view #'GroupsViewPage}]]) + (utils/canonical->all-page-names "groups"))) + (def cloud-routes (mapv (fn [page-alias] [page-alias @@ -122,6 +138,7 @@ cloud-routes deployment-routes deployment-group-routes + groups-routes ["sign-up" {:name ::routes/sign-up :layout #'LayoutAuthentication @@ -219,11 +236,7 @@ ["profile" {:name ::routes/profile :layout #'LayoutPage - :view #'profile}] - ["groups" - {:name ::routes/groups - :layout #'LayoutPage - :view #'GroupsViewPage}]] + :view #'profile}]] ["/*" {:name ::routes/catch-all :layout #'LayoutPage diff --git a/code/src/cljs/sixsq/nuvla/ui/routing/routes.cljs b/code/src/cljs/sixsq/nuvla/ui/routing/routes.cljs index 9965c5577..d30b9ce8c 100644 --- a/code/src/cljs/sixsq/nuvla/ui/routing/routes.cljs +++ b/code/src/cljs/sixsq/nuvla/ui/routing/routes.cljs @@ -50,5 +50,6 @@ (def api-slashed ::api-slashed) (def api-sub-page ::api-sub-page) (def groups ::groups) +(def groups-details ::groups-details) (def profile ::profile) (def catch-all ::catch-all) diff --git a/code/src/cljs/sixsq/nuvla/ui/routing/utils.cljs b/code/src/cljs/sixsq/nuvla/ui/routing/utils.cljs index 494d71f28..1ba7ac56e 100644 --- a/code/src/cljs/sixsq/nuvla/ui/routing/utils.cljs +++ b/code/src/cljs/sixsq/nuvla/ui/routing/utils.cljs @@ -101,7 +101,8 @@ "infrastructures" "clouds" "deployment" "deployments" "deployment-set" "deployment-groups" - "deployment-set-details" "deployment-groups-details"}) + "deployment-set-details" "deployment-groups-details" + "group" "groups"}) (defn ->canonical-route-name [route-name] diff --git a/code/src/cljs/sixsq/nuvla/ui/session/events.cljs b/code/src/cljs/sixsq/nuvla/ui/session/events.cljs index 8b618e94e..58af01019 100644 --- a/code/src/cljs/sixsq/nuvla/ui/session/events.cljs +++ b/code/src/cljs/sixsq/nuvla/ui/session/events.cljs @@ -236,7 +236,7 @@ (reg-event-fx ::search-groups (fn [] - {::cimi-api-fx/search [:group {:select "id, name, acl, users, description, operations" + {::cimi-api-fx/search [:group {:select "id, name, acl, users, description, operations, parents" :last 10000 :orderby "name:asc,id:asc"} #(dispatch [::set-groups %])] From 686c49cb0a707cd14928faae63dcf31da6c142b6 Mon Sep 17 00:00:00 2001 From: khaled basbous Date: Wed, 23 Jul 2025 11:35:15 +0300 Subject: [PATCH 08/10] Add group modal --- .../ui/common_components/i18n/dictionary.cljs | 2 +- .../sixsq/nuvla/ui/pages/groups/views.cljs | 102 ++++++++++++++++-- .../sixsq/nuvla/ui/pages/profile/views.cljs | 60 ----------- 3 files changed, 92 insertions(+), 72 deletions(-) diff --git a/code/src/cljs/sixsq/nuvla/ui/common_components/i18n/dictionary.cljs b/code/src/cljs/sixsq/nuvla/ui/common_components/i18n/dictionary.cljs index 1cfc2e86e..62cf494d7 100644 --- a/code/src/cljs/sixsq/nuvla/ui/common_components/i18n/dictionary.cljs +++ b/code/src/cljs/sixsq/nuvla/ui/common_components/i18n/dictionary.cljs @@ -73,7 +73,7 @@ :add-dropdown "Add:" :add-edges "Add edges" :add-first-tag "Add your first tag" - :add-group "add group" + :add-group "Add Group" :add-group-members "add group members" :add-license "Add a license" :add-payment-method "Add payment method" diff --git a/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs b/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs index 7c1a2c66e..1c5f2f3ca 100644 --- a/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs +++ b/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs @@ -1,11 +1,13 @@ (ns sixsq.nuvla.ui.pages.groups.views (:require ["@stripe/react-stripe-js" :as react-stripe] + [cljs.spec.alpha :as s] [clojure.string :as str] [re-frame.core :refer [dispatch subscribe]] [reagent.core :as r] [sixsq.nuvla.ui.common-components.i18n.subs :as i18n-subs] [sixsq.nuvla.ui.common-components.plugins.full-text-search :as full-text-search-plugin] [sixsq.nuvla.ui.pages.profile.events :as events] + [sixsq.nuvla.ui.pages.profile.spec :as spec] [sixsq.nuvla.ui.routing.routes :as routes] [sixsq.nuvla.ui.routing.events :as routing-events] [sixsq.nuvla.ui.session.subs :as session-subs] @@ -14,6 +16,7 @@ [sixsq.nuvla.ui.utils.icons :as icons] [sixsq.nuvla.ui.utils.semantic-ui :as ui] [sixsq.nuvla.ui.utils.semantic-ui-extensions :as uix] + [sixsq.nuvla.ui.utils.style :as style] [sixsq.nuvla.ui.utils.ui-callback :as ui-callback])) (defn ConfirmActionModal @@ -179,15 +182,20 @@ (fn [{:keys [id name description users] :as group}] (let [group-name (or name id)] [:<> - [ui/Grid {:columns 2 :stackable true} - [ui/GridColumn {:floated :left :width 13} - [ui/Header {:as :h3} - [icons/UserGroupIcon] - [ui/HeaderContent - group-name - [ui/HeaderSubheader description " (" id ")"]]]] - [ui/GridColumn {:floated :right :width 3} - [ui/Button {:basic true :floated :right} [:b "Add Subgroup"]]]] + [:div {:style {:display :flex + :align-items :flex-start + :justify-content :space-between + :flex-wrap :wrap + :padding-bottom "1em"}} + [ui/Header {:as :h3} + [icons/UserGroupIcon] + [ui/HeaderContent + group-name + [ui/HeaderSubheader description " (" id ")"]]] + (when (utils-general/can-operation? "add-subgroup" group) + [ui/Button {:secondary true :size "small" :icon true} + [icons/PlusSquareIcon] + "Add Subgroup"])] [ui/Header {:as :h3 :dividing true} "Members"] (if (empty? users) [uix/MsgNoItemsToShow [uix/TR (if editable? :empty-group-message @@ -232,12 +240,84 @@ ^{:key (:id child)} [Group child selected-group])])]])))) +(defn sanitize-name [name] + (when name + (str/lower-case + (str/replace + (str/trim + (str/join "" (re-seq #"[a-zA-Z0-9-_\ ]" name))) + " " "-")))) + +(defn AddGroupButton + [] + (let [tr (subscribe [::i18n-subs/tr]) + show? (r/atom false) + group-name (r/atom "") + group-desc (r/atom "") + validate? (r/atom false) + loading? (r/atom false) + close-fn #(reset! show? false)] + (fn [] + (let [group-identifier (sanitize-name @group-name) + form-valid? (and (s/valid? ::spec/group-name @group-name) + (s/valid? ::spec/group-description @group-desc))] + [ui/Modal + {:open @show? + :close-icon true + :on-close close-fn + :trigger (r/as-element + [ui/Button {:primary true + :size "small" + :icon true + :on-click #(reset! show? true)} + [icons/PlusSquareIcon] (@tr [:add-group])])} + [uix/ModalHeader {:header (@tr [:add-group])}] + [ui/ModalContent + [ui/Message {:hidden (not (and @validate? (not form-valid?))) + :error true} + [ui/MessageHeader (@tr [:validation-error])] + [ui/MessageContent (@tr [:validation-error-message])]] + (when-not (str/blank? group-identifier) + [:i {:style {:padding-left "1ch" + :color :grey}} + [:b "id : "] + (str "group/" group-identifier)]) + [ui/Table style/definition + [ui/TableBody + [uix/TableRowField (@tr [:name]), :required? true, :default-value @group-name, + :validate-form? @validate?, :spec ::spec/group-name, + :on-change #(reset! group-name %)] + [uix/TableRowField (@tr [:description]), :required? true, + :spec ::spec/group-description, :validate-form? @validate?, + :default-value @group-desc, :on-change #(reset! group-desc %)]]]] + [ui/ModalActions + [uix/Button + {:text (@tr [:create]) + :primary true + :disabled (and @validate? (not form-valid?)) + :icon icons/i-info-full + :loading @loading? + :on-click #(if (not form-valid?) + (reset! validate? true) + (do + (reset! show? false) + (dispatch + [::events/add-group group-identifier @group-name @group-desc loading?])))}]]])))) + (defn GroupHierarchySegment [selected-group] (let [groups-hierarchy @(subscribe [::session-subs/groups-hierarchies])] [ui/Segment {:raised true :style {:overflow-x :auto :min-height "100%"}} - [ui/Header {:as :h3} "Groups"] + + [:div {:style {:display :flex + :align-items :baseline + :justify-content :space-between + :flex-wrap :wrap + :padding-bottom "1em"}} + [ui/Header {:as :h3} "Groups"] + [AddGroupButton]] + [full-text-search-plugin/FullTextSearch {:db-path [::deployments-search] :change-event [:a] @@ -251,7 +331,7 @@ [{path :path}] (let [[_ uuid] path selected-group (when uuid - @(subscribe [::session-subs/group (str "group/" uuid)]))] + @(subscribe [::session-subs/group (str "group/" uuid)]))] [ui/Grid {:stackable false} [ui/GridColumn {:stretched true :computer 4 diff --git a/code/src/cljs/sixsq/nuvla/ui/pages/profile/views.cljs b/code/src/cljs/sixsq/nuvla/ui/pages/profile/views.cljs index ee450044e..18982e5e4 100644 --- a/code/src/cljs/sixsq/nuvla/ui/pages/profile/views.cljs +++ b/code/src/cljs/sixsq/nuvla/ui/pages/profile/views.cljs @@ -63,65 +63,6 @@ 1)) -(defn AddGroupButton - [] - (let [tr (subscribe [::i18n-subs/tr]) - show? (r/atom false) - group-name (r/atom "") - group-desc (r/atom "") - validate? (r/atom false) - loading? (r/atom false) - close-fn #(reset! show? false)] - (fn [] - (let [group-identifier (utils-general/sanitize-name @group-name) - form-valid? (and (s/valid? ::spec/group-name @group-name) - (s/valid? ::spec/group-description @group-desc))] - [ui/Modal - {:open @show? - :close-icon true - :on-close close-fn - :trigger (r/as-element - [ui/MenuItem {:on-click #(reset! show? true)} - [icons/UsersIcon] - (str/capitalize (@tr [:add-group]))])} - [uix/ModalHeader {:header (str/capitalize (@tr [:add-group]))}] - [ui/ModalContent - [ui/Message {:hidden (not (and @validate? (not form-valid?))) - :error true} - [ui/MessageHeader (@tr [:validation-error])] - [ui/MessageContent (@tr [:validation-error-message])]] - [ui/Input {:name "name" - :placeholder (@tr [:name]) - :type :text - :error (when (and @validate? - (not (s/valid? ::spec/group-name @group-name))) true) - :fluid true - :on-change (ui-callback/input-callback - #(reset! group-name %))}] - [:br] - [ui/Input {:name "description" - :placeholder (@tr [:description]) - :type :text - :error (when (and @validate? - (not (s/valid? ::spec/group-description @group-desc))) true) - :fluid true - :on-change (ui-callback/input-callback - #(reset! group-desc %))}]] - [ui/ModalActions - [uix/Button - {:text (@tr [:create]) - :primary true - :disabled (and @validate? (not form-valid?)) - :icon icons/i-info-full - :loading @loading? - :on-click #(if (not form-valid?) - (reset! validate? true) - (do - (reset! show? false) - (dispatch - [::events/add-group group-identifier @group-name @group-desc loading?])))}]]])))) - - (defn ModalChangePassword [] (let [open? (subscribe [::subs/modal-open? :change-password]) error (subscribe [::subs/error-message]) @@ -1442,7 +1383,6 @@ :icon "user secret" :content (str/capitalize (@tr [:change-password])) :on-click #(dispatch [::events/open-modal :change-password])}] - [AddGroupButton] (when can-enable-2fa? [ui/MenuItem {:icon "shield" From 595f5b8b7cbd20dddd84ee1c2c9eb82a1daf5a1e Mon Sep 17 00:00:00 2001 From: khaled basbous Date: Wed, 23 Jul 2025 11:52:43 +0300 Subject: [PATCH 09/10] move events to groups --- .../sixsq/nuvla/ui/pages/groups/events.cljs | 65 +++++++++++++++ .../sixsq/nuvla/ui/pages/groups/views.cljs | 23 ++--- .../sixsq/nuvla/ui/pages/profile/events.cljs | 83 +------------------ .../sixsq/nuvla/ui/pages/profile/spec.cljs | 9 +- .../sixsq/nuvla/ui/pages/profile/subs.cljs | 5 -- 5 files changed, 79 insertions(+), 106 deletions(-) create mode 100644 code/src/cljs/sixsq/nuvla/ui/pages/groups/events.cljs diff --git a/code/src/cljs/sixsq/nuvla/ui/pages/groups/events.cljs b/code/src/cljs/sixsq/nuvla/ui/pages/groups/events.cljs new file mode 100644 index 000000000..10d8b66df --- /dev/null +++ b/code/src/cljs/sixsq/nuvla/ui/pages/groups/events.cljs @@ -0,0 +1,65 @@ +(ns sixsq.nuvla.ui.pages.groups.events + (:require [day8.re-frame.http-fx] + [re-frame.core :refer [dispatch reg-event-db reg-event-fx]] + [sixsq.nuvla.ui.cimi-api.effects :as cimi-api-fx] + [sixsq.nuvla.ui.common-components.messages.events :as messages-events] + [sixsq.nuvla.ui.config :as config] + [sixsq.nuvla.ui.session.events :as session-events] + [sixsq.nuvla.ui.session.spec :as session-spec] + [sixsq.nuvla.ui.utils.response :as response])) + +(reg-event-fx + ::add-group + (fn [{_db :db} [_ id name description loading?]] + (let [user {:template {:href "group-template/generic" + :group-identifier id} + :name name + :description description}] + {::cimi-api-fx/add + ["group" user + #(let [{:keys [status message resource-id]} (response/parse %)] + (dispatch [::session-events/search-groups]) + (dispatch [::messages-events/add + {:header (cond-> (str "added " resource-id) + status (str " (" status ")")) + :content message + :type :success}]) + (reset! loading? false))]}))) + +(reg-event-fx + ::edit-group + (fn [_ [_ group]] + (let [id (:id group)] + {::cimi-api-fx/edit [id group #(if (instance? js/Error %) + (let [{:keys [status message]} (response/parse-ex-info %)] + (dispatch [::messages-events/add + {:header (cond-> "Group update failed" + status (str " (" status ")")) + :content message + :type :error}])) + (do + (dispatch [::session-events/search-groups]) + (dispatch [::messages-events/add + {:header "Group updated" + :content "Group updated successfully." + :type :info}])))]}))) + +(reg-event-fx + ::invite-to-group + (fn [{db :db} [_ group-id username]] + (let [on-error #(let [{:keys [status message]} (response/parse-ex-info %)] + (dispatch [::messages-events/add + {:header (cond-> (str "Invitation to " group-id " for " username " failed!") + status (str " (" status ")")) + :content message + :type :error}])) + on-success #(dispatch [::messages-events/add + {:header "Invitation successfully sent to user" + :content (str "User will appear in " group-id + " when he accept the invitation sent to his email address.") + :type :info}]) + data {:username username + :redirect-url (str (::session-spec/server-redirect-uri db) + "?message=join-group-accepted") + :set-password-url (str @config/path-prefix "/set-password")}] + {::cimi-api-fx/operation [group-id "invite" on-success :on-error on-error :data data]}))) \ No newline at end of file diff --git a/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs b/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs index 1c5f2f3ca..218ec1e89 100644 --- a/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs +++ b/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs @@ -1,13 +1,11 @@ (ns sixsq.nuvla.ui.pages.groups.views - (:require ["@stripe/react-stripe-js" :as react-stripe] - [cljs.spec.alpha :as s] + (:require [cljs.spec.alpha :as s] [clojure.string :as str] [re-frame.core :refer [dispatch subscribe]] [reagent.core :as r] [sixsq.nuvla.ui.common-components.i18n.subs :as i18n-subs] [sixsq.nuvla.ui.common-components.plugins.full-text-search :as full-text-search-plugin] - [sixsq.nuvla.ui.pages.profile.events :as events] - [sixsq.nuvla.ui.pages.profile.spec :as spec] + [sixsq.nuvla.ui.pages.groups.events :as events] [sixsq.nuvla.ui.routing.routes :as routes] [sixsq.nuvla.ui.routing.events :as routing-events] [sixsq.nuvla.ui.session.subs :as session-subs] @@ -16,9 +14,13 @@ [sixsq.nuvla.ui.utils.icons :as icons] [sixsq.nuvla.ui.utils.semantic-ui :as ui] [sixsq.nuvla.ui.utils.semantic-ui-extensions :as uix] + [sixsq.nuvla.ui.utils.spec :as us] [sixsq.nuvla.ui.utils.style :as style] [sixsq.nuvla.ui.utils.ui-callback :as ui-callback])) +(s/def ::group-name us/nonblank-string) +(s/def ::group-description us/nonblank-string) + (defn ConfirmActionModal [{:keys [on-confirm header Content Icon]}] (let [tr (subscribe [::i18n-subs/tr])] @@ -259,8 +261,8 @@ close-fn #(reset! show? false)] (fn [] (let [group-identifier (sanitize-name @group-name) - form-valid? (and (s/valid? ::spec/group-name @group-name) - (s/valid? ::spec/group-description @group-desc))] + form-valid? (and (s/valid? ::group-name @group-name) + (s/valid? ::group-description @group-desc))] [ui/Modal {:open @show? :close-icon true @@ -279,23 +281,22 @@ [ui/MessageContent (@tr [:validation-error-message])]] (when-not (str/blank? group-identifier) [:i {:style {:padding-left "1ch" - :color :grey}} + :color :grey}} [:b "id : "] (str "group/" group-identifier)]) [ui/Table style/definition [ui/TableBody [uix/TableRowField (@tr [:name]), :required? true, :default-value @group-name, - :validate-form? @validate?, :spec ::spec/group-name, + :validate-form? @validate?, :spec ::group-name, :on-change #(reset! group-name %)] [uix/TableRowField (@tr [:description]), :required? true, - :spec ::spec/group-description, :validate-form? @validate?, + :spec ::group-description, :validate-form? @validate?, :default-value @group-desc, :on-change #(reset! group-desc %)]]]] [ui/ModalActions [uix/Button - {:text (@tr [:create]) + {:text (str/capitalize (@tr [:add])) :primary true :disabled (and @validate? (not form-valid?)) - :icon icons/i-info-full :loading @loading? :on-click #(if (not form-valid?) (reset! validate? true) diff --git a/code/src/cljs/sixsq/nuvla/ui/pages/profile/events.cljs b/code/src/cljs/sixsq/nuvla/ui/pages/profile/events.cljs index f73b37750..a368b55d8 100644 --- a/code/src/cljs/sixsq/nuvla/ui/pages/profile/events.cljs +++ b/code/src/cljs/sixsq/nuvla/ui/pages/profile/events.cljs @@ -8,13 +8,11 @@ [sixsq.nuvla.ui.common-components.i18n.spec :as i18n-spec] [sixsq.nuvla.ui.common-components.messages.events :as messages-events] [sixsq.nuvla.ui.common-components.plugins.audit-log :as audit-log-plugin] - [sixsq.nuvla.ui.config :as config] [sixsq.nuvla.ui.main.spec :as main-spec] [sixsq.nuvla.ui.pages.profile.effects :as fx] [sixsq.nuvla.ui.pages.profile.spec :as spec] [sixsq.nuvla.ui.routing.events :as routing-events] [sixsq.nuvla.ui.routing.routes :as routes] - [sixsq.nuvla.ui.session.events :as session-events] [sixsq.nuvla.ui.session.spec :as session-spec] [sixsq.nuvla.ui.session.utils :as session-utils] [sixsq.nuvla.ui.utils.general :as general-utils] @@ -31,16 +29,6 @@ [:dispatch [::audit-log-plugin/load-events [::spec/events] {:event-name event-names} false]]]})) -(reg-event-db - ::add-group-member - (fn [db [_ member]] - (update-in db [::spec/group :users] #(conj % member)))) - -(reg-event-db - ::remove-group-member - (fn [db [_ member]] - (update-in db [::spec/group :users] #(vec (disj (set %) member))))) - (reg-event-db ::set-user (fn [{:keys [::spec/loading] :as db} [_ user]] @@ -52,79 +40,10 @@ (fn [{{:keys [::session-spec/session] :as db} :db} _] (when-let [user (session-utils/get-user-id session)] (let [is-group? (-> session session-utils/get-active-claim session-utils/is-group?)] - (cond-> {:fx [(when is-group? [:dispatch [::get-group]])]} + (cond-> {} (not is-group?) (assoc ::cimi-api-fx/get [user #(do (dispatch [::set-user %]))] :db (update db ::spec/loading conj :user))))))) -(reg-event-fx - ::add-group - (fn [{_db :db} [_ id name description loading?]] - (let [user {:template {:href "group-template/generic" - :group-identifier id} - :name name - :description description}] - {::cimi-api-fx/add - ["group" user - #(let [{:keys [status message resource-id]} (response/parse %)] - (dispatch [::session-events/search-groups]) - (dispatch [::messages-events/add - {:header (cond-> (str "added " resource-id) - status (str " (" status ")")) - :content message - :type :success}]) - (reset! loading? false))]}))) - -(reg-event-db - ::set-group - (fn [{:keys [::spec/loading] :as db} [_ group]] - (assoc db ::spec/group group - ::spec/loading (disj loading :group)))) - -(reg-event-fx - ::get-group - (fn [{{:keys [::session-spec/session] :as db} :db}] - (when-let [group (session-utils/get-active-claim session)] - {:db (update db ::spec/loading conj :group) - ::cimi-api-fx/get [group #(dispatch [::set-group %])]}))) - -(reg-event-fx - ::edit-group - (fn [_ [_ group]] - (let [id (:id group)] - {::cimi-api-fx/edit [id group #(if (instance? js/Error %) - (let [{:keys [status message]} (response/parse-ex-info %)] - (dispatch [::messages-events/add - {:header (cond-> "Group update failed" - status (str " (" status ")")) - :content message - :type :error}])) - (do - (dispatch [::session-events/search-groups]) - (dispatch [::messages-events/add - {:header "Group updated" - :content "Group updated successfully." - :type :info}])))]}))) - -(reg-event-fx - ::invite-to-group - (fn [{db :db} [_ group-id username]] - (let [on-error #(let [{:keys [status message]} (response/parse-ex-info %)] - (dispatch [::messages-events/add - {:header (cond-> (str "Invitation to " group-id " for " username " failed!") - status (str " (" status ")")) - :content message - :type :error}])) - on-success #(dispatch [::messages-events/add - {:header "Invitation successfully sent to user" - :content (str "User will appear in " group-id - " when he accept the invitation sent to his email address.") - :type :info}]) - data {:username username - :redirect-url (str (::session-spec/server-redirect-uri db) - "?message=join-group-accepted") - :set-password-url (str @config/path-prefix "/set-password")}] - {::cimi-api-fx/operation [group-id "invite" on-success :on-error on-error :data data]}))) - (reg-event-fx ::get-customer (fn [{db :db} [_ id]] diff --git a/code/src/cljs/sixsq/nuvla/ui/pages/profile/spec.cljs b/code/src/cljs/sixsq/nuvla/ui/pages/profile/spec.cljs index 820fb3502..b49b5125d 100644 --- a/code/src/cljs/sixsq/nuvla/ui/pages/profile/spec.cljs +++ b/code/src/cljs/sixsq/nuvla/ui/pages/profile/spec.cljs @@ -1,8 +1,7 @@ (ns sixsq.nuvla.ui.pages.profile.spec (:require [clojure.spec.alpha :as s] [sixsq.nuvla.ui.common-components.plugins.audit-log :as audit-log-plugin] - [sixsq.nuvla.ui.common-components.plugins.nav-tab :as nav-tab] - [sixsq.nuvla.ui.utils.spec :as us])) + [sixsq.nuvla.ui.common-components.plugins.nav-tab :as nav-tab])) (s/def ::user any?) (s/def ::customer any?) @@ -17,11 +16,6 @@ (s/def ::loading set?) (s/def ::setup-intent any?) (s/def ::vendor any?) -(s/def ::group any?) -(s/def ::group-name us/nonblank-string) -(s/def ::group-description us/nonblank-string) -(s/def ::group-form (s/keys :req [::group-name - ::group-description])) (s/def ::tab any?) (s/def ::two-factor-step (s/nilable string?)) (s/def ::two-factor-enable? boolean?) @@ -43,7 +37,6 @@ ::error-message nil ::loading #{} ::vendor nil - ::group nil ::tab (nav-tab/build-spec) ::two-factor-step :install-app ::two-factor-enable? true diff --git a/code/src/cljs/sixsq/nuvla/ui/pages/profile/subs.cljs b/code/src/cljs/sixsq/nuvla/ui/pages/profile/subs.cljs index b5ce01b68..d3e267c87 100644 --- a/code/src/cljs/sixsq/nuvla/ui/pages/profile/subs.cljs +++ b/code/src/cljs/sixsq/nuvla/ui/pages/profile/subs.cljs @@ -198,11 +198,6 @@ (fn [db] (::spec/vendor db))) -(reg-sub - ::group - (fn [db] - (::spec/group db))) - (reg-sub ::two-factor-step (fn [db] From ee8b7296cbc44e568d523622267b86c467a36da1 Mon Sep 17 00:00:00 2001 From: khaled basbous Date: Thu, 24 Jul 2025 07:45:28 +0300 Subject: [PATCH 10/10] Add group and add subgroup --- .../sixsq/nuvla/ui/pages/groups/events.cljs | 36 +++-- .../sixsq/nuvla/ui/pages/groups/views.cljs | 141 +++++++++--------- 2 files changed, 94 insertions(+), 83 deletions(-) diff --git a/code/src/cljs/sixsq/nuvla/ui/pages/groups/events.cljs b/code/src/cljs/sixsq/nuvla/ui/pages/groups/events.cljs index 10d8b66df..ef7000eb9 100644 --- a/code/src/cljs/sixsq/nuvla/ui/pages/groups/events.cljs +++ b/code/src/cljs/sixsq/nuvla/ui/pages/groups/events.cljs @@ -10,21 +10,27 @@ (reg-event-fx ::add-group - (fn [{_db :db} [_ id name description loading?]] - (let [user {:template {:href "group-template/generic" - :group-identifier id} - :name name - :description description}] - {::cimi-api-fx/add - ["group" user - #(let [{:keys [status message resource-id]} (response/parse %)] - (dispatch [::session-events/search-groups]) - (dispatch [::messages-events/add - {:header (cond-> (str "added " resource-id) - status (str " (" status ")")) - :content message - :type :success}]) - (reset! loading? false))]}))) + (fn [{_db :db} [_ {:keys [parent-group group-identifier group-name group-desc loading?]}]] + (let [on-success #(let [{:keys [status message resource-id]} (response/parse %)] + (dispatch [::session-events/search-groups]) + (dispatch [::messages-events/add + {:header (cond-> (str "added " resource-id) + status (str " (" status ")")) + :content message + :type :success}]) + (reset! loading? false))] + (if parent-group + {::cimi-api-fx/operation + [(:id parent-group) "add-subgroup" + on-success + :data {:group-identifier group-identifier + :name group-name + :description group-desc}]} + {::cimi-api-fx/add + ["group" {:template {:href "group-template/generic" + :group-identifier group-identifier} + :name group-name + :description group-desc} on-success]})))) (reg-event-fx ::edit-group diff --git a/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs b/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs index 218ec1e89..b09741e3e 100644 --- a/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs +++ b/code/src/cljs/sixsq/nuvla/ui/pages/groups/views.cljs @@ -178,6 +178,74 @@ :value (or @invite-user "") :on-change (ui-callback/value #(reset! invite-user %))}])))) +(defn sanitize-name [name] + (when name + (str/lower-case + (str/replace + (str/trim + (str/join "" (re-seq #"[a-zA-Z0-9-_\ ]" name))) + " " "-")))) + +(defn AddGroupButton + [_opts] + (let [tr (subscribe [::i18n-subs/tr]) + show? (r/atom false) + group-name (r/atom "") + group-desc (r/atom "") + validate? (r/atom false) + loading? (r/atom false) + close-fn #(reset! show? false)] + (fn [{:keys [parent-group header]}] + (let [group-identifier (sanitize-name @group-name) + form-valid? (and (s/valid? ::group-name @group-name) + (s/valid? ::group-description @group-desc))] + [ui/Modal + {:open @show? + :close-icon true + :on-close close-fn + :trigger (r/as-element + [ui/Button {:secondary true + :size "small" + :icon true + :on-click #(reset! show? true)} + [icons/PlusSquareIcon] + header])} + [uix/ModalHeader {:header header}] + [ui/ModalContent + [ui/Message {:hidden (not (and @validate? (not form-valid?))) + :error true} + [ui/MessageHeader (@tr [:validation-error])] + [ui/MessageContent (@tr [:validation-error-message])]] + (when-not (str/blank? group-identifier) + [:i {:style {:padding-left "1ch" + :color :grey}} + [:b "id : "] + (str "group/" group-identifier)]) + [ui/Table style/definition + [ui/TableBody + [uix/TableRowField (@tr [:name]), :required? true, :default-value @group-name, + :validate-form? @validate?, :spec ::group-name, + :on-change #(reset! group-name %)] + [uix/TableRowField (@tr [:description]), :required? true, + :spec ::group-description, :validate-form? @validate?, + :default-value @group-desc, :on-change #(reset! group-desc %)]]]] + [ui/ModalActions + [uix/Button + {:text (str/capitalize (@tr [:add])) + :primary true + :disabled (and @validate? (not form-valid?)) + :loading @loading? + :on-click #(if (not form-valid?) + (reset! validate? true) + (do + (reset! show? false) + (dispatch + [::events/add-group {:parent-group parent-group + :group-identifier group-identifier + :group-name @group-name + :group-desc @group-desc + :loading? loading?}])))}]]])))) + (defn GroupMembers [group] (let [editable? (utils-general/editable? group false)] @@ -195,9 +263,8 @@ group-name [ui/HeaderSubheader description " (" id ")"]]] (when (utils-general/can-operation? "add-subgroup" group) - [ui/Button {:secondary true :size "small" :icon true} - [icons/PlusSquareIcon] - "Add Subgroup"])] + [AddGroupButton {:header "Add Subgroup" + :parent-group group}])] [ui/Header {:as :h3 :dividing true} "Members"] (if (empty? users) [uix/MsgNoItemsToShow [uix/TR (if editable? :empty-group-message @@ -242,72 +309,10 @@ ^{:key (:id child)} [Group child selected-group])])]])))) -(defn sanitize-name [name] - (when name - (str/lower-case - (str/replace - (str/trim - (str/join "" (re-seq #"[a-zA-Z0-9-_\ ]" name))) - " " "-")))) - -(defn AddGroupButton - [] - (let [tr (subscribe [::i18n-subs/tr]) - show? (r/atom false) - group-name (r/atom "") - group-desc (r/atom "") - validate? (r/atom false) - loading? (r/atom false) - close-fn #(reset! show? false)] - (fn [] - (let [group-identifier (sanitize-name @group-name) - form-valid? (and (s/valid? ::group-name @group-name) - (s/valid? ::group-description @group-desc))] - [ui/Modal - {:open @show? - :close-icon true - :on-close close-fn - :trigger (r/as-element - [ui/Button {:primary true - :size "small" - :icon true - :on-click #(reset! show? true)} - [icons/PlusSquareIcon] (@tr [:add-group])])} - [uix/ModalHeader {:header (@tr [:add-group])}] - [ui/ModalContent - [ui/Message {:hidden (not (and @validate? (not form-valid?))) - :error true} - [ui/MessageHeader (@tr [:validation-error])] - [ui/MessageContent (@tr [:validation-error-message])]] - (when-not (str/blank? group-identifier) - [:i {:style {:padding-left "1ch" - :color :grey}} - [:b "id : "] - (str "group/" group-identifier)]) - [ui/Table style/definition - [ui/TableBody - [uix/TableRowField (@tr [:name]), :required? true, :default-value @group-name, - :validate-form? @validate?, :spec ::group-name, - :on-change #(reset! group-name %)] - [uix/TableRowField (@tr [:description]), :required? true, - :spec ::group-description, :validate-form? @validate?, - :default-value @group-desc, :on-change #(reset! group-desc %)]]]] - [ui/ModalActions - [uix/Button - {:text (str/capitalize (@tr [:add])) - :primary true - :disabled (and @validate? (not form-valid?)) - :loading @loading? - :on-click #(if (not form-valid?) - (reset! validate? true) - (do - (reset! show? false) - (dispatch - [::events/add-group group-identifier @group-name @group-desc loading?])))}]]])))) - (defn GroupHierarchySegment [selected-group] - (let [groups-hierarchy @(subscribe [::session-subs/groups-hierarchies])] + (let [tr @(subscribe [::i18n-subs/tr]) + groups-hierarchy @(subscribe [::session-subs/groups-hierarchies])] [ui/Segment {:raised true :style {:overflow-x :auto :min-height "100%"}} @@ -317,7 +322,7 @@ :flex-wrap :wrap :padding-bottom "1em"}} [ui/Header {:as :h3} "Groups"] - [AddGroupButton]] + [AddGroupButton {:header (tr [:add-group])}]] [full-text-search-plugin/FullTextSearch {:db-path [::deployments-search]