diff --git a/package-lock.json b/package-lock.json index cb30c78..1d12fb1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1473,9 +1473,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==" }, "node_modules/lodash.curry": { "version": "4.1.1", @@ -3988,9 +3988,9 @@ } }, "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==" }, "lodash.curry": { "version": "4.1.1", @@ -4722,7 +4722,7 @@ "resolved": "https://registry.npmjs.org/victory-axis/-/victory-axis-36.6.8.tgz", "integrity": "sha512-tClVJEay1YOJAh9rRyyLx8pei7Sr1/xTz04bJmfzFoAxFoPBtvgfFwXhfZ1YjGIl7m5Wh2CiYMY3figueLzYtg==", "requires": { - "lodash": "^4.17.19", + "lodash": "4.17.23", "prop-types": "^15.8.1", "victory-core": "^36.6.8" } @@ -4732,7 +4732,7 @@ "resolved": "https://registry.npmjs.org/victory-bar/-/victory-bar-36.6.8.tgz", "integrity": "sha512-jLLPm3IW8/2uSLPvQD9bxzXnTraUYBIDTkbZPZy7oHP01OVzP1sj+MMHcINCWcUbyUyLZDL3u8CvViXjS273JQ==", "requires": { - "lodash": "^4.17.19", + "lodash": "4.17.23", "prop-types": "^15.8.1", "victory-core": "^36.6.8", "victory-vendor": "^36.6.8" @@ -4743,7 +4743,7 @@ "resolved": "https://registry.npmjs.org/victory-chart/-/victory-chart-36.6.8.tgz", "integrity": "sha512-kC1jL63PAmqUrvZNOfwAXNuaIwz4nvXYUuEPu59WRBCOIGDGRgv2wJ1O7O0xYXqDkI57EtAYf9KUK+miEn/Btg==", "requires": { - "lodash": "^4.17.19", + "lodash": "4.17.23", "prop-types": "^15.8.1", "react-fast-compare": "^3.2.0", "victory-axis": "^36.6.8", @@ -4757,7 +4757,7 @@ "resolved": "https://registry.npmjs.org/victory-core/-/victory-core-36.6.8.tgz", "integrity": "sha512-SkyEszZKGyxjqfptfFWYdI22CvCuE9LhkaDpikzIhT2gcE3SuOBO5fk/740XMYE2ZUsJ4Fu/Vy4+8jZi17y44A==", "requires": { - "lodash": "^4.17.21", + "lodash": "4.17.23", "prop-types": "^15.8.1", "react-fast-compare": "^3.2.0", "victory-vendor": "^36.6.8" @@ -4768,7 +4768,7 @@ "resolved": "https://registry.npmjs.org/victory-polar-axis/-/victory-polar-axis-36.6.8.tgz", "integrity": "sha512-aU+Wp5six21POhI9oXeREnZHljpqcmwFHHnliVGrwgRsuc7TAjfXPWVOX9guEFfh6zQW6IZWWWTTLAN/PIEm9w==", "requires": { - "lodash": "^4.17.19", + "lodash": "4.17.23", "prop-types": "^15.8.1", "victory-core": "^36.6.8" } @@ -4778,7 +4778,7 @@ "resolved": "https://registry.npmjs.org/victory-scatter/-/victory-scatter-36.6.8.tgz", "integrity": "sha512-GKSNneBxIWLsF3eBSTW5IwT5S4YdsfFl4PVCP3/wTa2myfS5DIS9FufEnJp/FEZGalEXYWxeR47rlWqABxAj5A==", "requires": { - "lodash": "^4.17.19", + "lodash": "4.17.23", "prop-types": "^15.8.1", "victory-core": "^36.6.8" } @@ -4789,7 +4789,7 @@ "integrity": "sha512-hWPOVqMD3Sv6Rl1iyO6ibQrwYF9/eLCnRo0T59/Hsid6On0AJJjL9gv0oEIM5fqz7R7zx9PJmMk877IctEOemw==", "requires": { "json-stringify-safe": "^5.0.1", - "lodash": "^4.17.19", + "lodash": "4.17.23", "prop-types": "^15.8.1", "react-fast-compare": "^3.2.0", "victory-core": "^36.6.8" @@ -4800,7 +4800,7 @@ "resolved": "https://registry.npmjs.org/victory-tooltip/-/victory-tooltip-36.6.8.tgz", "integrity": "sha512-9P+QeAGyDpP0trJnQ1NtnbDhpoJB0Ghc2boYEehvL12p0OzolY9/Nq5SDP0tu5i1BBujwFXtnoCDqt+mOH25fA==", "requires": { - "lodash": "^4.17.19", + "lodash": "4.17.23", "prop-types": "^15.8.1", "victory-core": "^36.6.8" } diff --git a/package.json b/package.json index 4399f28..39db033 100644 --- a/package.json +++ b/package.json @@ -22,5 +22,8 @@ "victory-core": "^36.6.8", "victory-scatter": "^36.6.8", "victory-tooltip": "^36.6.8" + }, + "overrides": { + "lodash": "4.17.23" } } diff --git a/src/com/yetanalytics/lrs_admin_ui/db.cljs b/src/com/yetanalytics/lrs_admin_ui/db.cljs index db93978..0419687 100644 --- a/src/com/yetanalytics/lrs_admin_ui/db.cljs +++ b/src/com/yetanalytics/lrs_admin_ui/db.cljs @@ -177,6 +177,28 @@ (s/def ::csv-download-properties (s/keys :req-un [::csvd/property-paths])) +(s/def ::statements-file-upload-xapi-version + (s/nilable string?)) + +(s/def ::statements-file-upload-statements-count + (s/nilable integer?)) + +(s/def ::statements-file-upload-file-upload-file + (s/nilable #(instance? js/File %))) + +(s/def :event-log/code {:good :bad}) +(s/def :event-log/event string?) +(s/def :event-log/duration int?) +(s/def :event-log/timestamp int?) + +(s/def ::statements-file-upload-event-log + (s/nilable + (s/coll-of + (s/keys :req-un [:event-log/code + :event-log/event + :event-log/timestamp] + :opt-un [:event-log/duration])))) + (s/def ::dialog-ref any?) (s/def :dialog-choice/label string?) @@ -243,6 +265,10 @@ ::editing-reaction-template-errors ::editing-reaction-template-json ::csv-download-properties + ::statements-file-upload-xapi-version + ::statements-file-upload-file-upload-file + ::statements-file-upload-statements-count + ::statements-file-upload-event-log ::dialog-ref ::dialog-data ::no-val? diff --git a/src/com/yetanalytics/lrs_admin_ui/functions/time.cljs b/src/com/yetanalytics/lrs_admin_ui/functions/time.cljs index ef5cee4..b5ef8b5 100644 --- a/src/com/yetanalytics/lrs_admin_ui/functions/time.cljs +++ b/src/com/yetanalytics/lrs_admin_ui/functions/time.cljs @@ -48,3 +48,7 @@ (defn timeline-until-default [] (.toISOString (js/Date.))) + +(defn ms->local [ms] + (let [date (js/Date. ms)] + (format "%s, %s" (.toLocaleDateString date) (.toLocaleTimeString date)))) diff --git a/src/com/yetanalytics/lrs_admin_ui/handlers.cljs b/src/com/yetanalytics/lrs_admin_ui/handlers.cljs index bde53c8..7076ef0 100644 --- a/src/com/yetanalytics/lrs_admin_ui/handlers.cljs +++ b/src/com/yetanalytics/lrs_admin_ui/handlers.cljs @@ -92,7 +92,8 @@ ::db/reactions [] ::db/last-interaction-time (.now js/Date) ::db/supported-versions db/supported-versions-set - ::db/reaction-version "2.0.0"} + ::db/reaction-version "2.0.0" + ::db/statements-file-upload-event-log []} :fx [[:dispatch [:db/verify-login]] [:dispatch [:db/get-env]]]})) @@ -677,6 +678,121 @@ (fn [[edn-data edn-data-name]] (download/download-edn edn-data edn-data-name))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Uploads +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(re-frame/reg-event-fx + :statements-file-upload/file-change + (fn [{:keys [db]} [_ file text]] + (let [parsed (try (.parse js/JSON text) + (catch :default _e :unparseable))] + (case parsed + :unparseable + {:fx [[:dispatch [:notification/notify true "File not valid JSON"]]] + :db (-> db + (update + ::db/statements-file-upload-event-log conj + {:code :bad + :event (str "File " (.-name file) " not valid JSON, not loaded") + :timestamp (.now js/Date)}) + (assoc ::db/statements-file-upload-file + nil + ::db/statements-file-upload-file-text + nil + ::db/statements-file-upload-file-name + nil + ::db/statements-file-upload-statements-count + 0))} + {:db (assoc db + ::db/statements-file-upload-file + file + ::db/statements-file-upload-file-text + text + ::db/statements-file-upload-file-name + (.-name file) + ::db/statements-file-upload-statements-count + (if (.isArray js/Array parsed) + (.-length parsed) + 1))})))) + +(re-frame/reg-event-fx + :statements-file-upload/upload-click + (fn [{{[credential] ::db/credentials + :as db} :db :as _cofx} [_event-name]] + (if credential + {:fx [[:dispatch [:statements-file-upload/upload (db ::db/statements-file-upload-file-text)] ]]} + {:fx [[:dispatch [:notification/notify true + "Please select a credential"]]]}))) + +(re-frame/reg-event-fx + :statements-file-upload/upload + (fn [{{{credential :credential} ::db/browser + server-host ::db/server-host + proxy-path ::db/proxy-path + xapi-version ::db/statements-file-upload-xapi-version + :as _db} :db} [_ file-text]] + (let [xapi-version (or xapi-version "1.0.3") + start-ts (.now js/Date)] + {:http-xhrio + (httpfn/req-xapi + {:method :post + :headers {"Authorization" (format "Basic %s" + (httpfn/make-basic-auth credential)) + "Content-Type" "application/json"} + :uri (httpfn/serv-uri + server-host + "/xapi/statements" + proxy-path) + :response-format (ajax/json-response-format {:keywords? true}) + :body file-text + :interceptors [(httpfn/xapi-version-interceptor xapi-version) ] + :on-success [:statements-file-upload/success-handler xapi-version start-ts] + :on-failure [:statements-file-upload/error-handler]})}))) + +(re-frame/reg-event-fx + :statements-file-upload/success-handler + (fn [{{file ::db/statements-file-upload-file + c ::db/statements-file-upload-statements-count + :as db} :db} + [_ xapi-version start-ts _result]] + + (let [filename (.-name file) + duration (- (.now js/Date) + start-ts)] + {:fx [[:dispatch [:notification/notify true "Upload Successful!"]]] + :db (update db ::db/statements-file-upload-event-log conj + {:code :good + :event (str "Successfully uploaded " c " statements from " filename " under XAPI version " xapi-version) + :duration duration + :timestamp (.now js/Date)})}))) + +(re-frame/reg-event-fx + :statements-file-upload/error-handler + (fn [{{file ::db/statements-file-upload-file + :as db} :db} + [_ result]] + (let [filename (.-name file) + precursor (str "Failed to upload " filename ": ") + msg (cond (#{0 -1} (:status result)) + "Couldn't reach server" + :else + (get-in result [:response :error :message]))] + + {:fx [[:dispatch [:notification/notify true msg]]] + :db (update db ::db/statements-file-upload-event-log conj + {:code :bad + :event (str precursor msg) + :timestamp (.now js/Date)})}))) + +(re-frame/reg-event-db + :statements-file-upload/set-xapi-version + (fn [db [_ version]] + (assoc db ::db/statements-file-upload-xapi-version + version))) + + + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Data Browser ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/src/com/yetanalytics/lrs_admin_ui/language.cljs b/src/com/yetanalytics/lrs_admin_ui/language.cljs index c87c8ae..9776172 100644 --- a/src/com/yetanalytics/lrs_admin_ui/language.cljs +++ b/src/com/yetanalytics/lrs_admin_ui/language.cljs @@ -75,6 +75,12 @@ :csv.property-paths.add {:en-US "Add CSV Column "} :csv.property-paths.instructions {:en-US "To export your data, select the xAPI statement property paths as CSV columns below."} :csv.filters {:en-US "Filters:"} + ;;JSON File Upload + :statements.file-upload.title {:en-US "Upload Statements"} + :statements.file-upload.button {:en-US "Upload"} + :statements.file-upload.choose-file-button {:en-US "Choose file"} + :statements.file-upload.xapi-version {:en-US "XAPI Version"} + :statements.file-upload.key-note {:en-US "Please Choose an API Key Above to Upload Statements File"} ;;Monitor :monitor.title {:en-US "LRS Monitor"} :monitor.no-data {:en-US "No Statement Data"} diff --git a/src/com/yetanalytics/lrs_admin_ui/subs.cljs b/src/com/yetanalytics/lrs_admin_ui/subs.cljs index 3855dc3..24a848d 100644 --- a/src/com/yetanalytics/lrs_admin_ui/subs.cljs +++ b/src/com/yetanalytics/lrs_admin_ui/subs.cljs @@ -225,6 +225,35 @@ (fn [property-paths _] (s/valid? :validation/property-paths property-paths))) +;; Upload JSON file +(reg-sub + :statements-file-upload/xapi-version + (fn [db _] + (or (get db ::db/statements-file-upload-xapi-version) + "1.0.3"))) + +(reg-sub + :statements-file-upload/file + (fn [db _] + (get db ::db/statements-file-upload-file))) + +(reg-sub + :statements-file-upload/filename + (fn [_qv] + [(subscribe [:statements-file-upload/file])]) + (fn [[file] _qv] + (.-name file))) + +(reg-sub + :statements-file-upload/statement-count + (fn [db _] + (get db ::db/statements-file-upload-statements-count))) + +(reg-sub + :statements-file-upload/event-log + (fn [db _] + (::db/statements-file-upload-event-log db))) + ;; OIDC State (reg-sub :oidc/login-available? diff --git a/src/com/yetanalytics/lrs_admin_ui/views/browser.cljs b/src/com/yetanalytics/lrs_admin_ui/views/browser.cljs index 170de5d..df39f08 100644 --- a/src/com/yetanalytics/lrs_admin_ui/views/browser.cljs +++ b/src/com/yetanalytics/lrs_admin_ui/views/browser.cljs @@ -237,10 +237,71 @@ :on-click #(dispatch [:csv/auth-and-download address]) :value @(subscribe [:lang/get :datamgmt.download.button])}])])) +(defn file-summary [] + [:div {:class "browser"} + [:span "File: "] + [:span @(subscribe [:statements-file-upload/filename]) ": "] + [:span @(subscribe [:statements-file-upload/statement-count]) " statements"]]) + +(defn- json-upload [] + [:div + [:h4 {:class "content-title"} + @(subscribe [:lang/get :statements.file-upload.title])] + (if-not (:credential @(subscribe [:db/get-browser])) + [:div {:class "browser"} + @(subscribe [:lang/get :statements.file-upload.key-note])] + [:div + [:div + (when @(subscribe [:statements-file-upload/file]) + [file-summary]) + [:br] + [:label.btn-brand-bold {:for "file"} + (if-not @(subscribe [:statements-file-upload/file]) + @(subscribe [:lang/get :statements.file-upload.choose-file-button]) + "Change file")] + [:input#file {:style {:opacity 0 :position :absolute} + :type :file + :name "file" + :on-change #(let [file (aget (.-files (.-target %)) 0)] + (.then (.text file) + (fn [text] + (dispatch [:statements-file-upload/file-change file text]))))}]] + + (when @(subscribe [:statements-file-upload/file]) + [:div + [:br] + [:button {:type "button" + :class "btn-brand-bold" + :on-click (fn [_e] + (dispatch [:statements-file-upload/upload-click]))} + @(subscribe [:lang/get :statements.file-upload.button])] + [:span " " @(subscribe [:lang/get :statements.file-upload.xapi-version]) ": " + [:select + {::on-change #(dispatch [:statements-file-upload/set-xapi-version (fns/ps-event-val %)])} + [:option "1.0.3"] + [:option "2.0.0"]]]]) + (let [events @(subscribe [:statements-file-upload/event-log])] + (when (seq events) + (let [cols [{:name "Event" + :selector #(str + ({"good" "✅" "bad" "❌"} (get % "code")) + " " + (get % "event"))} + {:name "Duration" + :selector #(str (get % "duration"))} + {:name "Timestamp" + :selector #(time/ms->local (get % "timestamp"))}] + data events + other-opts {:columns cols + :data data}] + [data-table other-opts])))])]) + (defn browser [] [:div {:class "left-content-wrapper"} [:h2 {:class "content-title"} @(subscribe [:lang/get :browser.title])] [browser-main] [:div {:class "h-divider"}] - [csv-download]]) + [csv-download] + [:div {:class "h-divider"}] + [json-upload]])