diff --git a/env/dev/cljs/commiteth/dev.cljs b/env/dev/cljs/commiteth/dev.cljs index ad14423..aec7204 100644 --- a/env/dev/cljs/commiteth/dev.cljs +++ b/env/dev/cljs/commiteth/dev.cljs @@ -7,7 +7,7 @@ (figwheel/watch-and-reload :websocket-url "ws://localhost:3449/figwheel-ws" - :on-jsload core/mount-components) + :on-jsload core/on-js-load) (devtools/install!) diff --git a/project.clj b/project.clj index db676a9..010ce46 100644 --- a/project.clj +++ b/project.clj @@ -37,8 +37,9 @@ [tentacles "0.5.1"] [re-frisk "0.3.2"] [bk/ring-gzip "0.2.1"] - ;; - ] + [crypto-random "1.2.0"] + [crypto-equality "1.0.0"] + [cheshire "5.7.0"]] :min-lein-version "2.0.0" :source-paths ["src/clj" "src/cljc"] diff --git a/resources/migrations/20170215174652-repo-enable-state.down.sql b/resources/migrations/20170215174652-repo-enable-state.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/resources/migrations/20170215174652-repo-enable-state.up.sql b/resources/migrations/20170215174652-repo-enable-state.up.sql new file mode 100644 index 0000000..411ae1d --- /dev/null +++ b/resources/migrations/20170215174652-repo-enable-state.up.sql @@ -0,0 +1,15 @@ + +CREATE TABLE "public"."repo_state" ( + "id" int, + "description" character varying(32) NOT NULL, + PRIMARY KEY ("id") +); + +INSERT INTO "public"."repo_state"("id", "description") VALUES(0, 'Not enabled'); +INSERT INTO "public"."repo_state"("id", "description") VALUES(1, 'Creating hook'); +INSERT INTO "public"."repo_state"("id", "description") VALUES(2, 'Enabled'); +INSERT INTO "public"."repo_state"("id", "description") VALUES(-1, 'Failed to create hook'); + +ALTER TABLE "public"."repositories" DROP COLUMN "enabled"; +ALTER TABLE "public"."repositories" + ADD COLUMN "state" int NOT NULL DEFAULT 0; diff --git a/resources/migrations/20170215194951-repo-hook-secret.down.sql b/resources/migrations/20170215194951-repo-hook-secret.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/resources/migrations/20170215194951-repo-hook-secret.up.sql b/resources/migrations/20170215194951-repo-hook-secret.up.sql new file mode 100644 index 0000000..13df082 --- /dev/null +++ b/resources/migrations/20170215194951-repo-hook-secret.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE "public"."repositories" + ADD COLUMN "hook_secret" character varying(64); diff --git a/resources/sql/queries.sql b/resources/sql/queries.sql index 3a5aa30..b16dbc6 100644 --- a/resources/sql/queries.sql +++ b/resources/sql/queries.sql @@ -50,38 +50,58 @@ WHERE r.repo_id = :repo_id; -- Repositories -------------------------------------------------------------------- --- :name toggle-repository! : repo - (rename-keys {:id :repo_id :name :repo}) - (merge {:enabled true}))))) + (or + (db/create-repository! con-db (-> repo + (rename-keys {:id :repo_id + :name :repo}) + (merge {:state 0}))) + (db/get-repo {:repo (:name repo) + :login (:login repo)})))) (defn get-enabled "Lists enabled repositories ids for a given login" @@ -25,8 +24,16 @@ (db/get-enabled-repositories con-db {:user_id user-id})) (mapcat vals))) -(defn update-hook-id - "Updates github webhook id for a given repository" - [repo-id hook-id] +(defn update-repo + [repo-id updates] (jdbc/with-db-connection [con-db *db*] - (db/update-hook-id con-db {:repo_id repo-id :hook_id hook-id}))) + (db/update-repo-generic con-db {:repo_id repo-id + :updates updates}))) + + +(defn get-repo + "Get a repo from DB given it's full name (owner/repo-name)" + [full-name] + (let [[login repo-name] (str/split full-name #"/")] + (jdbc/with-db-connection [con-db *db*] + (db/get-repo {:login login :repo repo-name})))) diff --git a/src/clj/commiteth/eth/core.clj b/src/clj/commiteth/eth/core.clj index 04d3881..a2d7b3d 100644 --- a/src/clj/commiteth/eth/core.clj +++ b/src/clj/commiteth/eth/core.clj @@ -87,7 +87,6 @@ owner2 (format-param owner) data (str contract-code owner1 owner2) value (format "0x%x" 1)] - (println data) (send-transaction (eth-account) nil value {:data data}))) (defn- format-call-params diff --git a/src/clj/commiteth/github/core.clj b/src/clj/commiteth/github/core.clj index 7554e24..5cc87d6 100644 --- a/src/clj/commiteth/github/core.clj +++ b/src/clj/commiteth/github/core.clj @@ -41,7 +41,7 @@ :redirect_uri (redirect-uri) :state state}})) -(defn- auth-params +(defn auth-params [token] {:oauth-token token :client-id (client-id) @@ -64,19 +64,22 @@ :permissions :private]) -(def login-field [:owner :login]) - -(defn list-repos - "List all repos managed by the given user." +(defn get-user-repos + "List all repos managed by the given user. Returns map of repository + maps grouped by owner. Owner is an organization name or the user's + own login." [token] - (->> - (map #(merge - {:login (get-in % login-field)} - (select-keys % repo-fields)) - (repos/repos (merge (auth-params token) {:type "all" - :all-pages true}))) - (filter #(not (:fork %))) - (filter #(-> % :permissions :admin)))) + (let [all-repos-with-admin-access + (->> + (map #(merge + {:owner-login (get-in % [:owner :login])} + {:owner-type (get-in % [:owner :type])} + (select-keys % repo-fields)) + (repos/repos (merge (auth-params token) {:type "all" + :all-pages true}))) + (filter #(not (:fork %))) + (filter #(-> % :permissions :admin)))] + (group-by :owner-login all-repos-with-admin-access))) (defn get-user [token] @@ -91,12 +94,13 @@ :email))) (defn add-webhook - [full-repo token] + [full-repo token secret] (log/debug "adding webhook" full-repo token) ; TODO: use secret key in config param (let [[user repo] (str/split full-repo #"/")] (repos/create-hook user repo "web" {:url (str (server-address) "/webhook") + :secret secret :content_type "json"} (merge (auth-params token) {:events ["issues", "issue_comment", "pull_request"] diff --git a/src/clj/commiteth/handler.clj b/src/clj/commiteth/handler.clj index 1d94959..732fb42 100644 --- a/src/clj/commiteth/handler.clj +++ b/src/clj/commiteth/handler.clj @@ -23,9 +23,7 @@ (wrap-routes middleware/wrap-csrf) (wrap-routes middleware/wrap-formats)) #'redirect-routes - (-> #'webhook-routes - (wrap-routes wrap-json-params) - (wrap-routes wrap-keyword-params)) + #'webhook-routes #'service-routes #'qr-routes (route/not-found diff --git a/src/clj/commiteth/routes/services.clj b/src/clj/commiteth/routes/services.clj index 1cd569a..84d262c 100644 --- a/src/clj/commiteth/routes/services.clj +++ b/src/clj/commiteth/routes/services.clj @@ -11,7 +11,8 @@ [commiteth.bounties :as bounties] [commiteth.github.core :as github] [clojure.tools.logging :as log] - [commiteth.eth.core :as eth])) + [commiteth.eth.core :as eth] + [crypto.random :as random])) (defn access-error [_ _] (unauthorized {:error "unauthorized"})) @@ -28,6 +29,65 @@ [_ binding acc] (update-in acc [:letks] into [binding `(:identity ~'+compojure-api-request+)])) + +(defn enable-repo [repo-id repo login token] + (log/debug "enable-repo" repo-id repo) + (let [hook-secret (random/base64 32)] + (try + (repositories/update-repo repo-id {:state 1 + :hook_secret hook-secret}) + (let [created-hook (github/add-webhook repo token hook-secret)] + (log/debug "Created webhook:" created-hook) + (github/create-label repo token) + (repositories/update-repo repo-id {:state 2 + :hook_id (:id created-hook)}) + (bounties/add-bounties-for-existing-issues repo repo-id login)) + (catch Exception e + (log/info "exception when creating webhook" (.getMessage e)) + (repositories/update-repo repo-id {:state -1}))))) + + +(defn disable-repo [repo-id repo hook-id token] + (log/debug "disable-repo" repo-id repo) + (do + (github/remove-webhook repo hook-id token) + (repositories/update-repo repo-id {:hook_secret "" + :state 0 + :hook_id nil}))) + + +(defn handle-toggle-repo [user params] + (log/debug "handle-toggle-repo" user params) + (let [{token :token + login :login + user-id :id} user + {repo-id :id + repo :full_name} params + db-item (repositories/create (merge params {:user_id user-id + :login login})) + is-enabled (= 2 (:state db-item))] + (if is-enabled + (disable-repo repo-id repo (:hook_id db-item) token) + (enable-repo repo-id repo login token)) + (merge + {:enabled (not is-enabled)} + (select-keys params [:id :full_name])))) + +(defn in? [coll elem] + (some #(= elem %) coll)) + +(defn handle-get-user-repos [user] + (log/debug "handle-get-user-repos") + (let [github-repos (github/get-user-repos (:token user)) + enabled-repos (vec (repositories/get-enabled (:id user))) + repo-enabled? (fn [repo] (in? enabled-repos (:id repo))) + update-enabled (fn [repo] (assoc repo :enabled (repo-enabled? repo)))] + (into {} + (map (fn [[group repos]] {group + (map update-enabled repos)}) + github-repos)))) + + (defapi service-routes {:swagger {:ui "/swagger-ui" :spec "/swagger.json" @@ -57,10 +117,7 @@ (GET "/repositories" [] :auth-rules authenticated? :current-user user - (ok {:repositories (->> (github/list-repos (:token user)) - (map #(select-keys % - [:id :html_url - :name :full_name :description])))})) + (ok {:repositories (handle-get-user-repos user)})) (GET "/enabled-repositories" [] :auth-rules authenticated? :current-user user @@ -86,20 +143,4 @@ (POST "/repository/toggle" {:keys [params]} :auth-rules authenticated? :current-user user - (ok (let [{repo-id :id - repo :full_name} params - {token :token - login :login - user-id :id} user - result (or - (repositories/create (merge params {:user_id user-id})) - (repositories/toggle repo-id))] - (if (:enabled result) - (let [created-hook (github/add-webhook repo token)] - (log/debug "Created webhook:" created-hook) - (future - (github/create-label repo token) - (repositories/update-hook-id repo-id (:id created-hook)) - (bounties/add-bounties-for-existing-issues result))) - (github/remove-webhook repo (:hook_id result) token)) - result)))))) + (ok (handle-toggle-repo user params)))))) diff --git a/src/clj/commiteth/routes/webhooks.clj b/src/clj/commiteth/routes/webhooks.clj index ebc665a..979bbd7 100644 --- a/src/clj/commiteth/routes/webhooks.clj +++ b/src/clj/commiteth/routes/webhooks.clj @@ -1,15 +1,19 @@ (ns commiteth.routes.webhooks - (:require [compojure.core :refer [defroutes POST]] - [commiteth.github.core :as github] - [commiteth.db.pull-requests :as pull-requests] - [commiteth.db.issues :as issues] - [commiteth.db.users :as users] + (:require [cheshire.core :as json] + [clojure.string :as str :refer [join]] + [clojure.tools.logging :as log] [commiteth.bounties :as bounties] - [ring.util.http-response :refer [ok]] - [clojure.string :refer [join]] - [clojure.tools.logging :as log]) - (:import [java.lang Integer])) - + [commiteth.db + [issues :as issues] + [pull-requests :as pull-requests] + [repositories :as repos] + [users :as users]] + [commiteth.github.core :as github] + [commiteth.util.digest :refer [hex-hmac-sha1]] + [compojure.core :refer [defroutes POST]] + [crypto.equality :as crypto] + [ring.util.http-response :refer [ok]]) + (:import java.lang.Integer)) (defn find-issue-event [events type owner] @@ -125,15 +129,28 @@ (handle-issue-closed webhook-payload))) (ok (str webhook-payload))) + (defn handle-pull-request [pull-request] (when (= "closed" (:action pull-request)) (handle-pull-request-closed pull-request)) (ok (str pull-request))) + +(defn validate-secret [webhook-payload raw-payload github-signature] + (let [full-name (get-in webhook-payload [:repository :full_name]) + repo (repos/get-repo full-name) + secret (:hook_secret repo) + signature (str "sha1=" (hex-hmac-sha1 secret raw-payload))] + (crypto/eq? signature github-signature))) + + (defroutes webhook-routes - (POST "/webhook" {:keys [params headers]} - (case (get headers "x-github-event") - "issues" (handle-issue params) - "pull_request" (handle-pull-request params) - (ok)))) + (POST "/webhook" {:keys [headers body]} + (let [raw-payload (slurp body) + payload (json/parse-string raw-payload true)] + (if (validate-secret payload raw-payload (get headers "x-hub-signature")) + (case (get headers "x-github-event") + "issues" (handle-issue payload) + "pull_request" (handle-pull-request payload) + (ok)))))) diff --git a/src/cljs/commiteth/bounties.cljs b/src/cljs/commiteth/bounties.cljs new file mode 100644 index 0000000..ff17b15 --- /dev/null +++ b/src/cljs/commiteth/bounties.cljs @@ -0,0 +1,6 @@ +(ns commiteth.bounties + (:require [re-frame.core :as rf])) + +(defn bounties-page [] + (fn [] + [:div "Bounties view not implemented"])) diff --git a/src/cljs/commiteth/core.cljs b/src/cljs/commiteth/core.cljs index fb76fd0..5af3efa 100644 --- a/src/cljs/commiteth/core.cljs +++ b/src/cljs/commiteth/core.cljs @@ -10,6 +10,8 @@ [commiteth.handlers] [commiteth.subscriptions] [commiteth.activity :refer [activity-page]] + [commiteth.repos :refer [repos-page]] + [commiteth.bounties :refer [bounties-page]] [commiteth.manage :refer [manage-page]] [commiteth.issues :refer [issues-page]] [commiteth.common :refer [input checkbox]] @@ -19,7 +21,7 @@ [re-frisk.core :refer [enable-re-frisk!]]) (:import goog.History)) -(defn error-pane +#_(defn error-pane [] (let [error (rf/subscribe [:error])] (fn [] @@ -31,12 +33,12 @@ :on-click #(rf/dispatch [:clear-error])} (str @error)])))) -(defn save-address +#_(defn save-address [user-id address] (fn [_] (rf/dispatch [:save-user-address user-id address]))) -(defn address-settings [] +#_(defn address-settings [] (let [user (rf/subscribe [:user]) user-id (:id @user) address (rf/subscribe [:get-in [:user :address]])] @@ -92,8 +94,8 @@ (fn [] (let [tabs (apply conj [[:activity "Activity"]] (when @user - [[:issues "Bounties"] - [:manage "Repositories"]]))] + [[:repos "Repositories"] + [:bounties "Bounties"]]))] (into [:div.ui.attached.tabular.menu.tiny] (for [[page caption] tabs] (let [props {:class (str "ui item" @@ -118,20 +120,21 @@ [:h2.ui.header "Commit ETH"] [:h2.ui.subheader "Earn ETH by committing to open source projects"] [:div.ui.divider.hidden]]) - [tabs]]]))) + [tabs]]]))) (def pages {:activity #'activity-page - :issues #'issues-page - :manage #'manage-page}) + :repos #'repos-page + :bounties #'bounties-page}) (defn page [] (fn [] [:div.ui.pusher [page-header] - [error-pane] - [:div.ui.vertical.segment - [(pages @(rf/subscribe [:page]))]]])) +;; [error-pane] + [:div.ui.vertical.segment.foo + [:div.page-content + [(pages @(rf/subscribe [:page]))]]]])) (secretary/set-config! :prefix "#") @@ -140,7 +143,7 @@ (secretary/defroute "/manage" [] (if js/user - (rf/dispatch [:set-active-page :manage]) + (rf/dispatch [:set-active-page :repos]) (secretary/dispatch! "/"))) (defn hook-browser-navigation! [] @@ -154,12 +157,18 @@ (defn mount-components [] (r/render [#'page] (.getElementById js/document "app"))) +(defonce active-user (r/atom nil)) + (defn load-user [] - (when-let [login js/user] - (rf/dispatch [:set-active-user - {:login login - :id (js/parseInt js/userId) - :token js/token}]))) + (if-let [login js/user] + (when-not (= login @active-user) + (println "active user changed, loading user data") + (reset! active-user login) + (rf/dispatch [:set-active-user + {:login login + :id (js/parseInt js/userId) + :token js/token}])) + (reset! active-user nil))) (defn load-issues [] (rf/dispatch [:load-bounties])) @@ -168,7 +177,13 @@ (load-issues) (load-user)) -(js/setInterval load-data 60000) +(defonce timer-id (r/atom nil)) + +(defn on-js-load [] + (when-not (nil? @timer-id) + (js/clearInterval @timer-id)) + (reset! timer-id (js/setInterval load-data 60000)) + (mount-components)) (defn init! [] (rf/dispatch-sync [:initialize-db]) @@ -177,4 +192,4 @@ (load-interceptors!) (hook-browser-navigation!) (load-data) - (mount-components)) + (on-js-load)) diff --git a/src/cljs/commiteth/db.cljs b/src/cljs/commiteth/db.cljs index 4f05233..1c0b01e 100644 --- a/src/cljs/commiteth/db.cljs +++ b/src/cljs/commiteth/db.cljs @@ -1,11 +1,10 @@ (ns commiteth.db) (def default-db - {:page :issues + {:page :activity :user nil :repos-loading? false - :repos [] - :enabled-repos {} + :repos {} :all-bounties [] :owner-bounties [] :error nil diff --git a/src/cljs/commiteth/handlers.cljs b/src/cljs/commiteth/handlers.cljs index 2c93c4e..6ca97e6 100644 --- a/src/cljs/commiteth/handlers.cljs +++ b/src/cljs/commiteth/handlers.cljs @@ -5,159 +5,176 @@ [cuerdas.core :as str])) (reg-fx - :http - (fn [{:keys [method url on-success params]}] - (method url - {:headers {"Accept" "application/transit+json"} - :handler on-success - :params params}))) + :http + (fn [{:keys [method url on-success on-error finally params]}] + (method url + {:headers {"Accept" "application/transit+json"} + :handler on-success + :error-handler on-error + :finally finally + :params params}))) (reg-event-db - :initialize-db - (fn [_ _] - db/default-db)) + :initialize-db + (fn [_ _] + db/default-db)) (reg-event-db - :assoc-in - (fn [db [_ path value]] - (assoc-in db path value))) + :assoc-in + (fn [db [_ path value]] + (assoc-in db path value))) (reg-event-db - :set-error - (fn [db [_ text]] - (assoc db :error text))) + :set-error + (fn [db [_ text]] + (assoc db :error text))) (reg-event-db - :clear-error - (fn [db _] - (dissoc db :error))) + :clear-error + (fn [db _] + (dissoc db :error))) (reg-event-db - :set-active-page - (fn [db [_ page]] - (assoc db :page page))) + :set-active-page + (fn [db [_ page]] + (assoc db :page page))) (reg-event-db - :set-page - (fn [db [_ table page]] - (assoc-in db [:pagination table :page] page))) + :set-page + (fn [db [_ table page]] + (assoc-in db [:pagination table :page] page))) (reg-event-db - :init-pagination - (fn [db [_ bounties]] - (let [{page-size :page-size} (:pagination-props db)] - (assoc-in db [:pagination :all-bounties] - {:page 0 - :pages (Math/ceil (/ (count bounties) page-size))})))) + :init-pagination + (fn [db [_ bounties]] + (let [{page-size :page-size} (:pagination-props db)] + (assoc-in db [:pagination :all-bounties] + {:page 0 + :pages (Math/ceil (/ (count bounties) page-size))})))) (reg-event-fx - :set-active-user - (fn [{:keys [db]} [_ user]] - {:db (assoc db :user user) - :dispatch-n [[:load-user-profile] - [:load-user-repos] - [:load-enabled-repos] - [:load-owner-bounties]]})) + :set-active-user + (fn [{:keys [db]} [_ user]] + {:db (assoc db :user user) + :dispatch-n [[:load-user-profile] + [:load-user-repos] + [:load-owner-bounties]]})) (reg-event-fx - :sign-out - (fn [{:keys [db]} [_]] - {:db (assoc db :user nil) - :http {:method GET - :url "/logout"}})) + :sign-out + (fn [{:keys [db]} [_]] + {:db (assoc db :user nil) + :http {:method GET + :url "/logout"}})) (reg-event-fx - :load-bounties - (fn [{:keys [db]} [_]] - {:db db - :http {:method GET - :url "/api/bounties/all" - :on-success #(dispatch [:set-bounties %])}})) + :load-bounties + (fn [{:keys [db]} [_]] + {:db db + :http {:method GET + :url "/api/bounties/all" + :on-success #(dispatch [:set-bounties %])}})) (reg-event-fx - :save-payout-hash - (fn [{:keys [db]} [_ issue-id payout-hash]] - {:db db - :http {:method POST - :url (str/format "/api/user/bounty/%s/payout" issue-id) - :on-success #(println %) - :params {:payout-hash payout-hash}}})) + :save-payout-hash + (fn [{:keys [db]} [_ issue-id payout-hash]] + {:db db + :http {:method POST + :url (str/format "/api/user/bounty/%s/payout" issue-id) + :on-success #(println %) + :params {:payout-hash payout-hash}}})) (reg-event-fx - :set-bounties - (fn [{:keys [db]} [_ bounties]] - {:db (assoc db :all-bounties bounties) - :dispatch [:init-pagination bounties]})) + :set-bounties + (fn [{:keys [db]} [_ bounties]] + {:db (assoc db :all-bounties bounties) + :dispatch [:init-pagination bounties]})) (reg-event-fx - :load-owner-bounties - (fn [{:keys [db]} [_]] - {:db db - :http {:method GET - :url "/api/user/bounties" - :on-success #(dispatch [:set-owner-bounties %])}})) + :load-owner-bounties + (fn [{:keys [db]} [_]] + {:db db + :http {:method GET + :url "/api/user/bounties" + :on-success #(dispatch [:set-owner-bounties %])}})) (reg-event-db - :set-owner-bounties - (fn [db [_ issues]] - (assoc db :owner-bounties issues))) + :set-owner-bounties + (fn [db [_ issues]] + (assoc db :owner-bounties issues))) (reg-event-fx - :load-user-profile - (fn [{:keys [db]} [_]] - {:db db - :http {:method GET - :url "/api/user" - :on-success #(dispatch [:set-user-profile %])}})) + :load-user-profile + (fn [{:keys [db]} [_]] + {:db db + :http {:method GET + :url "/api/user" + :on-success #(dispatch [:set-user-profile %])}})) (reg-event-db - :set-user-profile - (fn [db [_ user-profile]] - (assoc db :user (:user user-profile)))) + :set-user-profile + (fn [db [_ user-profile]] + (assoc db :user (:user user-profile)))) (reg-event-db - :set-user-repos - (fn [db [_ repos]] - (-> db - (assoc :repos repos) - (assoc :repos-loading? false)))) + :set-user-repos + (fn [db [_ repos]] + (-> db + (assoc :repos repos) + (assoc :repos-loading? false)))) (reg-event-fx - :load-user-repos - (fn [{:keys [db]} [_]] - {:db (assoc db :repos-loading? true) - :http {:method GET - :url "/api/user/repositories" - :on-success #(dispatch [:set-user-repos (:repositories %)])}})) + :load-user-repos + (fn [{:keys [db]} [_]] + {:db (assoc db :repos-loading? true) + :http {:method GET + :url "/api/user/repositories" + :on-success #(dispatch [:set-user-repos (:repositories %)])}})) + + +(defn update-repo-state [all-repos full-name data] + (let [[owner repo-name] (js->clj (.split full-name "/"))] + (println "update-repo-busy-state" owner repo-name) + (update all-repos + owner + (fn [repos] (map (fn [repo] (if (= (:name repo) repo-name) + (assoc repo + :busy? (:busy? data) + :enabled (:enabled data)) + repo)) + repos))))) + +(reg-event-fx + :toggle-repo + (fn [{:keys [db]} [_ repo]] + (println repo) + {:db (assoc db :repos (update-repo-state (:repos db) (:full_name repo) {:busy? true + :enabled (:enabled repo)})) + :http {:method POST + :url "/api/user/repository/toggle" + :on-success #(dispatch [:repo-toggle-success %]) +;; TODO :on-error #(dispatch [:repo-toggle-error %]) + :finally #(println "finally" %) + :params (select-keys repo [:id :login :full_name :name])}})) + + + (reg-event-db - :set-enabled-repos - (fn [db [_ repos]] - (assoc db :enabled-repos (zipmap repos (repeat true))))) + :repo-toggle-success + (fn [db [_ repo]] + (println "repo-toggle-success" repo) + (assoc db :repos (update-repo-state (:repos db) + (:full_name repo) + {:busy? false + :enabled (:enabled repo)} )))) + (reg-event-fx - :load-enabled-repos - (fn [{:keys [db]} [_]] - {:db db - :http {:method GET - :url "/api/user/enabled-repositories" - :on-success #(dispatch [:set-enabled-repos %])}})) - -(reg-event-fx - :toggle-repo - (fn [{:keys [db]} [_ repo]] - (println "toggle-repo" repo) - {:db db - :http {:method POST - :url "/api/user/repository/toggle" - :on-success #(println %) - :params (select-keys repo [:id :login :full_name :name])}})) - -(reg-event-fx - :save-user-address - (fn [{:keys [db]} [_ user-id address]] - {:db db - :http {:method POST - :url "/api/user/address" - :on-success #(println %) - :params {:user-id user-id :address address}}})) + :save-user-address + (fn [{:keys [db]} [_ user-id address]] + {:db db + :http {:method POST + :url "/api/user/address" + :on-success #(println %) + :params {:user-id user-id :address address}}})) diff --git a/src/cljs/commiteth/manage.cljs b/src/cljs/commiteth/manage.cljs index f69f41e..f138e5e 100644 --- a/src/cljs/commiteth/manage.cljs +++ b/src/cljs/commiteth/manage.cljs @@ -4,6 +4,10 @@ [commiteth.issues :refer [issues-list-table issue-url]] [clojure.set :refer [rename-keys]])) + + + + (defn repository-row [repo] (let [{repo-id :id url :html_url diff --git a/src/cljs/commiteth/repos.cljs b/src/cljs/commiteth/repos.cljs new file mode 100644 index 0000000..3be08be --- /dev/null +++ b/src/cljs/commiteth/repos.cljs @@ -0,0 +1,60 @@ +(ns commiteth.repos + (:require [re-frame.core :as rf])) + + +(defn repo-toggle-button [enabled busy on-click] + (let [add-busy-styles (fn [x] (conj x (when busy {:class (str "busy loading")}))) + button (if enabled + [:div.ui.button.small.repo-added-button (add-busy-styles {}) + [:i.icon.check] + "Added"] + [:div.ui.button.small + (add-busy-styles {:on-click on-click}) + "Add"])] + + [:div.ui.two.column.container + button + (when enabled + [:a.ui.item.remove-link {:on-click on-click} "Remove"])])) + + +(defn repo-card [repo] + [:div.ui.card + [:div.content + [:div.repo-label (:full_name repo)] + [:div.repo-description (:description repo)]] + [:div.repo-button-container + [repo-toggle-button + (:enabled repo) + (:busy? repo) + #(rf/dispatch [:toggle-repo repo])]]]) + +(defn repo-group-title [group login] + [:h3 + (if (= group login) + "Personal repositories" + group)]) + + +(defn repos-list [] + (let [repos (rf/subscribe [:repos]) + user (rf/subscribe [:user]) + repo-groups (keys @repos)] + (fn [] + (into [:div.ui.container] + (for [[group group-repos] + (map (fn [group] [group (get @repos group)]) repo-groups)] + [:div [repo-group-title group (:login @user)] + (into [:div.ui.cards] + (map repo-card group-repos))]))))) + + +(defn repos-page [] + (let [repos-loading? (rf/subscribe [:repos-loading?])] + (fn [] + (if @repos-loading? + [:div.ui.container + [:p] + [:div.ui.active.dimmer + [:div.ui.loader]]] + [repos-list])))) diff --git a/src/less/style.less b/src/less/style.less index 703dfd6..5968a9b 100644 --- a/src/less/style.less +++ b/src/less/style.less @@ -5,11 +5,22 @@ font-weight: normal; } +@font-face { + font-family: 'postgrotesk-medium'; + src: url('/fonts/PostGrotesk-Medium.woff') format('woff'); + font-weight: normal; +} + .ui.button { background-color: #b0f1ee; white-space: nowrap; + padding: 1em 1.2em 1em; } +.ui.small.button { + font-size: 15px!important; +} + .commiteth-header { background-color: #2f3f44!important; @@ -27,7 +38,6 @@ } .ui.header { - //font-weight: normal; color: #fff; } @@ -62,13 +72,17 @@ span.dropdown.icon { font-family: 'postgrotesk'; } -.ui.tabular { +.ui.attached.tabular { background-color: #2f3f44; + border-bottom: none; .ui.item { - padding: 1.5em; + padding: 1em; color: #fff; opacity: 0.98; cursor: pointer; + font-family: 'postgrotesk'; + font-size: 16px; + font-weight: normal; } .ui.item.active { border-radius: .8rem .8rem 0 0!important; @@ -79,24 +93,52 @@ span.dropdown.icon { } } - .repo-label { color: #1bb5c1; - font-weight: bold; + font-family: 'postgrotesk-medium'; + font-size: 16px; } .repo-description { color: #474951; + font-size: 15px; + font-weight: normal; + line-height: 24px; + margin-top: 16px; + margin-bottom: 16px; } +.repo-button-container { + display: inline-block; + vertical-align: bottom; + padding: 0em 1em 0em; + font-family: 'postgrotesk-medium'; + font-size: 15px; + a { + padding: 1em; + font-size: 15px; + } +} + +.page-content { + margin-top: 2em; +} + + .ui.button.repo-added-button { color: white; background-color: #61deb0; cursor: default; } + +.ui.cards>.card { + border: #e7e7e7 solid 0.1em; + box-shadow: none; + border-radius: 0.3em; + padding: 0.8em 1em 1.1em; + +} .ui.segment { - // margin: 2.5em; - // line-height: 3em; border: #e7e7e7 solid 0.1em; box-shadow: none; border-radius: 0.3em;