Merge remote-tracking branch 'origin/staging' into develop
|
@ -9,31 +9,24 @@
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
[app.common.exceptions :as ex]
|
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.common.transit :as t]
|
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.http :as-alias http]
|
[app.http :as-alias http]
|
||||||
[app.http.access-token :as-alias actoken]
|
[app.http.access-token :as-alias actoken]
|
||||||
[app.http.client :as http.client]
|
|
||||||
[app.loggers.audit.tasks :as-alias tasks]
|
[app.loggers.audit.tasks :as-alias tasks]
|
||||||
[app.loggers.webhooks :as-alias webhooks]
|
[app.loggers.webhooks :as-alias webhooks]
|
||||||
[app.main :as-alias main]
|
|
||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as-alias rpc]
|
||||||
[app.rpc.retry :as rtry]
|
[app.rpc.retry :as rtry]
|
||||||
[app.setup :as-alias setup]
|
[app.setup :as-alias setup]
|
||||||
[app.tokens :as tokens]
|
|
||||||
[app.util.services :as-alias sv]
|
[app.util.services :as-alias sv]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[app.worker :as wrk]
|
[app.worker :as wrk]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[integrant.core :as ig]
|
[integrant.core :as ig]
|
||||||
[lambdaisland.uri :as u]
|
|
||||||
[promesa.exec :as px]
|
|
||||||
[ring.request :as rreq]))
|
[ring.request :as rreq]))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
@ -195,14 +188,14 @@
|
||||||
:profile-id (::profile-id event)
|
:profile-id (::profile-id event)
|
||||||
:ip-addr (::ip-addr event)
|
:ip-addr (::ip-addr event)
|
||||||
:context (::context event)
|
:context (::context event)
|
||||||
:props (::props event)}]
|
:props (::props event)}
|
||||||
|
tnow (dt/now)]
|
||||||
|
|
||||||
(when (contains? cf/flags :audit-log)
|
(when (contains? cf/flags :audit-log)
|
||||||
;; NOTE: this operation may cause primary key conflicts on inserts
|
;; NOTE: this operation may cause primary key conflicts on inserts
|
||||||
;; because of the timestamp precission (two concurrent requests), in
|
;; because of the timestamp precission (two concurrent requests), in
|
||||||
;; this case we just retry the operation.
|
;; this case we just retry the operation.
|
||||||
(let [tnow (dt/now)
|
(let [params (-> params
|
||||||
params (-> params
|
|
||||||
(assoc :created-at tnow)
|
(assoc :created-at tnow)
|
||||||
(assoc :tracked-at tnow)
|
(assoc :tracked-at tnow)
|
||||||
(update :props db/tjson)
|
(update :props db/tjson)
|
||||||
|
@ -211,6 +204,23 @@
|
||||||
(assoc :source "backend"))]
|
(assoc :source "backend"))]
|
||||||
(db/insert! cfg :audit-log params)))
|
(db/insert! cfg :audit-log params)))
|
||||||
|
|
||||||
|
(when (and (or (contains? cf/flags :telemetry)
|
||||||
|
(cf/get :telemetry-enabled))
|
||||||
|
(not (contains? cf/flags :audit-log)))
|
||||||
|
;; NOTE: this operation may cause primary key conflicts on inserts
|
||||||
|
;; because of the timestamp precission (two concurrent requests), in
|
||||||
|
;; this case we just retry the operation.
|
||||||
|
;;
|
||||||
|
;; NOTE: this is only executed when general audit log is disabled
|
||||||
|
(let [params (-> params
|
||||||
|
(assoc :created-at tnow)
|
||||||
|
(assoc :tracked-at tnow)
|
||||||
|
(assoc :props (db/tjson {}))
|
||||||
|
(assoc :context (db/tjson {}))
|
||||||
|
(assoc :ip-addr (db/inet "0.0.0.0"))
|
||||||
|
(assoc :source "backend"))]
|
||||||
|
(db/insert! cfg :audit-log params)))
|
||||||
|
|
||||||
(when (and (contains? cf/flags :webhooks)
|
(when (and (contains? cf/flags :webhooks)
|
||||||
(::webhooks/event? event))
|
(::webhooks/event? event))
|
||||||
(let [batch-key (::webhooks/batch-key event)
|
(let [batch-key (::webhooks/batch-key event)
|
||||||
|
@ -249,137 +259,3 @@
|
||||||
(rtry/invoke! cfg db/tx-run! handle-event! event))
|
(rtry/invoke! cfg db/tx-run! handle-event! event))
|
||||||
(catch Throwable cause
|
(catch Throwable cause
|
||||||
(l/error :hint "unexpected error processing event" :cause cause))))
|
(l/error :hint "unexpected error processing event" :cause cause))))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
;; TASK: ARCHIVE
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
|
|
||||||
;; This is a task responsible to send the accumulated events to
|
|
||||||
;; external service for archival.
|
|
||||||
|
|
||||||
(declare archive-events)
|
|
||||||
|
|
||||||
(s/def ::tasks/uri ::us/string)
|
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::tasks/archive-task [_]
|
|
||||||
(s/keys :req [::db/pool ::setup/props ::http.client/client]))
|
|
||||||
|
|
||||||
(defmethod ig/init-key ::tasks/archive
|
|
||||||
[_ cfg]
|
|
||||||
(fn [params]
|
|
||||||
;; NOTE: this let allows overwrite default configured values from
|
|
||||||
;; the repl, when manually invoking the task.
|
|
||||||
(let [enabled (or (contains? cf/flags :audit-log-archive)
|
|
||||||
(:enabled params false))
|
|
||||||
uri (cf/get :audit-log-archive-uri)
|
|
||||||
uri (or uri (:uri params))
|
|
||||||
cfg (assoc cfg ::uri uri)]
|
|
||||||
|
|
||||||
(when (and enabled (not uri))
|
|
||||||
(ex/raise :type :internal
|
|
||||||
:code :task-not-configured
|
|
||||||
:hint "archive task not configured, missing uri"))
|
|
||||||
|
|
||||||
(when enabled
|
|
||||||
(loop [total 0]
|
|
||||||
(let [n (archive-events cfg)]
|
|
||||||
(if n
|
|
||||||
(do
|
|
||||||
(px/sleep 100)
|
|
||||||
(recur (+ total ^long n)))
|
|
||||||
(when (pos? total)
|
|
||||||
(l/dbg :hint "events archived" :total total)))))))))
|
|
||||||
|
|
||||||
(def ^:private sql:retrieve-batch-of-audit-log
|
|
||||||
"select *
|
|
||||||
from audit_log
|
|
||||||
where archived_at is null
|
|
||||||
order by created_at asc
|
|
||||||
limit 128
|
|
||||||
for update skip locked;")
|
|
||||||
|
|
||||||
(defn archive-events
|
|
||||||
[{:keys [::db/pool ::uri] :as cfg}]
|
|
||||||
(letfn [(decode-row [{:keys [props ip-addr context] :as row}]
|
|
||||||
(cond-> row
|
|
||||||
(db/pgobject? props)
|
|
||||||
(assoc :props (db/decode-transit-pgobject props))
|
|
||||||
|
|
||||||
(db/pgobject? context)
|
|
||||||
(assoc :context (db/decode-transit-pgobject context))
|
|
||||||
|
|
||||||
(db/pgobject? ip-addr "inet")
|
|
||||||
(assoc :ip-addr (db/decode-inet ip-addr))))
|
|
||||||
|
|
||||||
(row->event [row]
|
|
||||||
(select-keys row [:type
|
|
||||||
:name
|
|
||||||
:source
|
|
||||||
:created-at
|
|
||||||
:tracked-at
|
|
||||||
:profile-id
|
|
||||||
:ip-addr
|
|
||||||
:props
|
|
||||||
:context]))
|
|
||||||
|
|
||||||
(send [events]
|
|
||||||
(let [token (tokens/generate (::setup/props cfg)
|
|
||||||
{:iss "authentication"
|
|
||||||
:iat (dt/now)
|
|
||||||
:uid uuid/zero})
|
|
||||||
body (t/encode {:events events})
|
|
||||||
headers {"content-type" "application/transit+json"
|
|
||||||
"origin" (cf/get :public-uri)
|
|
||||||
"cookie" (u/map->query-string {:auth-token token})}
|
|
||||||
params {:uri uri
|
|
||||||
:timeout 12000
|
|
||||||
:method :post
|
|
||||||
:headers headers
|
|
||||||
:body body}
|
|
||||||
resp (http.client/req! cfg params)]
|
|
||||||
(if (= (:status resp) 204)
|
|
||||||
true
|
|
||||||
(do
|
|
||||||
(l/error :hint "unable to archive events"
|
|
||||||
:resp-status (:status resp)
|
|
||||||
:resp-body (:body resp))
|
|
||||||
false))))
|
|
||||||
|
|
||||||
(mark-as-archived [conn rows]
|
|
||||||
(db/exec-one! conn ["update audit_log set archived_at=now() where id = ANY(?)"
|
|
||||||
(->> (map :id rows)
|
|
||||||
(db/create-array conn "uuid"))]))]
|
|
||||||
|
|
||||||
(db/with-atomic [conn pool]
|
|
||||||
(let [rows (db/exec! conn [sql:retrieve-batch-of-audit-log])
|
|
||||||
xform (comp (map decode-row)
|
|
||||||
(map row->event))
|
|
||||||
events (into [] xform rows)]
|
|
||||||
(when-not (empty? events)
|
|
||||||
(l/trc :hint "archive events chunk" :uri uri :events (count events))
|
|
||||||
(when (send events)
|
|
||||||
(mark-as-archived conn rows)
|
|
||||||
(count events)))))))
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
;; GC Task
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
|
|
||||||
(def ^:private sql:clean-archived
|
|
||||||
"delete from audit_log
|
|
||||||
where archived_at is not null")
|
|
||||||
|
|
||||||
(defn- clean-archived
|
|
||||||
[{:keys [::db/pool]}]
|
|
||||||
(let [result (db/exec-one! pool [sql:clean-archived])
|
|
||||||
result (:next.jdbc/update-count result)]
|
|
||||||
(l/debug :hint "delete archived audit log entries" :deleted result)
|
|
||||||
result))
|
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::tasks/gc [_]
|
|
||||||
(s/keys :req [::db/pool]))
|
|
||||||
|
|
||||||
(defmethod ig/init-key ::tasks/gc
|
|
||||||
[_ cfg]
|
|
||||||
(fn [_]
|
|
||||||
(clean-archived cfg)))
|
|
||||||
|
|
140
backend/src/app/loggers/audit/archive_task.clj
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
|
(ns app.loggers.audit.archive-task
|
||||||
|
(:require
|
||||||
|
[app.common.exceptions :as ex]
|
||||||
|
[app.common.logging :as l]
|
||||||
|
[app.common.transit :as t]
|
||||||
|
[app.common.uuid :as uuid]
|
||||||
|
[app.config :as cf]
|
||||||
|
[app.db :as db]
|
||||||
|
[app.http.client :as http]
|
||||||
|
[app.setup :as-alias setup]
|
||||||
|
[app.tokens :as tokens]
|
||||||
|
[app.util.time :as dt]
|
||||||
|
[clojure.spec.alpha :as s]
|
||||||
|
[integrant.core :as ig]
|
||||||
|
[lambdaisland.uri :as u]
|
||||||
|
[promesa.exec :as px]))
|
||||||
|
|
||||||
|
;; This is a task responsible to send the accumulated events to
|
||||||
|
;; external service for archival.
|
||||||
|
|
||||||
|
(defn- decode-row
|
||||||
|
[{:keys [props ip-addr context] :as row}]
|
||||||
|
(cond-> row
|
||||||
|
(db/pgobject? props)
|
||||||
|
(assoc :props (db/decode-transit-pgobject props))
|
||||||
|
|
||||||
|
(db/pgobject? context)
|
||||||
|
(assoc :context (db/decode-transit-pgobject context))
|
||||||
|
|
||||||
|
(db/pgobject? ip-addr "inet")
|
||||||
|
(assoc :ip-addr (db/decode-inet ip-addr))))
|
||||||
|
|
||||||
|
(def ^:private event-keys
|
||||||
|
[:type
|
||||||
|
:name
|
||||||
|
:source
|
||||||
|
:created-at
|
||||||
|
:tracked-at
|
||||||
|
:profile-id
|
||||||
|
:ip-addr
|
||||||
|
:props
|
||||||
|
:context])
|
||||||
|
|
||||||
|
(defn- row->event
|
||||||
|
[row]
|
||||||
|
(select-keys row event-keys))
|
||||||
|
|
||||||
|
(defn- send!
|
||||||
|
[{:keys [::uri] :as cfg} events]
|
||||||
|
(let [token (tokens/generate (::setup/props cfg)
|
||||||
|
{:iss "authentication"
|
||||||
|
:iat (dt/now)
|
||||||
|
:uid uuid/zero})
|
||||||
|
body (t/encode {:events events})
|
||||||
|
headers {"content-type" "application/transit+json"
|
||||||
|
"origin" (cf/get :public-uri)
|
||||||
|
"cookie" (u/map->query-string {:auth-token token})}
|
||||||
|
params {:uri uri
|
||||||
|
:timeout 12000
|
||||||
|
:method :post
|
||||||
|
:headers headers
|
||||||
|
:body body}
|
||||||
|
resp (http/req! cfg params)]
|
||||||
|
(if (= (:status resp) 204)
|
||||||
|
true
|
||||||
|
(do
|
||||||
|
(l/error :hint "unable to archive events"
|
||||||
|
:resp-status (:status resp)
|
||||||
|
:resp-body (:body resp))
|
||||||
|
false))))
|
||||||
|
|
||||||
|
(defn- mark-archived!
|
||||||
|
[{:keys [::db/conn]} rows]
|
||||||
|
(let [ids (db/create-array conn "uuid" (map :id rows))]
|
||||||
|
(db/exec-one! conn ["update audit_log set archived_at=now() where id = ANY(?)" ids])))
|
||||||
|
|
||||||
|
(def ^:private xf:create-event
|
||||||
|
(comp (map decode-row)
|
||||||
|
(map row->event)))
|
||||||
|
|
||||||
|
(def ^:private sql:get-audit-log-chunk
|
||||||
|
"SELECT *
|
||||||
|
FROM audit_log
|
||||||
|
WHERE archived_at is null
|
||||||
|
ORDER BY created_at ASC
|
||||||
|
LIMIT 128
|
||||||
|
FOR UPDATE
|
||||||
|
SKIP LOCKED")
|
||||||
|
|
||||||
|
(defn- get-event-rows
|
||||||
|
[{:keys [::db/conn] :as cfg}]
|
||||||
|
(->> (db/exec! conn [sql:get-audit-log-chunk])
|
||||||
|
(not-empty)))
|
||||||
|
|
||||||
|
(defn- archive-events!
|
||||||
|
[{:keys [::uri] :as cfg}]
|
||||||
|
(db/tx-run! cfg (fn [cfg]
|
||||||
|
(when-let [rows (get-event-rows cfg)]
|
||||||
|
(let [events (into [] xf:create-event rows)]
|
||||||
|
(l/trc :hint "archive events chunk" :uri uri :events (count events))
|
||||||
|
(when (send! cfg events)
|
||||||
|
(mark-archived! cfg rows)
|
||||||
|
(count events)))))))
|
||||||
|
|
||||||
|
(defmethod ig/pre-init-spec ::handler [_]
|
||||||
|
(s/keys :req [::db/pool ::setup/props ::http/client]))
|
||||||
|
|
||||||
|
(defmethod ig/init-key ::handler
|
||||||
|
[_ cfg]
|
||||||
|
(fn [params]
|
||||||
|
;; NOTE: this let allows overwrite default configured values from
|
||||||
|
;; the repl, when manually invoking the task.
|
||||||
|
(let [enabled (or (contains? cf/flags :audit-log-archive)
|
||||||
|
(:enabled params false))
|
||||||
|
|
||||||
|
uri (cf/get :audit-log-archive-uri)
|
||||||
|
uri (or uri (:uri params))
|
||||||
|
cfg (assoc cfg ::uri uri)]
|
||||||
|
|
||||||
|
(when (and enabled (not uri))
|
||||||
|
(ex/raise :type :internal
|
||||||
|
:code :task-not-configured
|
||||||
|
:hint "archive task not configured, missing uri"))
|
||||||
|
|
||||||
|
(when enabled
|
||||||
|
(loop [total 0]
|
||||||
|
(if-let [n (archive-events! cfg)]
|
||||||
|
(do
|
||||||
|
(px/sleep 100)
|
||||||
|
(recur (+ total ^long n)))
|
||||||
|
|
||||||
|
(when (pos? total)
|
||||||
|
(l/dbg :hint "events archived" :total total))))))))
|
||||||
|
|
31
backend/src/app/loggers/audit/gc_task.clj
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
|
(ns app.loggers.audit.gc-task
|
||||||
|
(:require
|
||||||
|
[app.common.logging :as l]
|
||||||
|
[app.db :as db]
|
||||||
|
[clojure.spec.alpha :as s]
|
||||||
|
[integrant.core :as ig]))
|
||||||
|
|
||||||
|
(def ^:private sql:clean-archived
|
||||||
|
"DELETE FROM audit_log
|
||||||
|
WHERE archived_at IS NOT NULL")
|
||||||
|
|
||||||
|
(defn- clean-archived!
|
||||||
|
[{:keys [::db/pool]}]
|
||||||
|
(let [result (db/exec-one! pool [sql:clean-archived])
|
||||||
|
result (db/get-update-count result)]
|
||||||
|
(l/debug :hint "delete archived audit log entries" :deleted result)
|
||||||
|
result))
|
||||||
|
|
||||||
|
(defmethod ig/pre-init-spec ::handler [_]
|
||||||
|
(s/keys :req [::db/pool]))
|
||||||
|
|
||||||
|
(defmethod ig/init-key ::handler
|
||||||
|
[_ cfg]
|
||||||
|
(fn [_]
|
||||||
|
(clean-archived! cfg)))
|
|
@ -21,7 +21,6 @@
|
||||||
[app.http.session :as-alias session]
|
[app.http.session :as-alias session]
|
||||||
[app.http.session.tasks :as-alias session.tasks]
|
[app.http.session.tasks :as-alias session.tasks]
|
||||||
[app.http.websocket :as http.ws]
|
[app.http.websocket :as http.ws]
|
||||||
[app.loggers.audit.tasks :as-alias audit.tasks]
|
|
||||||
[app.loggers.webhooks :as-alias webhooks]
|
[app.loggers.webhooks :as-alias webhooks]
|
||||||
[app.metrics :as-alias mtx]
|
[app.metrics :as-alias mtx]
|
||||||
[app.metrics.definition :as-alias mdef]
|
[app.metrics.definition :as-alias mdef]
|
||||||
|
@ -346,8 +345,8 @@
|
||||||
:storage-gc-deleted (ig/ref ::sto.gc-deleted/handler)
|
:storage-gc-deleted (ig/ref ::sto.gc-deleted/handler)
|
||||||
:storage-gc-touched (ig/ref ::sto.gc-touched/handler)
|
:storage-gc-touched (ig/ref ::sto.gc-touched/handler)
|
||||||
:session-gc (ig/ref ::session.tasks/gc)
|
:session-gc (ig/ref ::session.tasks/gc)
|
||||||
:audit-log-archive (ig/ref ::audit.tasks/archive)
|
:audit-log-archive (ig/ref :app.loggers.audit.archive-task/handler)
|
||||||
:audit-log-gc (ig/ref ::audit.tasks/gc)
|
:audit-log-gc (ig/ref :app.loggers.audit.gc-task/handler)
|
||||||
|
|
||||||
:process-webhook-event
|
:process-webhook-event
|
||||||
(ig/ref ::webhooks/process-event-handler)
|
(ig/ref ::webhooks/process-event-handler)
|
||||||
|
@ -411,12 +410,12 @@
|
||||||
::svgo/optimizer
|
::svgo/optimizer
|
||||||
{}
|
{}
|
||||||
|
|
||||||
::audit.tasks/archive
|
:app.loggers.audit.archive-task/handler
|
||||||
{::setup/props (ig/ref ::setup/props)
|
{::setup/props (ig/ref ::setup/props)
|
||||||
::db/pool (ig/ref ::db/pool)
|
::db/pool (ig/ref ::db/pool)
|
||||||
::http.client/client (ig/ref ::http.client/client)}
|
::http.client/client (ig/ref ::http.client/client)}
|
||||||
|
|
||||||
::audit.tasks/gc
|
:app.loggers.audit.gc-task/handler
|
||||||
{::db/pool (ig/ref ::db/pool)}
|
{::db/pool (ig/ref ::db/pool)}
|
||||||
|
|
||||||
::webhooks/process-event-handler
|
::webhooks/process-event-handler
|
||||||
|
|
|
@ -376,7 +376,10 @@
|
||||||
:fn (mg/resource "app/migrations/sql/0118-mod-task-table.sql")}
|
:fn (mg/resource "app/migrations/sql/0118-mod-task-table.sql")}
|
||||||
|
|
||||||
{:name "0119-mod-file-table"
|
{:name "0119-mod-file-table"
|
||||||
:fn (mg/resource "app/migrations/sql/0119-mod-file-table.sql")}])
|
:fn (mg/resource "app/migrations/sql/0119-mod-file-table.sql")}
|
||||||
|
|
||||||
|
{:name "0120-mod-audit-log-table"
|
||||||
|
:fn (mg/resource "app/migrations/sql/0120-mod-audit-log-table.sql")}])
|
||||||
|
|
||||||
(defn apply-migrations!
|
(defn apply-migrations!
|
||||||
[pool name migrations]
|
[pool name migrations]
|
||||||
|
|
11
backend/src/app/migrations/sql/0120-mod-audit-log-table.sql
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
CREATE TABLE new_audit_log (LIKE audit_log INCLUDING ALL);
|
||||||
|
INSERT INTO new_audit_log SELECT * FROM audit_log;
|
||||||
|
ALTER TABLE audit_log RENAME TO old_audit_log;
|
||||||
|
ALTER TABLE new_audit_log RENAME TO audit_log;
|
||||||
|
DROP TABLE old_audit_log;
|
||||||
|
|
||||||
|
DROP INDEX new_audit_log_id_archived_at_idx;
|
||||||
|
ALTER TABLE audit_log DROP CONSTRAINT new_audit_log_pkey;
|
||||||
|
ALTER TABLE audit_log ADD PRIMARY KEY (id);
|
||||||
|
ALTER TABLE audit_log ALTER COLUMN created_at SET DEFAULT now();
|
||||||
|
ALTER TABLE audit_log ALTER COLUMN tracked_at SET DEFAULT now();
|
|
@ -19,7 +19,20 @@
|
||||||
[app.rpc.climit :as-alias climit]
|
[app.rpc.climit :as-alias climit]
|
||||||
[app.rpc.doc :as-alias doc]
|
[app.rpc.doc :as-alias doc]
|
||||||
[app.rpc.helpers :as rph]
|
[app.rpc.helpers :as rph]
|
||||||
[app.util.services :as sv]))
|
[app.util.services :as sv]
|
||||||
|
[app.util.time :as dt]))
|
||||||
|
|
||||||
|
(def ^:private event-columns
|
||||||
|
[:id
|
||||||
|
:name
|
||||||
|
:source
|
||||||
|
:type
|
||||||
|
:tracked-at
|
||||||
|
:created-at
|
||||||
|
:profile-id
|
||||||
|
:ip-addr
|
||||||
|
:props
|
||||||
|
:context])
|
||||||
|
|
||||||
(defn- event->row [event]
|
(defn- event->row [event]
|
||||||
[(uuid/next)
|
[(uuid/next)
|
||||||
|
@ -27,24 +40,38 @@
|
||||||
(:source event)
|
(:source event)
|
||||||
(:type event)
|
(:type event)
|
||||||
(:timestamp event)
|
(:timestamp event)
|
||||||
|
(:created-at event)
|
||||||
(:profile-id event)
|
(:profile-id event)
|
||||||
(db/inet (:ip-addr event))
|
(db/inet (:ip-addr event))
|
||||||
(db/tjson (:props event))
|
(db/tjson (:props event))
|
||||||
(db/tjson (d/without-nils (:context event)))])
|
(db/tjson (d/without-nils (:context event)))])
|
||||||
|
|
||||||
(def ^:private event-columns
|
(defn- adjust-timestamp
|
||||||
[:id :name :source :type :tracked-at
|
[{:keys [timestamp created-at] :as event}]
|
||||||
:profile-id :ip-addr :props :context])
|
(let [margin (inst-ms (dt/diff timestamp created-at))]
|
||||||
|
(if (or (neg? margin)
|
||||||
|
(> margin 3600000))
|
||||||
|
;; If event is in future or lags more than 1 hour, we reasign
|
||||||
|
;; timestamp to the server creation date
|
||||||
|
(-> event
|
||||||
|
(assoc :timestamp created-at)
|
||||||
|
(update :context assoc :original-timestamp timestamp))
|
||||||
|
event)))
|
||||||
|
|
||||||
(defn- handle-events
|
(defn- handle-events
|
||||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id events] :as params}]
|
[{:keys [::db/pool]} {:keys [::rpc/profile-id events] :as params}]
|
||||||
(let [request (-> params meta ::http/request)
|
(let [request (-> params meta ::http/request)
|
||||||
ip-addr (audit/parse-client-ip request)
|
ip-addr (audit/parse-client-ip request)
|
||||||
|
tnow (dt/now)
|
||||||
xform (comp
|
xform (comp
|
||||||
(map #(assoc % :profile-id profile-id))
|
(map (fn [event]
|
||||||
(map #(assoc % :ip-addr ip-addr))
|
(-> event
|
||||||
(map #(assoc % :source "frontend"))
|
(assoc :created-at tnow)
|
||||||
|
(assoc :profile-id profile-id)
|
||||||
|
(assoc :ip-addr ip-addr)
|
||||||
|
(assoc :source "frontend"))))
|
||||||
(filter :profile-id)
|
(filter :profile-id)
|
||||||
|
(map adjust-timestamp)
|
||||||
(map event->row))
|
(map event->row))
|
||||||
events (sequence xform events)]
|
events (sequence xform events)]
|
||||||
(when (seq events)
|
(when (seq events)
|
||||||
|
|
|
@ -868,7 +868,8 @@
|
||||||
|
|
||||||
(db/delete! conn :file-library-rel {:library-file-id id})
|
(db/delete! conn :file-library-rel {:library-file-id id})
|
||||||
(db/update! conn :file
|
(db/update! conn :file
|
||||||
{:is-shared false}
|
{:is-shared false
|
||||||
|
:modified-at (dt/now)}
|
||||||
{:id id})
|
{:id id})
|
||||||
file)
|
file)
|
||||||
|
|
||||||
|
@ -876,7 +877,8 @@
|
||||||
(true? (:is-shared params)))
|
(true? (:is-shared params)))
|
||||||
(let [file (assoc file :is-shared true)]
|
(let [file (assoc file :is-shared true)]
|
||||||
(db/update! conn :file
|
(db/update! conn :file
|
||||||
{:is-shared true}
|
{:is-shared true
|
||||||
|
:modified-at (dt/now)}
|
||||||
{:id id})
|
{:id id})
|
||||||
file)
|
file)
|
||||||
|
|
||||||
|
|
|
@ -22,13 +22,182 @@
|
||||||
[promesa.exec :as px]))
|
[promesa.exec :as px]))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; TASK ENTRY POINT
|
;; IMPL
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(declare get-stats)
|
(defn- send!
|
||||||
(declare send!)
|
[cfg data]
|
||||||
(declare get-subscriptions-newsletter-updates)
|
(let [request {:method :post
|
||||||
(declare get-subscriptions-newsletter-news)
|
:uri (cf/get :telemetry-uri)
|
||||||
|
:headers {"content-type" "application/json"}
|
||||||
|
:body (json/encode-str data)}
|
||||||
|
response (http/req! cfg request)]
|
||||||
|
(when (> (:status response) 206)
|
||||||
|
(ex/raise :type :internal
|
||||||
|
:code :invalid-response
|
||||||
|
:response-status (:status response)
|
||||||
|
:response-body (:body response)))))
|
||||||
|
|
||||||
|
(defn- get-subscriptions-newsletter-updates
|
||||||
|
[conn]
|
||||||
|
(let [sql "SELECT email FROM profile where props->>'~:newsletter-updates' = 'true'"]
|
||||||
|
(->> (db/exec! conn [sql])
|
||||||
|
(mapv :email))))
|
||||||
|
|
||||||
|
(defn- get-subscriptions-newsletter-news
|
||||||
|
[conn]
|
||||||
|
(let [sql "SELECT email FROM profile where props->>'~:newsletter-news' = 'true'"]
|
||||||
|
(->> (db/exec! conn [sql])
|
||||||
|
(mapv :email))))
|
||||||
|
|
||||||
|
(defn- get-num-teams
|
||||||
|
[conn]
|
||||||
|
(-> (db/exec-one! conn ["SELECT count(*) AS count FROM team"]) :count))
|
||||||
|
|
||||||
|
(defn- get-num-projects
|
||||||
|
[conn]
|
||||||
|
(-> (db/exec-one! conn ["SELECT count(*) AS count FROM project"]) :count))
|
||||||
|
|
||||||
|
(defn- get-num-files
|
||||||
|
[conn]
|
||||||
|
(-> (db/exec-one! conn ["SELECT count(*) AS count FROM file"]) :count))
|
||||||
|
|
||||||
|
(defn- get-num-file-changes
|
||||||
|
[conn]
|
||||||
|
(let [sql (str "SELECT count(*) AS count "
|
||||||
|
" FROM file_change "
|
||||||
|
" where date_trunc('day', created_at) = date_trunc('day', now())")]
|
||||||
|
(-> (db/exec-one! conn [sql]) :count)))
|
||||||
|
|
||||||
|
(defn- get-num-touched-files
|
||||||
|
[conn]
|
||||||
|
(let [sql (str "SELECT count(distinct file_id) AS count "
|
||||||
|
" FROM file_change "
|
||||||
|
" where date_trunc('day', created_at) = date_trunc('day', now())")]
|
||||||
|
(-> (db/exec-one! conn [sql]) :count)))
|
||||||
|
|
||||||
|
(defn- get-num-users
|
||||||
|
[conn]
|
||||||
|
(-> (db/exec-one! conn ["SELECT count(*) AS count FROM profile"]) :count))
|
||||||
|
|
||||||
|
(defn- get-num-fonts
|
||||||
|
[conn]
|
||||||
|
(-> (db/exec-one! conn ["SELECT count(*) AS count FROM team_font_variant"]) :count))
|
||||||
|
|
||||||
|
(defn- get-num-comments
|
||||||
|
[conn]
|
||||||
|
(-> (db/exec-one! conn ["SELECT count(*) AS count FROM comment"]) :count))
|
||||||
|
|
||||||
|
(def sql:team-averages
|
||||||
|
"with projects_by_team AS (
|
||||||
|
SELECT t.id, count(p.id) AS num_projects
|
||||||
|
FROM team AS t
|
||||||
|
LEFT JOIN project AS p ON (p.team_id = t.id)
|
||||||
|
GROUP BY 1
|
||||||
|
), files_by_project AS (
|
||||||
|
SELECT p.id, count(f.id) AS num_files
|
||||||
|
FROM project AS p
|
||||||
|
LEFT JOIN file AS f ON (f.project_id = p.id)
|
||||||
|
GROUP BY 1
|
||||||
|
), comment_threads_by_file AS (
|
||||||
|
SELECT f.id, count(ct.id) AS num_comment_threads
|
||||||
|
FROM file AS f
|
||||||
|
LEFT JOIN comment_thread AS ct ON (ct.file_id = f.id)
|
||||||
|
GROUP BY 1
|
||||||
|
), users_by_team AS (
|
||||||
|
SELECT t.id, count(tp.profile_id) AS num_users
|
||||||
|
FROM team AS t
|
||||||
|
LEFT JOIN team_profile_rel AS tp ON(tp.team_id = t.id)
|
||||||
|
GROUP BY 1
|
||||||
|
)
|
||||||
|
SELECT (SELECT avg(num_projects)::integer FROM projects_by_team) AS avg_projects_on_team,
|
||||||
|
(SELECT max(num_projects)::integer FROM projects_by_team) AS max_projects_on_team,
|
||||||
|
(SELECT avg(num_files)::integer FROM files_by_project) AS avg_files_on_project,
|
||||||
|
(SELECT max(num_files)::integer FROM files_by_project) AS max_files_on_project,
|
||||||
|
(SELECT avg(num_comment_threads)::integer FROM comment_threads_by_file) AS avg_comment_threads_on_file,
|
||||||
|
(SELECT max(num_comment_threads)::integer FROM comment_threads_by_file) AS max_comment_threads_on_file,
|
||||||
|
(SELECT avg(num_users)::integer FROM users_by_team) AS avg_users_on_team,
|
||||||
|
(SELECT max(num_users)::integer FROM users_by_team) AS max_users_on_team")
|
||||||
|
|
||||||
|
(defn- get-team-averages
|
||||||
|
[conn]
|
||||||
|
(->> [sql:team-averages]
|
||||||
|
(db/exec-one! conn)))
|
||||||
|
|
||||||
|
(defn- get-enabled-auth-providers
|
||||||
|
[conn]
|
||||||
|
(let [sql (str "SELECT auth_backend AS backend, count(*) AS total "
|
||||||
|
" FROM profile GROUP BY 1")
|
||||||
|
rows (db/exec! conn [sql])]
|
||||||
|
(->> rows
|
||||||
|
(map (fn [{:keys [backend total]}]
|
||||||
|
(let [backend (or backend "penpot")]
|
||||||
|
[(keyword (str "auth-backend-" backend))
|
||||||
|
total])))
|
||||||
|
(into {}))))
|
||||||
|
|
||||||
|
(defn- get-jvm-stats
|
||||||
|
[]
|
||||||
|
(let [^Runtime runtime (Runtime/getRuntime)]
|
||||||
|
{:jvm-heap-current (.totalMemory runtime)
|
||||||
|
:jvm-heap-max (.maxMemory runtime)
|
||||||
|
:jvm-cpus (.availableProcessors runtime)
|
||||||
|
:os-arch (System/getProperty "os.arch")
|
||||||
|
:os-name (System/getProperty "os.name")
|
||||||
|
:os-version (System/getProperty "os.version")
|
||||||
|
:user-tz (System/getProperty "user.timezone")}))
|
||||||
|
|
||||||
|
(def ^:private sql:get-counters
|
||||||
|
"SELECT name, count(*) AS count
|
||||||
|
FROM audit_log
|
||||||
|
WHERE source = 'backend'
|
||||||
|
AND tracked_at >= date_trunc('day', now())
|
||||||
|
GROUP BY 1
|
||||||
|
ORDER BY 2 DESC")
|
||||||
|
|
||||||
|
(defn- get-action-counters
|
||||||
|
[conn]
|
||||||
|
(let [counters (->> (db/exec! conn [sql:get-counters])
|
||||||
|
(d/index-by (comp keyword :name) :count))
|
||||||
|
total (reduce + 0 (vals counters))]
|
||||||
|
{:total-accomulated-events total
|
||||||
|
:event-counters counters}))
|
||||||
|
|
||||||
|
(def ^:private sql:clean-counters
|
||||||
|
"DELETE FROM audit_log
|
||||||
|
WHERE ip_addr = '0.0.0.0'::inet -- we know this is from telemetry
|
||||||
|
AND tracked_at < (date_trunc('day', now()) - '1 day'::interval)")
|
||||||
|
|
||||||
|
(defn- clean-counters-data!
|
||||||
|
[conn]
|
||||||
|
(when-not (contains? cf/flags :audit-log)
|
||||||
|
(db/exec-one! conn [sql:clean-counters])))
|
||||||
|
|
||||||
|
(defn- get-stats
|
||||||
|
[conn]
|
||||||
|
(let [referer (if (cf/get :telemetry-with-taiga)
|
||||||
|
"taiga"
|
||||||
|
(cf/get :telemetry-referer))]
|
||||||
|
(-> {:referer referer
|
||||||
|
:public-uri (cf/get :public-uri)
|
||||||
|
:total-teams (get-num-teams conn)
|
||||||
|
:total-projects (get-num-projects conn)
|
||||||
|
:total-files (get-num-files conn)
|
||||||
|
:total-users (get-num-users conn)
|
||||||
|
:total-fonts (get-num-fonts conn)
|
||||||
|
:total-comments (get-num-comments conn)
|
||||||
|
:total-file-changes (get-num-file-changes conn)
|
||||||
|
:total-touched-files (get-num-touched-files conn)}
|
||||||
|
(merge
|
||||||
|
(get-team-averages conn)
|
||||||
|
(get-jvm-stats)
|
||||||
|
(get-enabled-auth-providers conn)
|
||||||
|
(get-action-counters conn))
|
||||||
|
(d/without-nils))))
|
||||||
|
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;; TASK ENTRY POINT
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::handler [_]
|
(defmethod ig/pre-init-spec ::handler [_]
|
||||||
(s/keys :req [::http/client
|
(s/keys :req [::http/client
|
||||||
|
@ -48,6 +217,10 @@
|
||||||
data {:subscriptions subs
|
data {:subscriptions subs
|
||||||
:version (:full cf/version)
|
:version (:full cf/version)
|
||||||
:instance-id (:instance-id props)}]
|
:instance-id (:instance-id props)}]
|
||||||
|
|
||||||
|
(when enabled?
|
||||||
|
(clean-counters-data! pool))
|
||||||
|
|
||||||
(cond
|
(cond
|
||||||
;; If we have telemetry enabled, then proceed the normal
|
;; If we have telemetry enabled, then proceed the normal
|
||||||
;; operation.
|
;; operation.
|
||||||
|
@ -63,7 +236,8 @@
|
||||||
;; onboarding dialog or the profile section, then proceed to
|
;; onboarding dialog or the profile section, then proceed to
|
||||||
;; send a limited telemetry data, that consists in the list of
|
;; send a limited telemetry data, that consists in the list of
|
||||||
;; subscribed emails and the running penpot version.
|
;; subscribed emails and the running penpot version.
|
||||||
(seq subs)
|
(or (seq (:newsletter-updates subs))
|
||||||
|
(seq (:newsletter-news subs)))
|
||||||
(do
|
(do
|
||||||
(when send?
|
(when send?
|
||||||
(px/sleep (rand-int 10000))
|
(px/sleep (rand-int 10000))
|
||||||
|
@ -72,151 +246,3 @@
|
||||||
|
|
||||||
:else
|
:else
|
||||||
data))))
|
data))))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
;; IMPL
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
|
|
||||||
(defn- send!
|
|
||||||
[cfg data]
|
|
||||||
(let [request {:method :post
|
|
||||||
:uri (cf/get :telemetry-uri)
|
|
||||||
:headers {"content-type" "application/json"}
|
|
||||||
:body (json/encode-str data)}
|
|
||||||
response (http/req! cfg request)]
|
|
||||||
(when (> (:status response) 206)
|
|
||||||
(ex/raise :type :internal
|
|
||||||
:code :invalid-response
|
|
||||||
:response-status (:status response)
|
|
||||||
:response-body (:body response)))))
|
|
||||||
|
|
||||||
(defn- get-subscriptions-newsletter-updates
|
|
||||||
[conn]
|
|
||||||
(let [sql "select email from profile where props->>'~:newsletter-updates' = 'true'"]
|
|
||||||
(->> (db/exec! conn [sql])
|
|
||||||
(mapv :email))))
|
|
||||||
|
|
||||||
(defn- get-subscriptions-newsletter-news
|
|
||||||
[conn]
|
|
||||||
(let [sql "select email from profile where props->>'~:newsletter-news' = 'true'"]
|
|
||||||
(->> (db/exec! conn [sql])
|
|
||||||
(mapv :email))))
|
|
||||||
|
|
||||||
(defn- retrieve-num-teams
|
|
||||||
[conn]
|
|
||||||
(-> (db/exec-one! conn ["select count(*) as count from team;"]) :count))
|
|
||||||
|
|
||||||
(defn- retrieve-num-projects
|
|
||||||
[conn]
|
|
||||||
(-> (db/exec-one! conn ["select count(*) as count from project;"]) :count))
|
|
||||||
|
|
||||||
(defn- retrieve-num-files
|
|
||||||
[conn]
|
|
||||||
(-> (db/exec-one! conn ["select count(*) as count from file;"]) :count))
|
|
||||||
|
|
||||||
(defn- retrieve-num-file-changes
|
|
||||||
[conn]
|
|
||||||
(let [sql (str "select count(*) as count "
|
|
||||||
" from file_change "
|
|
||||||
" where date_trunc('day', created_at) = date_trunc('day', now())")]
|
|
||||||
(-> (db/exec-one! conn [sql]) :count)))
|
|
||||||
|
|
||||||
(defn- retrieve-num-touched-files
|
|
||||||
[conn]
|
|
||||||
(let [sql (str "select count(distinct file_id) as count "
|
|
||||||
" from file_change "
|
|
||||||
" where date_trunc('day', created_at) = date_trunc('day', now())")]
|
|
||||||
(-> (db/exec-one! conn [sql]) :count)))
|
|
||||||
|
|
||||||
(defn- retrieve-num-users
|
|
||||||
[conn]
|
|
||||||
(-> (db/exec-one! conn ["select count(*) as count from profile;"]) :count))
|
|
||||||
|
|
||||||
(defn- retrieve-num-fonts
|
|
||||||
[conn]
|
|
||||||
(-> (db/exec-one! conn ["select count(*) as count from team_font_variant;"]) :count))
|
|
||||||
|
|
||||||
(defn- retrieve-num-comments
|
|
||||||
[conn]
|
|
||||||
(-> (db/exec-one! conn ["select count(*) as count from comment;"]) :count))
|
|
||||||
|
|
||||||
(def sql:team-averages
|
|
||||||
"with projects_by_team as (
|
|
||||||
select t.id, count(p.id) as num_projects
|
|
||||||
from team as t
|
|
||||||
left join project as p on (p.team_id = t.id)
|
|
||||||
group by 1
|
|
||||||
), files_by_project as (
|
|
||||||
select p.id, count(f.id) as num_files
|
|
||||||
from project as p
|
|
||||||
left join file as f on (f.project_id = p.id)
|
|
||||||
group by 1
|
|
||||||
), comment_threads_by_file as (
|
|
||||||
select f.id, count(ct.id) as num_comment_threads
|
|
||||||
from file as f
|
|
||||||
left join comment_thread as ct on (ct.file_id = f.id)
|
|
||||||
group by 1
|
|
||||||
), users_by_team as (
|
|
||||||
select t.id, count(tp.profile_id) as num_users
|
|
||||||
from team as t
|
|
||||||
left join team_profile_rel as tp on(tp.team_id = t.id)
|
|
||||||
group by 1
|
|
||||||
)
|
|
||||||
select (select avg(num_projects)::integer from projects_by_team) as avg_projects_on_team,
|
|
||||||
(select max(num_projects)::integer from projects_by_team) as max_projects_on_team,
|
|
||||||
(select avg(num_files)::integer from files_by_project) as avg_files_on_project,
|
|
||||||
(select max(num_files)::integer from files_by_project) as max_files_on_project,
|
|
||||||
(select avg(num_comment_threads)::integer from comment_threads_by_file) as avg_comment_threads_on_file,
|
|
||||||
(select max(num_comment_threads)::integer from comment_threads_by_file) as max_comment_threads_on_file,
|
|
||||||
(select avg(num_users)::integer from users_by_team) as avg_users_on_team,
|
|
||||||
(select max(num_users)::integer from users_by_team) as max_users_on_team;")
|
|
||||||
|
|
||||||
(defn- retrieve-team-averages
|
|
||||||
[conn]
|
|
||||||
(->> [sql:team-averages]
|
|
||||||
(db/exec-one! conn)))
|
|
||||||
|
|
||||||
(defn- retrieve-enabled-auth-providers
|
|
||||||
[conn]
|
|
||||||
(let [sql (str "select auth_backend as backend, count(*) as total "
|
|
||||||
" from profile group by 1")
|
|
||||||
rows (db/exec! conn [sql])]
|
|
||||||
(->> rows
|
|
||||||
(map (fn [{:keys [backend total]}]
|
|
||||||
(let [backend (or backend "penpot")]
|
|
||||||
[(keyword (str "auth-backend-" backend))
|
|
||||||
total])))
|
|
||||||
(into {}))))
|
|
||||||
|
|
||||||
(defn- retrieve-jvm-stats
|
|
||||||
[]
|
|
||||||
(let [^Runtime runtime (Runtime/getRuntime)]
|
|
||||||
{:jvm-heap-current (.totalMemory runtime)
|
|
||||||
:jvm-heap-max (.maxMemory runtime)
|
|
||||||
:jvm-cpus (.availableProcessors runtime)
|
|
||||||
:os-arch (System/getProperty "os.arch")
|
|
||||||
:os-name (System/getProperty "os.name")
|
|
||||||
:os-version (System/getProperty "os.version")
|
|
||||||
:user-tz (System/getProperty "user.timezone")}))
|
|
||||||
|
|
||||||
(defn get-stats
|
|
||||||
[conn]
|
|
||||||
(let [referer (if (cf/get :telemetry-with-taiga)
|
|
||||||
"taiga"
|
|
||||||
(cf/get :telemetry-referer))]
|
|
||||||
(-> {:referer referer
|
|
||||||
:public-uri (cf/get :public-uri)
|
|
||||||
:total-teams (retrieve-num-teams conn)
|
|
||||||
:total-projects (retrieve-num-projects conn)
|
|
||||||
:total-files (retrieve-num-files conn)
|
|
||||||
:total-users (retrieve-num-users conn)
|
|
||||||
:total-fonts (retrieve-num-fonts conn)
|
|
||||||
:total-comments (retrieve-num-comments conn)
|
|
||||||
:total-file-changes (retrieve-num-file-changes conn)
|
|
||||||
:total-touched-files (retrieve-num-touched-files conn)}
|
|
||||||
(d/merge
|
|
||||||
(retrieve-team-averages conn)
|
|
||||||
(retrieve-jvm-stats)
|
|
||||||
(retrieve-enabled-auth-providers conn))
|
|
||||||
(d/without-nils))))
|
|
||||||
|
|
||||||
|
|
|
@ -112,7 +112,7 @@
|
||||||
;; "alter table task set unlogged;\n"
|
;; "alter table task set unlogged;\n"
|
||||||
;; "alter table task_default set unlogged;\n"
|
;; "alter table task_default set unlogged;\n"
|
||||||
;; "alter table task_completed set unlogged;\n"
|
;; "alter table task_completed set unlogged;\n"
|
||||||
"alter table audit_log_default set unlogged ;\n"
|
"alter table audit_log set unlogged ;\n"
|
||||||
"alter table storage_object set unlogged;\n"
|
"alter table storage_object set unlogged;\n"
|
||||||
"alter table server_error_report set unlogged;\n"
|
"alter table server_error_report set unlogged;\n"
|
||||||
"alter table server_prop set unlogged;\n"
|
"alter table server_prop set unlogged;\n"
|
||||||
|
|
|
@ -69,6 +69,11 @@
|
||||||
|
|
||||||
(def ^:dynamic ^:private *errors* nil)
|
(def ^:dynamic ^:private *errors* nil)
|
||||||
|
|
||||||
|
(defn- library-exists?
|
||||||
|
[file libraries shape]
|
||||||
|
(or (= (:component-file shape) (:id file))
|
||||||
|
(contains? libraries (:component-file shape))))
|
||||||
|
|
||||||
(defn- report-error
|
(defn- report-error
|
||||||
[code hint shape file page & {:as args}]
|
[code hint shape file page & {:as args}]
|
||||||
(let [error {:code code
|
(let [error {:code code
|
||||||
|
@ -218,12 +223,11 @@
|
||||||
"Shape not expected to be main instance"
|
"Shape not expected to be main instance"
|
||||||
shape file page))
|
shape file page))
|
||||||
|
|
||||||
(let [library-exists? (or (= (:component-file shape) (:id file))
|
(let [library-exists (library-exists? file libraries shape)
|
||||||
(contains? libraries (:component-file shape)))
|
component (when library-exists
|
||||||
component (when library-exists?
|
|
||||||
(ctf/resolve-component shape file libraries {:include-deleted? true}))]
|
(ctf/resolve-component shape file libraries {:include-deleted? true}))]
|
||||||
(if (nil? component)
|
(if (nil? component)
|
||||||
(when library-exists?
|
(when library-exists
|
||||||
(report-error :component-not-found
|
(report-error :component-not-found
|
||||||
(str/ffmt "Component % not found in file %" (:component-id shape) (:component-file shape))
|
(str/ffmt "Component % not found in file %" (:component-id shape) (:component-file shape))
|
||||||
shape file page))
|
shape file page))
|
||||||
|
@ -265,11 +269,10 @@
|
||||||
(defn- check-component-ref
|
(defn- check-component-ref
|
||||||
"Validate that the referenced shape exists in the near component."
|
"Validate that the referenced shape exists in the near component."
|
||||||
[shape file page libraries]
|
[shape file page libraries]
|
||||||
(let [library-exists? (or (= (:component-file shape) (:id file))
|
(let [library-exists (library-exists? file libraries shape)
|
||||||
(contains? libraries (:component-file shape)))
|
ref-shape (when library-exists
|
||||||
ref-shape (when library-exists?
|
|
||||||
(ctf/find-ref-shape file page libraries shape :include-deleted? true))]
|
(ctf/find-ref-shape file page libraries shape :include-deleted? true))]
|
||||||
(when (and library-exists? (nil? ref-shape))
|
(when (and library-exists (nil? ref-shape))
|
||||||
(report-error :ref-shape-not-found
|
(report-error :ref-shape-not-found
|
||||||
(str/ffmt "Referenced shape % not found in near component" (:shape-ref shape))
|
(str/ffmt "Referenced shape % not found in near component" (:shape-ref shape))
|
||||||
shape file page))))
|
shape file page))))
|
||||||
|
@ -313,20 +316,25 @@
|
||||||
- :component-root
|
- :component-root
|
||||||
- :shape-ref"
|
- :shape-ref"
|
||||||
[shape file page libraries]
|
[shape file page libraries]
|
||||||
(check-component-not-main-head shape file page libraries)
|
;; We propagate have to propagate to nested shapes if library is valid or not
|
||||||
(check-component-root shape file page)
|
(let [library-exists (library-exists? file libraries shape)]
|
||||||
(check-component-ref shape file page libraries)
|
(check-component-not-main-head shape file page libraries)
|
||||||
(run! #(check-shape % file page libraries :context :copy-top) (:shapes shape)))
|
(check-component-root shape file page)
|
||||||
|
(check-component-ref shape file page libraries)
|
||||||
|
(run! #(check-shape % file page libraries :context :copy-top :library-exists library-exists) (:shapes shape))))
|
||||||
|
|
||||||
(defn- check-shape-copy-root-nested
|
(defn- check-shape-copy-root-nested
|
||||||
"Root shape of a nested copy instance
|
"Root shape of a nested copy instance
|
||||||
- :component-id
|
- :component-id
|
||||||
- :component-file
|
- :component-file
|
||||||
- :shape-ref"
|
- :shape-ref"
|
||||||
[shape file page libraries]
|
[shape file page libraries library-exists]
|
||||||
(check-component-not-main-head shape file page libraries)
|
(check-component-not-main-head shape file page libraries)
|
||||||
(check-component-not-root shape file page)
|
(check-component-not-root shape file page)
|
||||||
(check-component-ref shape file page libraries)
|
;; We can have situations where the nested copy and the ancestor copy come from different libraries and some of them have been dettached
|
||||||
|
;; so we only validate the shape-ref if the ancestor is from a valid library
|
||||||
|
(when library-exists
|
||||||
|
(check-component-ref shape file page libraries))
|
||||||
(run! #(check-shape % file page libraries :context :copy-nested) (:shapes shape)))
|
(run! #(check-shape % file page libraries :context :copy-nested) (:shapes shape)))
|
||||||
|
|
||||||
(defn- check-shape-main-not-root
|
(defn- check-shape-main-not-root
|
||||||
|
@ -367,7 +375,7 @@
|
||||||
- :main-any
|
- :main-any
|
||||||
- :copy-any
|
- :copy-any
|
||||||
"
|
"
|
||||||
[shape-id file page libraries & {:keys [context] :or {context :not-component}}]
|
[shape-id file page libraries & {:keys [context library-exists] :or {context :not-component library-exists false}}]
|
||||||
(let [shape (ctst/get-shape page shape-id)]
|
(let [shape (ctst/get-shape page shape-id)]
|
||||||
(when (some? shape)
|
(when (some? shape)
|
||||||
(check-geometry shape file page)
|
(check-geometry shape file page)
|
||||||
|
@ -406,7 +414,7 @@
|
||||||
(report-error :nested-copy-not-allowed
|
(report-error :nested-copy-not-allowed
|
||||||
"Nested copy component only allowed inside other component"
|
"Nested copy component only allowed inside other component"
|
||||||
shape file page)
|
shape file page)
|
||||||
(check-shape-copy-root-nested shape file page libraries)))))
|
(check-shape-copy-root-nested shape file page libraries library-exists)))))
|
||||||
|
|
||||||
(if (ctk/in-component-copy? shape)
|
(if (ctk/in-component-copy? shape)
|
||||||
(if-not (#{:copy-top :copy-nested :copy-any} context)
|
(if-not (#{:copy-top :copy-nested :copy-any} context)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
export PATH=/usr/lib/jvm/openjdk/bin:/usr/local/nodejs/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
|
export PATH=/usr/lib/jvm/openjdk/bin:/usr/local/nodejs/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
|
||||||
export JAVA_OPTS="-Xmx900m -Xms50m"
|
export JAVA_OPTS="-Xmx1000m -Xms50m"
|
||||||
|
|
||||||
alias l='ls --color -GFlh'
|
alias l='ls --color -GFlh'
|
||||||
alias rm='rm -r'
|
alias rm='rm -r'
|
||||||
|
|
|
@ -19,12 +19,12 @@ popd
|
||||||
|
|
||||||
tmux -2 new-session -d -s penpot
|
tmux -2 new-session -d -s penpot
|
||||||
|
|
||||||
tmux rename-window -t penpot:0 'gulp'
|
tmux rename-window -t penpot:0 'frontend watch'
|
||||||
tmux select-window -t penpot:0
|
tmux select-window -t penpot:0
|
||||||
tmux send-keys -t penpot 'cd penpot/frontend' enter C-l
|
tmux send-keys -t penpot 'cd penpot/frontend' enter C-l
|
||||||
tmux send-keys -t penpot 'npx gulp watch' enter
|
tmux send-keys -t penpot 'yarn run watch' enter
|
||||||
|
|
||||||
tmux new-window -t penpot:1 -n 'shadow watch'
|
tmux new-window -t penpot:1 -n 'frontend shadow'
|
||||||
tmux select-window -t penpot:1
|
tmux select-window -t penpot:1
|
||||||
tmux send-keys -t penpot 'cd penpot/frontend' enter C-l
|
tmux send-keys -t penpot 'cd penpot/frontend' enter C-l
|
||||||
tmux send-keys -t penpot 'clojure -M:dev:shadow-cljs watch main' enter
|
tmux send-keys -t penpot 'clojure -M:dev:shadow-cljs watch main' enter
|
||||||
|
|
|
@ -19,21 +19,17 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"fmt:clj:check": "cljfmt check --parallel=false src/ test/",
|
"fmt:clj:check": "cljfmt check --parallel=false src/ test/",
|
||||||
"fmt:clj": "cljfmt fix --parallel=true src/ test/",
|
"fmt:clj": "cljfmt fix --parallel=true src/ test/",
|
||||||
"test:compile": "clojure -M:dev:shadow-cljs compile test --config-merge '{:autorun false}'",
|
|
||||||
"lint:scss": "yarn run prettier -c resources/styles -c src/**/*.scss",
|
"lint:scss": "yarn run prettier -c resources/styles -c src/**/*.scss",
|
||||||
"lint:scss:fix": "yarn run prettier -c resources/styles -c src/**/*.scss -w",
|
"lint:scss:fix": "yarn run prettier -c resources/styles -c src/**/*.scss -w",
|
||||||
"lint:clj": "clj-kondo --parallel --lint src/",
|
"lint:clj": "clj-kondo --parallel --lint src/",
|
||||||
|
"test:compile": "clojure -M:dev:shadow-cljs compile test --config-merge '{:autorun false}'",
|
||||||
"test:run": "node target/tests.cjs",
|
"test:run": "node target/tests.cjs",
|
||||||
"test:watch": "clojure -M:dev:shadow-cljs watch test",
|
"test:watch": "clojure -M:dev:shadow-cljs watch test",
|
||||||
"test": "yarn run test:compile && yarn run test:run",
|
"test": "yarn run test:compile && yarn run test:run",
|
||||||
"gulp:watch": "gulp watch",
|
"translations:validate": "node ./scripts/validate-translations.js",
|
||||||
"watch": "shadow-cljs watch main",
|
"translations:find-unused": "node ./scripts/find-unused-translations.js",
|
||||||
"validate-translations": "node ./scripts/validate-translations.js",
|
"compile": "node ./scripts/compile.js",
|
||||||
"find-unused-translations": "node ./scripts/find-unused-translations.js",
|
"watch": "node ./scripts/watch.js",
|
||||||
"build:clean": "gulp clean:output && gulp clean:dist",
|
|
||||||
"build:styles": "gulp build:styles",
|
|
||||||
"build:assets": "gulp build:assets",
|
|
||||||
"build:copy": "gulp build:copy",
|
|
||||||
"storybook:compile": "gulp template:storybook && clojure -M:dev:shadow-cljs compile storybook",
|
"storybook:compile": "gulp template:storybook && clojure -M:dev:shadow-cljs compile storybook",
|
||||||
"storybook:watch": "npm run storybook:compile && concurrently \"clojure -M:dev:shadow-cljs watch storybook\" \"storybook dev -p 6006\"",
|
"storybook:watch": "npm run storybook:compile && concurrently \"clojure -M:dev:shadow-cljs watch storybook\" \"storybook dev -p 6006\"",
|
||||||
"storybook:build": "npm run storybook:compile && storybook build"
|
"storybook:build": "npm run storybook:compile && storybook build"
|
||||||
|
@ -67,19 +63,26 @@
|
||||||
"map-stream": "0.0.7",
|
"map-stream": "0.0.7",
|
||||||
"marked": "^12.0.0",
|
"marked": "^12.0.0",
|
||||||
"mkdirp": "^3.0.1",
|
"mkdirp": "^3.0.1",
|
||||||
|
"mustache": "^4.2.0",
|
||||||
"nodemon": "^3.1.0",
|
"nodemon": "^3.1.0",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
|
"p-limit": "^5.0.0",
|
||||||
"postcss": "^8.4.35",
|
"postcss": "^8.4.35",
|
||||||
"postcss-clean": "^1.2.2",
|
"postcss-clean": "^1.2.2",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
|
"pretty-time": "^1.1.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"rimraf": "^5.0.5",
|
"rimraf": "^5.0.5",
|
||||||
"sass": "^1.71.1",
|
"sass": "^1.71.1",
|
||||||
|
"sass-embedded": "^1.71.1",
|
||||||
"shadow-cljs": "2.27.4",
|
"shadow-cljs": "2.27.4",
|
||||||
"storybook": "^7.6.17",
|
"storybook": "^7.6.17",
|
||||||
|
"svg-sprite": "^2.0.2",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"vite": "^5.1.4",
|
"vite": "^5.1.4",
|
||||||
"vitest": "^1.3.1"
|
"vitest": "^1.3.1",
|
||||||
|
"watcher": "^2.3.0",
|
||||||
|
"workerpool": "^9.1.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"date-fns": "^3.3.1",
|
"date-fns": "^3.3.1",
|
||||||
|
|
|
@ -1,3 +1 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 15.674"><path fill="#fff" fill-rule="evenodd" d="M7.976 0C3.566 0 0 3.592 0 8.035a8.03 8.03 0 0 0 5.454 7.623c.396.08.541-.173.541-.385 0-.187-.013-.825-.013-1.49-2.219.479-2.681-.958-2.681-.958-.356-.932-.885-1.171-.885-1.171-.726-.492.053-.492.053-.492.806.053 1.229.825 1.229.825.713 1.223 1.862.878 2.324.665.066-.519.277-.878.502-1.078-1.77-.186-3.632-.878-3.632-3.964 0-.878.317-1.597.819-2.155-.079-.2-.357-1.025.079-2.129 0 0 .674-.213 2.192.825a7.633 7.633 0 0 1 3.988 0c1.519-1.038 2.192-.825 2.192-.825.436 1.104.159 1.929.079 2.129.516.558.819 1.277.819 2.155 0 3.086-1.862 3.765-3.644 3.964.29.253.541.732.541 1.49 0 1.078-.013 1.943-.013 2.208 0 .213.145.466.541.386a8.028 8.028 0 0 0 5.454-7.623C15.952 3.592 12.374 0 7.976 0Z" class="fills" clip-rule="evenodd"/></svg>
|
||||||
<path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" fill="#fff"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 687 B After Width: | Height: | Size: 852 B |
|
@ -1 +1 @@
|
||||||
<svg viewBox="3658.551 302.026 20 17.949" width="20" height="17.949" xmlns="http://www.w3.org/2000/svg" style="-webkit-print-color-adjust:exact"><path d="m3668.55 319.974 3.685-11.043h-7.364l3.68 11.043ZM3659.71 308.932l-1.122 3.355a.733.733 0 0 0 .277.83l9.685 6.857-8.84-11.042ZM3659.71 308.931h5.16l-2.22-6.65c-.114-.34-.61-.34-.727 0l-2.213 6.65Z" style="fill:#fff"/><path d="m3677.396 308.932 1.118 3.355a.733.733 0 0 1-.276.83l-9.688 6.857 8.846-11.042ZM3677.396 308.931h-5.16l2.216-6.65c.114-.34.61-.34.727 0l2.217 6.65ZM3668.55 319.974l3.685-11.042h5.16l-8.845 11.042ZM3668.55 319.974l-8.84-11.042h5.16l3.68 11.042Z" style="fill:#fff"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" id="screenshot-43864c00-8517-80fc-8004-14a71e4f14d1" fill="none" version="1.1" viewBox="0 0 16 16"><g id="shape-43864c00-8517-80fc-8004-14a71e4f14d1" fill="#000"><defs id="shape-43864c00-8517-80fc-8004-14a71e4f65d8" fill="#000"><style id="shape-43864c00-8517-80fc-8004-14a71e4f65da">.cls-2{fill:#fc6d26}</style></defs><g id="shape-43864c00-8517-80fc-8004-14a71e4f65d9" fill="#000"><g id="shape-43864c00-8517-80fc-8004-14a71e4f65dc"><path id="fills-43864c00-8517-80fc-8004-14a71e4f65dc" fill="#e24329" d="M15.733 6.099Zl-2.2-5.741a.561.561 0 0 0-.224-.269.583.583 0 0 0-.666.035.587.587 0 0 0-.194.294l-1.47 4.498H5.025L3.555.418a.57.57 0 0 0-.194-.294.581.581 0 0 0-.666-.036.57.57 0 0 0-.224.27L.289 6.038l-.022.058a4.043 4.043 0 0 0 1.342 4.673l.007.006.02.014 3.317 2.485 1.642 1.242.999.754a.67.67 0 0 0 .813 0l1-.754 1.641-1.242 3.337-2.5.009-.006a4.045 4.045 0 0 0 1.339-4.669Z" class="fills"/></g><g id="shape-43864c00-8517-80fc-8004-14a71e4f65dd"><g id="fills-43864c00-8517-80fc-8004-14a71e4f65dd" class="fills"><path d="M15.733 6.099Zc-1.083.16-2.083.61-2.95 1.259L8 10.974l3.047 2.303 3.337-2.499.008-.007a4.045 4.045 0 0 0 1.341-4.672Z" class="cls-2"/></g></g><g id="shape-43864c00-8517-80fc-8004-14a71e4f65de"><path id="fills-43864c00-8517-80fc-8004-14a71e4f65de" fill="#fca326" d="m4.953 13.277 1.642 1.242.999.755a.674.674 0 0 0 .813 0l1-.755 1.641-1.242S9.629 12.203 8 10.974c-1.629 1.229-3.047 2.303-3.047 2.303Z" class="fills"/></g><g id="shape-43864c00-8517-80fc-8004-14a71e4f65df"><g id="fills-43864c00-8517-80fc-8004-14a71e4f65df" class="fills"><path d="M3.217 7.358a7.364 7.364 0 0 0-2.928-1.32l-.022.058a4.043 4.043 0 0 0 1.342 4.673l.007.006.02.014 3.317 2.485L8 10.971Z" class="cls-2"/></g></g></g></g></svg>
|
Before Width: | Height: | Size: 650 B After Width: | Height: | Size: 1.7 KiB |
|
@ -1 +1 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="5345 -1143 500 500"><path fill="#fff" fill-rule="evenodd" d="M5845-887c0-18-1-35-4-51h-240v96h137c-6 32-24 58-51 76v63h82c49-44 76-108 76-184z" clip-rule="evenodd"/><path fill="#fff" fill-rule="evenodd" d="M5601-643c68 0 126-22 168-60l-82-63a156 156 0 0 1-229-79h-85v64c42 82 128 138 228 138z" clip-rule="evenodd"/><path fill="#fff" fill-rule="evenodd" d="M5458-845a148 148 0 0 1 0-95v-65h-85a246 246 0 0 0 0 224z" clip-rule="evenodd"/><path fill="#fff" fill-rule="evenodd" d="M5601-1043c37 0 71 12 97 37l73-72a256 256 0 0 0-399 73l86 65c20-59 76-103 143-103z" clip-rule="evenodd"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/><path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/><path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/><path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/><path fill="none" d="M1 1h22v22H1z"/></svg>
|
Before Width: | Height: | Size: 638 B After Width: | Height: | Size: 719 B |
|
@ -1 +1 @@
|
||||||
<svg viewBox="7437 302 20.011 18.182" width="20.011" height="18.182" xmlns="http://www.w3.org/2000/svg" style="-webkit-print-color-adjust:exact"><path d="M7455.039 309.1c-1.9-1.183-4.555-1.918-7.46-1.918-5.845 0-10.579 2.922-10.579 6.526 0 3.3 3.945 6.007 9.055 6.473v-1.9c-3.442-.43-6.024-2.313-6.024-4.573 0-2.564 3.37-4.662 7.549-4.662 2.08 0 3.962.52 5.325 1.363l-1.937 1.202h6.043v-3.73l-1.972 1.22Zm-8.984-5.146v16.227l3.03-1.9V302l-3.03 1.954Z" style="fill:#fff;fill-opacity:1"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16"><g fill="#000"><path fill="#b2b2b2" d="M7.339 13.094v.207c-2.477-.311-4.354-1.669-4.354-3.305 0-1.229 1.058-2.3 2.64-2.891l-.062-1.48C2.778 6.328.813 8.019.813 9.996c0 2.384 2.839 4.348 6.529 4.678h.001l-.004-1.58Z" class="fills"/><path fill="#f7931e" d="M9.524 13.647ZL9.521 1.326 7.339 2.445v4.482h.004v7.747Z" class="fills"/><path fill="#b2b2b2" d="m15.187 9.123-.295-3.128-1.123.635c-.798-.485-1.816-1.083-3.144-1.091v1.372c.11.031.216.064.322.098.442.144.849.324 1.208.535l-1.181.664 4.213.915Z" class="fills"/></g></svg>
|
Before Width: | Height: | Size: 492 B After Width: | Height: | Size: 598 B |
|
@ -26,7 +26,6 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
window.penpotTranslations = JSON.parse({{& translations}});
|
window.penpotTranslations = JSON.parse({{& translations}});
|
||||||
window.penpotThemes = {{& themes}};
|
|
||||||
window.penpotVersion = "%version%";
|
window.penpotVersion = "%version%";
|
||||||
window.penpotBuildDate = "%buildDate%";
|
window.penpotBuildDate = "%buildDate%";
|
||||||
</script>
|
</script>
|
||||||
|
@ -39,8 +38,8 @@
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{{>../public/images/sprites/symbol/icons.svg}}
|
{{> ../public/images/sprites/symbol/icons.svg }}
|
||||||
{{>../public/images/sprites/symbol/cursors.svg}}
|
{{> ../public/images/sprites/symbol/cursors.svg }}
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<section id="modal"></section>
|
<section id="modal"></section>
|
||||||
{{# manifest}}
|
{{# manifest}}
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
<link rel="icon" href="images/favicon.png" />
|
<link rel="icon" href="images/favicon.png" />
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
window.penpotThemes = {{& themes}};
|
|
||||||
window.penpotVersion = "%version%";
|
window.penpotVersion = "%version%";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
405
frontend/scripts/_helpers.js
Normal file
|
@ -0,0 +1,405 @@
|
||||||
|
import proc from "node:child_process";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import ph from "node:path";
|
||||||
|
import os from "node:os";
|
||||||
|
import url from "node:url";
|
||||||
|
|
||||||
|
import * as marked from "marked";
|
||||||
|
import SVGSpriter from "svg-sprite";
|
||||||
|
import Watcher from "watcher";
|
||||||
|
import gettext from "gettext-parser";
|
||||||
|
import l from "lodash";
|
||||||
|
import log from "fancy-log";
|
||||||
|
import mustache from "mustache";
|
||||||
|
import pLimit from "p-limit";
|
||||||
|
import ppt from "pretty-time";
|
||||||
|
import wpool from "workerpool";
|
||||||
|
|
||||||
|
function getCoreCount() {
|
||||||
|
return os.cpus().length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// const __filename = url.fileURLToPath(import.meta.url);
|
||||||
|
export const dirname = url.fileURLToPath(new URL(".", import.meta.url));
|
||||||
|
|
||||||
|
export function startWorker() {
|
||||||
|
return wpool.pool(dirname + "/_worker.js", {
|
||||||
|
maxWorkers: getCoreCount()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findFiles(basePath, predicate, options={}) {
|
||||||
|
predicate = predicate ?? function() { return true; }
|
||||||
|
|
||||||
|
let files = await fs.readdir(basePath, {recursive: options.recursive ?? false})
|
||||||
|
files = files.filter((path) => path.endsWith(".svg"));
|
||||||
|
files = files.map((path) => ph.join(basePath, path));
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncDirs(originPath, destPath) {
|
||||||
|
const command = `rsync -ar --delete ${originPath} ${destPath}`;
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
proc.exec(command, (cause, stdout) => {
|
||||||
|
if (cause) { reject(cause); }
|
||||||
|
else { resolve(); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSassFile(path) {
|
||||||
|
return path.endsWith(".scss");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSvgFile(path) {
|
||||||
|
return path.endsWith(".scss");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function compileSass(worker, path, options) {
|
||||||
|
path = ph.resolve(path);
|
||||||
|
|
||||||
|
log.info("compile:", path);
|
||||||
|
return worker.exec("compileSass", [path, options]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function compileSassAll(worker) {
|
||||||
|
const limitFn = pLimit(4);
|
||||||
|
const sourceDir = "src";
|
||||||
|
|
||||||
|
let files = await fs.readdir(sourceDir, { recursive: true })
|
||||||
|
files = files.filter((path) => path.endsWith(".scss"));
|
||||||
|
files = files.map((path) => ph.join(sourceDir, path));
|
||||||
|
// files = files.slice(0, 10);
|
||||||
|
|
||||||
|
const procs = [
|
||||||
|
compileSass(worker, "resources/styles/main-default.scss", {}),
|
||||||
|
compileSass(worker, "resources/styles/debug.scss", {})
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let path of files) {
|
||||||
|
const proc = limitFn(() => compileSass(worker, path, {modules: true}));
|
||||||
|
procs.push(proc);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await Promise.all(procs);
|
||||||
|
|
||||||
|
return result.reduce((acc, item, index) => {
|
||||||
|
acc.index[item.outputPath] = item.css;
|
||||||
|
acc.items.push(item.outputPath);
|
||||||
|
return acc;
|
||||||
|
}, {index:{}, items: []});
|
||||||
|
}
|
||||||
|
|
||||||
|
function compare(a, b) {
|
||||||
|
if (a < b) {
|
||||||
|
return -1;
|
||||||
|
} else if (a > b) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function concatSass(data) {
|
||||||
|
const output = []
|
||||||
|
|
||||||
|
for (let path of data.items) {
|
||||||
|
output.push(data.index[path]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return output.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function watch(baseDir, predicate, callback) {
|
||||||
|
predicate = predicate ?? (() => true);
|
||||||
|
|
||||||
|
|
||||||
|
const watcher = new Watcher(baseDir, {
|
||||||
|
persistent: true,
|
||||||
|
recursive: true
|
||||||
|
});
|
||||||
|
|
||||||
|
watcher.on("change", (path) => {
|
||||||
|
if (predicate(path)) {
|
||||||
|
callback(path);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readShadowManifest() {
|
||||||
|
try {
|
||||||
|
const manifestPath = "resources/public/js/manifest.json"
|
||||||
|
let content = await fs.readFile(manifestPath, { encoding: "utf8" });
|
||||||
|
content = JSON.parse(content);
|
||||||
|
|
||||||
|
const index = {
|
||||||
|
config: "js/config.js?ts=" + Date.now(),
|
||||||
|
polyfills: "js/polyfills.js?ts=" + Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let item of content) {
|
||||||
|
index[item.name] = "js/" + item["output-name"];
|
||||||
|
}
|
||||||
|
|
||||||
|
return index;
|
||||||
|
} catch (cause) {
|
||||||
|
// log.error("error on reading manifest (using default)", cause);
|
||||||
|
return {
|
||||||
|
config: "js/config.js",
|
||||||
|
polyfills: "js/polyfills.js",
|
||||||
|
main: "js/main.js",
|
||||||
|
shared: "js/shared.js",
|
||||||
|
worker: "js/worker.js",
|
||||||
|
rasterizer: "js/rasterizer.js",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderTemplate(path, context={}, partials={}) {
|
||||||
|
const content = await fs.readFile(path, {encoding: "utf-8"});
|
||||||
|
|
||||||
|
const ts = Math.floor(new Date());
|
||||||
|
|
||||||
|
context = Object.assign({}, context, {
|
||||||
|
ts: ts,
|
||||||
|
isDebug: process.env.NODE_ENV !== "production"
|
||||||
|
});
|
||||||
|
|
||||||
|
return mustache.render(content, context, partials);
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderer = {
|
||||||
|
link(href, title, text) {
|
||||||
|
return `<a href="${href}" target="_blank">${text}</a>`;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
marked.use({ renderer });
|
||||||
|
|
||||||
|
async function readTranslations() {
|
||||||
|
const langs = [
|
||||||
|
"ar",
|
||||||
|
"ca",
|
||||||
|
"de",
|
||||||
|
"el",
|
||||||
|
"en",
|
||||||
|
"eu",
|
||||||
|
"it",
|
||||||
|
"es",
|
||||||
|
"fa",
|
||||||
|
"fr",
|
||||||
|
"he",
|
||||||
|
"nb_NO",
|
||||||
|
"pl",
|
||||||
|
"pt_BR",
|
||||||
|
"ro",
|
||||||
|
"id",
|
||||||
|
"ru",
|
||||||
|
"tr",
|
||||||
|
"zh_CN",
|
||||||
|
"zh_Hant",
|
||||||
|
"hr",
|
||||||
|
"gl",
|
||||||
|
"pt_PT",
|
||||||
|
"cs",
|
||||||
|
"fo",
|
||||||
|
"ko",
|
||||||
|
"lv",
|
||||||
|
"nl",
|
||||||
|
// this happens when file does not matches correct
|
||||||
|
// iso code for the language.
|
||||||
|
["ja_jp", "jpn_JP"],
|
||||||
|
// ["fi", "fin_FI"],
|
||||||
|
["uk", "ukr_UA"],
|
||||||
|
"ha"
|
||||||
|
];
|
||||||
|
const result = {};
|
||||||
|
|
||||||
|
for (let lang of langs) {
|
||||||
|
let filename = `${lang}.po`;
|
||||||
|
if (l.isArray(lang)) {
|
||||||
|
filename = `${lang[1]}.po`;
|
||||||
|
lang = lang[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = await fs.readFile(`./translations/${filename}`, { encoding: "utf-8" });
|
||||||
|
|
||||||
|
lang = lang.toLowerCase();
|
||||||
|
|
||||||
|
const data = gettext.po.parse(content, "utf-8");
|
||||||
|
const trdata = data.translations[""];
|
||||||
|
|
||||||
|
for (let key of Object.keys(trdata)) {
|
||||||
|
if (key === "") continue;
|
||||||
|
const comments = trdata[key].comments || {};
|
||||||
|
|
||||||
|
if (l.isNil(result[key])) {
|
||||||
|
result[key] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMarkdown = l.includes(comments.flag, "markdown");
|
||||||
|
|
||||||
|
const msgs = trdata[key].msgstr;
|
||||||
|
if (msgs.length === 1) {
|
||||||
|
let message = msgs[0];
|
||||||
|
if (isMarkdown) {
|
||||||
|
message = marked.parseInline(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
result[key][lang] = message;
|
||||||
|
} else {
|
||||||
|
result[key][lang] = msgs.map((item) => {
|
||||||
|
if (isMarkdown) {
|
||||||
|
return marked.parseInline(item);
|
||||||
|
} else {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// if (key === "modals.delete-font.title") {
|
||||||
|
// console.dir(trdata[key], {depth:10});
|
||||||
|
// console.dir(result[key], {depth:10});
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateSvgSprite(files, prefix) {
|
||||||
|
const spriter = new SVGSpriter({
|
||||||
|
mode: {
|
||||||
|
symbol: { inline: true }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let path of files) {
|
||||||
|
const name = `${prefix}${ph.basename(path)}`
|
||||||
|
const content = await fs.readFile(path, {encoding: "utf-8"});
|
||||||
|
spriter.add(name, name, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { result } = await spriter.compileAsync();
|
||||||
|
const resource = result.symbol.sprite;
|
||||||
|
return resource.contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateSvgSprites() {
|
||||||
|
await fs.mkdir("resources/public/images/sprites/symbol/", { recursive: true });
|
||||||
|
|
||||||
|
const icons = await findFiles("resources/images/icons/", isSvgFile);
|
||||||
|
const iconsSprite = await generateSvgSprite(icons, "icon-");
|
||||||
|
await fs.writeFile("resources/public/images/sprites/symbol/icons.svg", iconsSprite);
|
||||||
|
|
||||||
|
const cursors = await findFiles("resources/images/cursors/", isSvgFile);
|
||||||
|
const cursorsSprite = await generateSvgSprite(icons, "cursor-");
|
||||||
|
await fs.writeFile("resources/public/images/sprites/symbol/cursors.svg", cursorsSprite);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateTemplates() {
|
||||||
|
await fs.mkdir("./resources/public/", { recursive: true });
|
||||||
|
|
||||||
|
const translations = await readTranslations();
|
||||||
|
const manifest = await readShadowManifest();
|
||||||
|
let content;
|
||||||
|
|
||||||
|
const iconsSprite = await fs.readFile("resources/public/images/sprites/symbol/icons.svg", "utf8");
|
||||||
|
const cursorsSprite = await fs.readFile("resources/public/images/sprites/symbol/cursors.svg", "utf8");
|
||||||
|
const partials = {
|
||||||
|
"../public/images/sprites/symbol/icons.svg": iconsSprite,
|
||||||
|
"../public/images/sprites/symbol/cursors.svg": cursorsSprite,
|
||||||
|
};
|
||||||
|
|
||||||
|
content = await renderTemplate("resources/templates/index.mustache", {
|
||||||
|
manifest: manifest,
|
||||||
|
translations: JSON.stringify(translations),
|
||||||
|
}, partials);
|
||||||
|
|
||||||
|
await fs.writeFile("./resources/public/index.html", content);
|
||||||
|
|
||||||
|
content = await renderTemplate("resources/templates/preview-body.mustache", {
|
||||||
|
manifest: manifest,
|
||||||
|
translations: JSON.stringify(translations),
|
||||||
|
});
|
||||||
|
|
||||||
|
await fs.writeFile("./.storybook/preview-body.html", content);
|
||||||
|
|
||||||
|
content = await renderTemplate("resources/templates/render.mustache", {
|
||||||
|
manifest: manifest,
|
||||||
|
translations: JSON.stringify(translations),
|
||||||
|
});
|
||||||
|
|
||||||
|
await fs.writeFile("./resources/public/render.html", content);
|
||||||
|
|
||||||
|
content = await renderTemplate("resources/templates/rasterizer.mustache", {
|
||||||
|
manifest: manifest,
|
||||||
|
translations: JSON.stringify(translations),
|
||||||
|
});
|
||||||
|
|
||||||
|
await fs.writeFile("./resources/public/rasterizer.html", content);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function compileStyles() {
|
||||||
|
const worker = startWorker();
|
||||||
|
const start = process.hrtime();
|
||||||
|
|
||||||
|
log.info("init: compile styles")
|
||||||
|
let result = await compileSassAll(worker);
|
||||||
|
result = concatSass(result);
|
||||||
|
|
||||||
|
await fs.mkdir("./resources/public/css", { recursive: true });
|
||||||
|
await fs.writeFile("./resources/public/css/main.css", result);
|
||||||
|
|
||||||
|
const end = process.hrtime(start);
|
||||||
|
log.info("done: compile styles", `(${ppt(end)})`);
|
||||||
|
worker.terminate();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function compileSvgSprites() {
|
||||||
|
const start = process.hrtime();
|
||||||
|
log.info("init: compile svgsprite")
|
||||||
|
await generateSvgSprites();
|
||||||
|
const end = process.hrtime(start);
|
||||||
|
log.info("done: compile svgsprite", `(${ppt(end)})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function compileTemplates() {
|
||||||
|
const start = process.hrtime();
|
||||||
|
log.info("init: compile templates")
|
||||||
|
await generateTemplates();
|
||||||
|
const end = process.hrtime(start);
|
||||||
|
log.info("done: compile templates", `(${ppt(end)})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function compilePolyfills() {
|
||||||
|
const start = process.hrtime();
|
||||||
|
log.info("init: compile polyfills")
|
||||||
|
|
||||||
|
|
||||||
|
const files = await findFiles("resources/polyfills/");
|
||||||
|
let result = [];
|
||||||
|
for (let path of files) {
|
||||||
|
const content = await fs.readFile(path, {encoding:"utf-8"});
|
||||||
|
result.push(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.mkdir("./resources/public/js", { recursive: true });
|
||||||
|
fs.writeFile("resources/public/js/polyfills.js", result.join("\n"));
|
||||||
|
|
||||||
|
const end = process.hrtime(start);
|
||||||
|
log.info("done: compile polyfills", `(${ppt(end)})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function copyAssets() {
|
||||||
|
const start = process.hrtime();
|
||||||
|
log.info("init: copy assets")
|
||||||
|
|
||||||
|
await syncDirs("resources/images/", "resources/public/images/");
|
||||||
|
await syncDirs("resources/fonts/", "resources/public/fonts/");
|
||||||
|
|
||||||
|
const end = process.hrtime(start);
|
||||||
|
log.info("done: copy assets", `(${ppt(end)})`);
|
||||||
|
}
|
||||||
|
|
97
frontend/scripts/_worker.js
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
import proc from "node:child_process";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import ph from "node:path";
|
||||||
|
import url from "node:url";
|
||||||
|
import * as sass from "sass-embedded";
|
||||||
|
import log from "fancy-log";
|
||||||
|
|
||||||
|
import wpool from "workerpool";
|
||||||
|
import postcss from "postcss";
|
||||||
|
import modulesProcessor from "postcss-modules";
|
||||||
|
import autoprefixerProcessor from "autoprefixer";
|
||||||
|
|
||||||
|
const compiler = await sass.initAsyncCompiler();
|
||||||
|
|
||||||
|
async function compileFile(path) {
|
||||||
|
const dir = ph.dirname(path);
|
||||||
|
const name = ph.basename(path, ".scss");
|
||||||
|
const dest = `${dir}${ph.sep}${name}.css`;
|
||||||
|
|
||||||
|
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const result = await compiler.compileAsync(path, {
|
||||||
|
loadPaths: ["node_modules/animate.css", "resources/styles/common/", "resources/styles"],
|
||||||
|
sourceMap: false
|
||||||
|
});
|
||||||
|
// console.dir(result);
|
||||||
|
resolve({
|
||||||
|
inputPath: path,
|
||||||
|
outputPath: dest,
|
||||||
|
css: result.css
|
||||||
|
});
|
||||||
|
} catch (cause) {
|
||||||
|
// console.error(cause);
|
||||||
|
reject(cause);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function configureModulesProcessor(options) {
|
||||||
|
const ROOT_NAME = "app";
|
||||||
|
|
||||||
|
return modulesProcessor({
|
||||||
|
getJSON: (cssFileName, json, outputFileName) => {
|
||||||
|
// We do nothing because we don't want the generated JSON files
|
||||||
|
},
|
||||||
|
// Calculates the whole css-module selector name.
|
||||||
|
// Should be the same as the one in the file `/src/app/main/style.clj`
|
||||||
|
generateScopedName: (selector, filename, css) => {
|
||||||
|
const dir = ph.dirname(filename);
|
||||||
|
const name = ph.basename(filename, ".css");
|
||||||
|
const parts = dir.split("/");
|
||||||
|
const rootIdx = parts.findIndex((s) => s === ROOT_NAME);
|
||||||
|
return parts.slice(rootIdx + 1).join("_") + "_" + name + "__" + selector;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function configureProcessor(options={}) {
|
||||||
|
const processors = [];
|
||||||
|
|
||||||
|
if (options.modules) {
|
||||||
|
processors.push(configureModulesProcessor(options));
|
||||||
|
}
|
||||||
|
processors.push(autoprefixerProcessor);
|
||||||
|
|
||||||
|
return postcss(processors);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function postProcessFile(data, options) {
|
||||||
|
const proc = configureProcessor(options);
|
||||||
|
|
||||||
|
// We compile to the same path (all in memory)
|
||||||
|
const result = await proc.process(data.css, {
|
||||||
|
from: data.outputPath,
|
||||||
|
to: data.outputPath,
|
||||||
|
map: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.assign(data, {
|
||||||
|
css: result.css
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function compile(path, options) {
|
||||||
|
let result = await compileFile(path);
|
||||||
|
return await postProcessFile(result, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
wpool.worker({
|
||||||
|
compileSass: compile
|
||||||
|
}, {
|
||||||
|
onTerminate: async (code) => {
|
||||||
|
// log.info("worker: terminate");
|
||||||
|
await compiler.dispose();
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,4 +1,6 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
# NOTE: this script should be called from the parent directory to
|
||||||
|
# properly work.
|
||||||
|
|
||||||
set -ex
|
set -ex
|
||||||
|
|
||||||
|
@ -12,13 +14,13 @@ export EXTRA_PARAMS=$SHADOWCLJS_EXTRA_PARAMS;
|
||||||
export NODE_ENV=production;
|
export NODE_ENV=production;
|
||||||
|
|
||||||
yarn install || exit 1;
|
yarn install || exit 1;
|
||||||
yarn run build:clean || exit 1;
|
rm -rf resources/public;
|
||||||
yarn run build:styles || exit 1;
|
rm -rf target/dist;
|
||||||
|
|
||||||
clojure -J-Xms100M -J-Xmx1000M -J-XX:+UseSerialGC -M:dev:shadow-cljs release main --config-merge "{:release-version \"${CURRENT_HASH}\"}" $EXTRA_PARAMS || exit 1
|
clojure -M:dev:shadow-cljs release main --config-merge "{:release-version \"${CURRENT_HASH}\"}" $EXTRA_PARAMS || exit 1
|
||||||
|
|
||||||
yarn run build:assets || exit 1;
|
yarn run compile || exit 1;
|
||||||
yarn run build:copy || exit 1;
|
rsync -avr resources/public/ target/dist/
|
||||||
|
|
||||||
sed -i -re "s/\%version\%/$CURRENT_VERSION/g" ./target/dist/index.html;
|
sed -i -re "s/\%version\%/$CURRENT_VERSION/g" ./target/dist/index.html;
|
||||||
sed -i -re "s/\%buildDate\%/$BUILD_DATE/g" ./target/dist/index.html;
|
sed -i -re "s/\%buildDate\%/$BUILD_DATE/g" ./target/dist/index.html;
|
||||||
|
|
10
frontend/scripts/compile.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import ppt from "pretty-time";
|
||||||
|
import log from "fancy-log";
|
||||||
|
import * as h from "./_helpers.js";
|
||||||
|
|
||||||
|
await h.compileStyles();
|
||||||
|
await h.copyAssets()
|
||||||
|
await h.compileSvgSprites()
|
||||||
|
await h.compileTemplates();
|
||||||
|
await h.compilePolyfills();
|
|
@ -1,62 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# This script automates compressing PNG images using the lossless Zopfli
|
|
||||||
# Compression Algorithm. The process is slow but can produce significantly
|
|
||||||
# better compression and, thus, smaller file sizes.
|
|
||||||
#
|
|
||||||
# This script is meant to be run manually, for example, before making a new
|
|
||||||
# release.
|
|
||||||
#
|
|
||||||
# Requirements
|
|
||||||
#
|
|
||||||
# zopflipng - https://github.com/google/zopfli
|
|
||||||
# Debian/Ubuntu: sudo apt install zopfli
|
|
||||||
# Fedora: sudo dnf install zopfli
|
|
||||||
# macOS: brew install zopfli
|
|
||||||
#
|
|
||||||
# Usage
|
|
||||||
#
|
|
||||||
# This script takes a single positional argument which is the path where to
|
|
||||||
# search for PNG files. By default, the target path is the current working
|
|
||||||
# directory. Run from the root of the repository to compress all PNG images. Run
|
|
||||||
# from the `frontend` subdirectory to compress all PNG images within that
|
|
||||||
# directory. Alternatively, run from any directory and pass an explicit path to
|
|
||||||
# `compress-png` to limit the script to that path/directory.
|
|
||||||
|
|
||||||
set -o errexit
|
|
||||||
set -o nounset
|
|
||||||
set -o pipefail
|
|
||||||
|
|
||||||
readonly TARGET="${1:-.}"
|
|
||||||
readonly ABS_TARGET="$(command -v realpath &>/dev/null && realpath "$TARGET")"
|
|
||||||
|
|
||||||
function png_total_size() {
|
|
||||||
find "$TARGET" -type f -iname '*.png' -exec du -ch {} + | tail -1
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "Compressing PNGs in ${ABS_TARGET:-$TARGET}"
|
|
||||||
|
|
||||||
echo "Before"
|
|
||||||
png_total_size
|
|
||||||
|
|
||||||
readonly opts=(
|
|
||||||
# More iterations means slower, potentially better compression.
|
|
||||||
#--iterations=500
|
|
||||||
-m
|
|
||||||
# Try all filter strategies (slow).
|
|
||||||
#--filters=01234mepb
|
|
||||||
# According to docs, remove colors behind alpha channel 0. No visual
|
|
||||||
# difference, removes hidden information.
|
|
||||||
--lossy_transparent
|
|
||||||
# Avoid information loss that could affect how images are rendered, see
|
|
||||||
# https://github.com/penpot/penpot/issues/1533#issuecomment-1030005203
|
|
||||||
# https://github.com/google/zopfli/issues/113
|
|
||||||
--keepchunks=cHRM,gAMA,pHYs,iCCP,sRGB,oFFs,sTER
|
|
||||||
# Since we have git behind our back, overwrite PNG files in-place (only
|
|
||||||
# when result is smaller).
|
|
||||||
-y
|
|
||||||
)
|
|
||||||
time find "$TARGET" -type f -iname '*.png' -exec zopflipng "${opts[@]}" {} {} \;
|
|
||||||
|
|
||||||
echo "After"
|
|
||||||
png_total_size
|
|
|
@ -1,11 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# A repl useful for debug macros.
|
|
||||||
|
|
||||||
export OPTIONS="\
|
|
||||||
-J-XX:-OmitStackTraceInFastThrow \
|
|
||||||
-J-Xms50m -J-Xmx512m \
|
|
||||||
-M:dev:jvm-repl";
|
|
||||||
|
|
||||||
set -ex;
|
|
||||||
exec clojure $OPTIONS;
|
|
74
frontend/scripts/watch.js
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import proc from "node:child_process";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import ph from "node:path";
|
||||||
|
|
||||||
|
import log from "fancy-log";
|
||||||
|
import * as h from "./_helpers.js";
|
||||||
|
import ppt from "pretty-time";
|
||||||
|
|
||||||
|
const worker = h.startWorker();
|
||||||
|
let sass = null;
|
||||||
|
|
||||||
|
async function compileSassAll() {
|
||||||
|
const start = process.hrtime();
|
||||||
|
log.info("init: compile styles")
|
||||||
|
|
||||||
|
sass = await h.compileSassAll(worker);
|
||||||
|
let output = await h.concatSass(sass);
|
||||||
|
await fs.writeFile("./resources/public/css/main.css", output);
|
||||||
|
|
||||||
|
const end = process.hrtime(start);
|
||||||
|
log.info("done: compile styles", `(${ppt(end)})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function compileSass(path) {
|
||||||
|
const start = process.hrtime();
|
||||||
|
log.info("changed:", path);
|
||||||
|
const result = await h.compileSass(worker, path, {modules:true});
|
||||||
|
sass.index[result.outputPath] = result.css;
|
||||||
|
|
||||||
|
const output = h.concatSass(sass);
|
||||||
|
|
||||||
|
await fs.writeFile("./resources/public/css/main.css", output);
|
||||||
|
|
||||||
|
const end = process.hrtime(start);
|
||||||
|
log.info("done:", `(${ppt(end)})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await compileSassAll();
|
||||||
|
await h.copyAssets()
|
||||||
|
await h.compileSvgSprites()
|
||||||
|
await h.compileTemplates();
|
||||||
|
await h.compilePolyfills();
|
||||||
|
|
||||||
|
log.info("watch: scss src (~)")
|
||||||
|
|
||||||
|
h.watch("src", h.isSassFile, async function (path) {
|
||||||
|
if (path.includes("common")) {
|
||||||
|
await compileSassAll(path);
|
||||||
|
} else {
|
||||||
|
await compileSass(path);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
log.info("watch: scss: resources (~)")
|
||||||
|
h.watch("resources/styles", h.isSassFile, async function (path) {
|
||||||
|
log.info("changed:", path);
|
||||||
|
await compileSassAll()
|
||||||
|
});
|
||||||
|
|
||||||
|
log.info("watch: templates (~)")
|
||||||
|
h.watch("resources/templates", null, async function (path) {
|
||||||
|
log.info("changed:", path);
|
||||||
|
await h.compileTemplates();
|
||||||
|
});
|
||||||
|
|
||||||
|
log.info("watch: assets (~)")
|
||||||
|
h.watch(["resources/images", "resources/fonts"], null, async function (path) {
|
||||||
|
log.info("changed:", path);
|
||||||
|
await h.compileSvgSprites();
|
||||||
|
await h.copyAssets();
|
||||||
|
await h.compileTemplates();
|
||||||
|
});
|
||||||
|
|
||||||
|
worker.terminate();
|
|
@ -30,7 +30,7 @@
|
||||||
[:a {:href cf/terms-of-service-uri :target "_blank"} (tr "auth.terms-of-service")])
|
[:a {:href cf/terms-of-service-uri :target "_blank"} (tr "auth.terms-of-service")])
|
||||||
|
|
||||||
(when show-all?
|
(when show-all?
|
||||||
[:span (tr "labels.and")])
|
[:span (dm/str " " (tr "labels.and") " ")])
|
||||||
|
|
||||||
(when show-privacy?
|
(when show-privacy?
|
||||||
[:a {:href cf/privacy-policy-uri :target "_blank"} (tr "auth.privacy-policy")])])))
|
[:a {:href cf/privacy-policy-uri :target "_blank"} (tr "auth.privacy-policy")])])))
|
||||||
|
@ -45,11 +45,12 @@
|
||||||
(dom/set-html-title (tr "title.default")))
|
(dom/set-html-title (tr "title.default")))
|
||||||
|
|
||||||
[:main {:class (stl/css :auth-section)}
|
[:main {:class (stl/css :auth-section)}
|
||||||
|
[:a {:href "#/" :class (stl/css :logo-btn)} i/logo]
|
||||||
[:div {:class (stl/css :login-illustration)}
|
[:div {:class (stl/css :login-illustration)}
|
||||||
i/login-illustration]
|
i/login-illustration]
|
||||||
|
|
||||||
[:section {:class (stl/css :auth-content)}
|
[:section {:class (stl/css :auth-content)}
|
||||||
[:a {:href "#/" :class (stl/css :logo-btn)} i/logo]
|
|
||||||
(case section
|
(case section
|
||||||
:auth-register
|
:auth-register
|
||||||
[:& register-page {:params params}]
|
[:& register-page {:params params}]
|
||||||
|
@ -69,6 +70,5 @@
|
||||||
:auth-recovery
|
:auth-recovery
|
||||||
[:& recovery-page {:params params}])
|
[:& recovery-page {:params params}])
|
||||||
|
|
||||||
(when (or (= section :auth-login)
|
(when (= section :auth-register)
|
||||||
(= section :auth-register))
|
|
||||||
[:& terms-login])]]))
|
[:& terms-login])]]))
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
@use "common/refactor/common-refactor.scss" as *;
|
@use "common/refactor/common-refactor.scss" as *;
|
||||||
|
|
||||||
.auth-section {
|
.auth-section {
|
||||||
|
position: relative;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: var(--panel-background-color);
|
background: var(--panel-background-color);
|
||||||
display: grid;
|
display: grid;
|
||||||
|
@ -43,8 +44,9 @@
|
||||||
|
|
||||||
.auth-content {
|
.auth-content {
|
||||||
grid-column: 4 / 6;
|
grid-column: 4 / 6;
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-direction: column;
|
grid-template-rows: 1fr auto;
|
||||||
|
gap: $s-24;
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
max-width: $s-412;
|
max-width: $s-412;
|
||||||
padding-bottom: $s-8;
|
padding-bottom: $s-8;
|
||||||
|
@ -53,6 +55,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-btn {
|
.logo-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: $s-20;
|
||||||
|
left: $s-20;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
margin-bottom: $s-52;
|
margin-bottom: $s-52;
|
||||||
|
@ -68,8 +73,6 @@
|
||||||
|
|
||||||
.terms-login {
|
.terms-login {
|
||||||
font-size: $fs-11;
|
font-size: $fs-11;
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: $s-4;
|
gap: $s-4;
|
||||||
|
@ -77,7 +80,10 @@
|
||||||
|
|
||||||
a {
|
a {
|
||||||
font-weight: $fw700;
|
font-weight: $fw700;
|
||||||
color: $df-secondary;
|
color: $da-primary;
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
span {
|
span {
|
||||||
border-bottom: $s-1 solid transparent;
|
border-bottom: $s-1 solid transparent;
|
||||||
|
|
|
@ -6,36 +6,37 @@
|
||||||
|
|
||||||
@use "common/refactor/common-refactor.scss" as *;
|
@use "common/refactor/common-refactor.scss" as *;
|
||||||
|
|
||||||
.auth-form {
|
.auth-form-wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-block-end: $s-16;
|
padding-block-end: 0;
|
||||||
|
display: grid;
|
||||||
|
gap: $s-24;
|
||||||
form {
|
form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: $s-12;
|
gap: $s-12;
|
||||||
margin-block-end: $s-24;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.separator {
|
.separator {
|
||||||
border-color: $db-quaternary;
|
border-color: var(--modal-separator-backogrund-color);
|
||||||
margin: $s-24 0;
|
margin: 0;
|
||||||
}
|
|
||||||
|
|
||||||
.error-wrapper {
|
|
||||||
padding-block-end: $s-8;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-title {
|
.auth-title {
|
||||||
@include bigTitleTipography;
|
@include bigTitleTipography;
|
||||||
color: $df-primary;
|
color: var(--title-foreground-color-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-subtitle {
|
.auth-subtitle {
|
||||||
margin-top: $s-24;
|
@include smallTitleTipography;
|
||||||
font-size: $fs-14;
|
color: var(--title-foreground-color);
|
||||||
color: $df-secondary;
|
}
|
||||||
|
|
||||||
|
.auth-tagline {
|
||||||
|
@include smallTitleTipography;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--title-foreground-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-field {
|
.form-field {
|
||||||
|
@ -45,77 +46,102 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttons-stack {
|
.buttons-stack {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-direction: column;
|
|
||||||
gap: $s-8;
|
gap: $s-8;
|
||||||
|
|
||||||
button,
|
|
||||||
:global(.btn-primary) {
|
|
||||||
@extend .button-primary;
|
|
||||||
font-size: $fs-11;
|
|
||||||
height: $s-40;
|
|
||||||
text-transform: uppercase;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.link-entry {
|
.login-button,
|
||||||
|
.login-ldap-button {
|
||||||
|
@extend .button-primary;
|
||||||
|
@include uppercaseTitleTipography;
|
||||||
|
height: $s-40;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-account,
|
||||||
|
.go-back {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: $s-12;
|
gap: $s-12;
|
||||||
padding: $s-24 0;
|
padding: 0;
|
||||||
border-top: $s-1 solid $db-quaternary;
|
border-block-start: none;
|
||||||
|
}
|
||||||
|
|
||||||
span {
|
.demo-account-link,
|
||||||
text-align: center;
|
.go-back-link {
|
||||||
font-size: $fs-14;
|
@extend .button-secondary;
|
||||||
color: $df-secondary;
|
@include uppercaseTitleTipography;
|
||||||
}
|
height: $s-40;
|
||||||
a {
|
}
|
||||||
@extend .button-secondary;
|
|
||||||
height: $s-40;
|
.links {
|
||||||
text-transform: uppercase;
|
display: grid;
|
||||||
font-size: $fs-11;
|
gap: $s-24;
|
||||||
}
|
}
|
||||||
&.register a {
|
|
||||||
@extend .button-primary;
|
.register,
|
||||||
|
.account,
|
||||||
|
.recovery-request {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: $s-8;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-text,
|
||||||
|
.account-text,
|
||||||
|
.recovery-text {
|
||||||
|
@include smallTitleTipography;
|
||||||
|
text-align: right;
|
||||||
|
color: var(--title-foreground-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-link,
|
||||||
|
.account-link,
|
||||||
|
.recovery-link,
|
||||||
|
.forgot-pass-link {
|
||||||
|
@include smallTitleTipography;
|
||||||
|
text-align: left;
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
display: inline;
|
||||||
|
color: var(--link-foreground-color);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.forgot-password {
|
.forgot-password {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
a {
|
|
||||||
font-size: $fs-14;
|
|
||||||
color: $df-secondary;
|
|
||||||
font-weight: $fw400;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.submit-btn,
|
.submit-btn,
|
||||||
.register-btn,
|
.register-btn,
|
||||||
.recover-btn {
|
.recover-btn {
|
||||||
@extend .button-primary;
|
@extend .button-primary;
|
||||||
font-size: $fs-11;
|
@include uppercaseTitleTipography;
|
||||||
height: $s-40;
|
height: $s-40;
|
||||||
text-transform: uppercase;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-btn {
|
.login-btn {
|
||||||
border-radius: $br-8;
|
@include smallTitleTipography;
|
||||||
font-size: $fs-14;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: $s-6;
|
gap: $s-6;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
border-radius: $br-8;
|
||||||
|
background-color: var(--button-secondary-background-color-rest);
|
||||||
|
color: var(--button-foreground-color-focus);
|
||||||
span {
|
span {
|
||||||
padding-top: $s-2;
|
padding-block-start: $s-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--app-white);
|
color: var(--button-foreground-color-focus);
|
||||||
|
background-color: var(--button-secondary-background-color-hover);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,39 +149,3 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: $s-8;
|
gap: $s-8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-google-auth {
|
|
||||||
color: var(--google-login-foreground);
|
|
||||||
background-color: var(--google-login-background);
|
|
||||||
&:hover {
|
|
||||||
background: var(--google-login-background-hover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-github-auth {
|
|
||||||
color: var(--github-login-foreground);
|
|
||||||
background: var(--github-login-background);
|
|
||||||
&:hover {
|
|
||||||
background: var(--github-login-background-hover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-oidc-auth {
|
|
||||||
color: var(--oidc-login-foreground);
|
|
||||||
background: var(--oidc-login-background);
|
|
||||||
&:hover {
|
|
||||||
background: var(--oidc-login-background-hover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-gitlab-auth {
|
|
||||||
color: var(--gitlab-login-foreground);
|
|
||||||
background: var(--gitlab-login-background);
|
|
||||||
&:hover {
|
|
||||||
background: var(--gitlab-login-background-hover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner {
|
|
||||||
margin: $s-16 0;
|
|
||||||
}
|
|
||||||
|
|
|
@ -38,10 +38,9 @@
|
||||||
(mf/defc demo-warning
|
(mf/defc demo-warning
|
||||||
{::mf/props :obj}
|
{::mf/props :obj}
|
||||||
[]
|
[]
|
||||||
[:div {:class (stl/css :banner)}
|
[:& context-notification
|
||||||
[:& context-notification
|
{:type :warning
|
||||||
{:type :warning
|
:content (tr "auth.demo-warning")}])
|
||||||
:content (tr "auth.demo-warning")}]])
|
|
||||||
|
|
||||||
(defn- login-with-oidc
|
(defn- login-with-oidc
|
||||||
[event provider params]
|
[event provider params]
|
||||||
|
@ -166,14 +165,15 @@
|
||||||
|
|
||||||
[:*
|
[:*
|
||||||
(when-let [message @error]
|
(when-let [message @error]
|
||||||
[:div {:class (stl/css :error-wrapper)}
|
[:& context-notification
|
||||||
[:& context-notification
|
{:type :warning
|
||||||
{:type :warning
|
:content message
|
||||||
:content message
|
:data-test "login-banner"
|
||||||
:data-test "login-banner"
|
:role "alert"}])
|
||||||
:role "alert"}]])
|
|
||||||
|
|
||||||
[:& fm/form {:on-submit on-submit :form form}
|
[:& fm/form {:on-submit on-submit
|
||||||
|
:class (stl/css :login-form)
|
||||||
|
:form form}
|
||||||
[:div {:class (stl/css :fields-row)}
|
[:div {:class (stl/css :fields-row)}
|
||||||
[:& fm/input
|
[:& fm/input
|
||||||
{:name :email
|
{:name :email
|
||||||
|
@ -193,6 +193,7 @@
|
||||||
(contains? cf/flags :login-with-password)))
|
(contains? cf/flags :login-with-password)))
|
||||||
[:div {:class (stl/css :fields-row :forgot-password)}
|
[:div {:class (stl/css :fields-row :forgot-password)}
|
||||||
[:& lk/link {:action on-recovery-request
|
[:& lk/link {:action on-recovery-request
|
||||||
|
:class (stl/css :forgot-pass-link)
|
||||||
:data-test "forgot-password"}
|
:data-test "forgot-password"}
|
||||||
(tr "auth.forgot-password")]])
|
(tr "auth.forgot-password")]])
|
||||||
|
|
||||||
|
@ -207,6 +208,7 @@
|
||||||
(when (contains? cf/flags :login-with-ldap)
|
(when (contains? cf/flags :login-with-ldap)
|
||||||
[:> fm/submit-button*
|
[:> fm/submit-button*
|
||||||
{:label (tr "auth.login-with-ldap-submit")
|
{:label (tr "auth.login-with-ldap-submit")
|
||||||
|
:class (stl/css :login-ldap-button)
|
||||||
:on-click on-submit-ldap}])]]]))
|
:on-click on-submit-ldap}])]]]))
|
||||||
|
|
||||||
(mf/defc login-buttons
|
(mf/defc login-buttons
|
||||||
|
@ -255,11 +257,11 @@
|
||||||
(when (k/enter? event)
|
(when (k/enter? event)
|
||||||
(login-oidc event))))]
|
(login-oidc event))))]
|
||||||
(when (contains? cf/flags :login-with-oidc)
|
(when (contains? cf/flags :login-with-oidc)
|
||||||
[:div {:class (stl/css :link-entry :link-oidc)}
|
[:button {:tab-index "0"
|
||||||
[:a {:tab-index "0"
|
:class (stl/css :link-entry :link-oidc)
|
||||||
:on-key-down handle-key-down
|
:on-key-down handle-key-down
|
||||||
:on-click login-oidc}
|
:on-click login-oidc}
|
||||||
(tr "auth.login-with-oidc-submit")]])))
|
(tr "auth.login-with-oidc-submit")])))
|
||||||
|
|
||||||
(mf/defc login-methods
|
(mf/defc login-methods
|
||||||
[{:keys [params on-success-callback origin] :as props}]
|
[{:keys [params on-success-callback origin] :as props}]
|
||||||
|
@ -282,35 +284,29 @@
|
||||||
[{:keys [params] :as props}]
|
[{:keys [params] :as props}]
|
||||||
(let [go-register
|
(let [go-register
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
#(st/emit! (rt/nav :auth-register {} params)))
|
#(st/emit! (rt/nav :auth-register {} params)))]
|
||||||
|
|
||||||
on-create-demo-profile
|
[:div {:class (stl/css :auth-form-wrapper)}
|
||||||
(mf/use-fn
|
|
||||||
#(st/emit! (du/create-demo-profile)))]
|
|
||||||
|
|
||||||
[:div {:class (stl/css :auth-form)}
|
|
||||||
[:h1 {:class (stl/css :auth-title)
|
[:h1 {:class (stl/css :auth-title)
|
||||||
:data-test "login-title"} (tr "auth.login-title")]
|
:data-test "login-title"} (tr "auth.login-account-title")]
|
||||||
|
|
||||||
|
[:p {:class (stl/css :auth-tagline)}
|
||||||
|
(tr "auth.login-tagline")]
|
||||||
|
|
||||||
(when (contains? cf/flags :demo-warning)
|
(when (contains? cf/flags :demo-warning)
|
||||||
[:& demo-warning])
|
[:& demo-warning])
|
||||||
|
|
||||||
[:hr {:class (stl/css :separator)}]
|
|
||||||
|
|
||||||
[:& login-methods {:params params}]
|
[:& login-methods {:params params}]
|
||||||
|
|
||||||
|
[:hr {:class (stl/css :separator)}]
|
||||||
|
|
||||||
[:div {:class (stl/css :links)}
|
[:div {:class (stl/css :links)}
|
||||||
(when (contains? cf/flags :registration)
|
(when (contains? cf/flags :registration)
|
||||||
[:div {:class (stl/css :link-entry :register)}
|
[:div {:class (stl/css :register)}
|
||||||
[:span (tr "auth.register") " "]
|
[:span {:class (stl/css :register-text)}
|
||||||
|
(tr "auth.register") " "]
|
||||||
[:& lk/link {:action go-register
|
[:& lk/link {:action go-register
|
||||||
|
:class (stl/css :register-link)
|
||||||
:data-test "register-submit"}
|
:data-test "register-submit"}
|
||||||
(tr "auth.register-submit")]])]
|
(tr "auth.register-submit")]])]]))
|
||||||
|
|
||||||
(when (contains? cf/flags :demo-users)
|
|
||||||
[:div {:class (stl/css :link-entry :demo-account)}
|
|
||||||
[:span (tr "auth.create-demo-profile") " "]
|
|
||||||
[:& lk/link {:action on-create-demo-profile
|
|
||||||
:data-test "demo-account-link"}
|
|
||||||
(tr "auth.create-demo-account")]])]))
|
|
||||||
|
|
||||||
|
|
|
@ -61,7 +61,9 @@
|
||||||
(fm/validate-not-empty :password-1 (tr "auth.password-not-empty"))
|
(fm/validate-not-empty :password-1 (tr "auth.password-not-empty"))
|
||||||
(fm/validate-not-empty :password-2 (tr "auth.password-not-empty"))]
|
(fm/validate-not-empty :password-2 (tr "auth.password-not-empty"))]
|
||||||
:initial params)]
|
:initial params)]
|
||||||
[:& fm/form {:on-submit on-submit :form form}
|
[:& fm/form {:on-submit on-submit
|
||||||
|
:class (stl/css :recovery-form)
|
||||||
|
:form form}
|
||||||
[:div {:class (stl/css :fields-row)}
|
[:div {:class (stl/css :fields-row)}
|
||||||
[:& fm/input {:type "password"
|
[:& fm/input {:type "password"
|
||||||
:name :password-1
|
:name :password-1
|
||||||
|
@ -84,13 +86,14 @@
|
||||||
|
|
||||||
(mf/defc recovery-page
|
(mf/defc recovery-page
|
||||||
[{:keys [params] :as props}]
|
[{:keys [params] :as props}]
|
||||||
[:div {:class (stl/css :auth-form)}
|
[:div {:class (stl/css :auth-form-wrapper)}
|
||||||
[:h1 {:class (stl/css :auth-title)} "Forgot your password?"]
|
[:h1 {:class (stl/css :auth-title)} "Forgot your password?"]
|
||||||
[:div {:class (stl/css :auth-subtitle)} "Please enter your new password"]
|
[:div {:class (stl/css :auth-subtitle)} "Please enter your new password"]
|
||||||
[:hr {:class (stl/css :separator)}]
|
[:hr {:class (stl/css :separator)}]
|
||||||
[:& recovery-form {:params params}]
|
[:& recovery-form {:params params}]
|
||||||
|
|
||||||
[:div {:class (stl/css :links)}
|
[:div {:class (stl/css :links)}
|
||||||
[:div {:class (stl/css :link-entry)}
|
[:div {:class (stl/css :go-back)}
|
||||||
[:a {:on-click #(st/emit! (rt/nav :auth-login))}
|
[:a {:on-click #(st/emit! (rt/nav :auth-login))
|
||||||
|
:class (stl/css :go-back-link)}
|
||||||
(tr "profile.recovery.go-to-login")]]]])
|
(tr "profile.recovery.go-to-login")]]]])
|
||||||
|
|
|
@ -76,6 +76,7 @@
|
||||||
(st/emit! (du/request-profile-recovery params)))))]
|
(st/emit! (du/request-profile-recovery params)))))]
|
||||||
|
|
||||||
[:& fm/form {:on-submit on-submit
|
[:& fm/form {:on-submit on-submit
|
||||||
|
:class (stl/css :recovery-request-form)
|
||||||
:form form}
|
:form form}
|
||||||
[:div {:class (stl/css :fields-row)}
|
[:div {:class (stl/css :fields-row)}
|
||||||
[:& fm/input {:name :email
|
[:& fm/input {:name :email
|
||||||
|
@ -95,14 +96,15 @@
|
||||||
[{:keys [params on-success-callback go-back-callback] :as props}]
|
[{:keys [params on-success-callback go-back-callback] :as props}]
|
||||||
(let [default-go-back #(st/emit! (rt/nav :auth-login))
|
(let [default-go-back #(st/emit! (rt/nav :auth-login))
|
||||||
go-back (or go-back-callback default-go-back)]
|
go-back (or go-back-callback default-go-back)]
|
||||||
[:div {:class (stl/css :auth-form)}
|
[:div {:class (stl/css :auth-form-wrapper)}
|
||||||
[:h1 {:class (stl/css :auth-title)} (tr "auth.recovery-request-title")]
|
[:h1 {:class (stl/css :auth-title)} (tr "auth.recovery-request-title")]
|
||||||
[:div {:class (stl/css :auth-subtitle)} (tr "auth.recovery-request-subtitle")]
|
[:div {:class (stl/css :auth-subtitle)} (tr "auth.recovery-request-subtitle")]
|
||||||
[:hr {:class (stl/css :separator)}]
|
[:hr {:class (stl/css :separator)}]
|
||||||
|
|
||||||
[:& recovery-form {:params params :on-success-callback on-success-callback}]
|
[:& recovery-form {:params params :on-success-callback on-success-callback}]
|
||||||
|
[:hr {:class (stl/css :separator)}]
|
||||||
[:div {:class (stl/css :link-entry)}
|
[:div {:class (stl/css :go-back)}
|
||||||
[:& lk/link {:action go-back
|
[:& lk/link {:action go-back
|
||||||
|
:class (stl/css :go-back-link)
|
||||||
:data-test "go-back-link"}
|
:data-test "go-back-link"}
|
||||||
(tr "labels.go-back")]]]))
|
(tr "labels.go-back")]]]))
|
||||||
|
|
|
@ -132,19 +132,18 @@
|
||||||
[{:keys [params on-success-callback]}]
|
[{:keys [params on-success-callback]}]
|
||||||
[:*
|
[:*
|
||||||
(when login/show-alt-login-buttons?
|
(when login/show-alt-login-buttons?
|
||||||
[:*
|
[:& login/login-buttons {:params params}])
|
||||||
[:hr {:class (stl/css :separator)}]
|
|
||||||
[:& login/login-buttons {:params params}]])
|
|
||||||
[:hr {:class (stl/css :separator)}]
|
[:hr {:class (stl/css :separator)}]
|
||||||
[:& register-form {:params params :on-success-callback on-success-callback}]])
|
[:& register-form {:params params :on-success-callback on-success-callback}]])
|
||||||
|
|
||||||
(mf/defc register-page
|
(mf/defc register-page
|
||||||
{::mf/props :obj}
|
{::mf/props :obj}
|
||||||
[{:keys [params]}]
|
[{:keys [params]}]
|
||||||
[:div {:class (stl/css :auth-form)}
|
[:div {:class (stl/css :auth-form-wrapper)}
|
||||||
[:h1 {:class (stl/css :auth-title)
|
[:h1 {:class (stl/css :auth-title)
|
||||||
:data-test "registration-title"} (tr "auth.register-title")]
|
:data-test "registration-title"} (tr "auth.register-title")]
|
||||||
[:div {:class (stl/css :auth-subtitle)} (tr "auth.register-subtitle")]
|
[:p {:class (stl/css :auth-tagline)}
|
||||||
|
(tr "auth.login-tagline")]
|
||||||
|
|
||||||
(when (contains? cf/flags :demo-warning)
|
(when (contains? cf/flags :demo-warning)
|
||||||
[:& login/demo-warning])
|
[:& login/demo-warning])
|
||||||
|
@ -152,18 +151,20 @@
|
||||||
[:& register-methods {:params params}]
|
[:& register-methods {:params params}]
|
||||||
|
|
||||||
[:div {:class (stl/css :links)}
|
[:div {:class (stl/css :links)}
|
||||||
[:div {:class (stl/css :link-entry :account)}
|
[:div {:class (stl/css :account)}
|
||||||
[:span (tr "auth.already-have-account") " "]
|
[:span {:class (stl/css :account-text)} (tr "auth.already-have-account") " "]
|
||||||
|
|
||||||
[:& lk/link {:action #(st/emit! (rt/nav :auth-login {} params))
|
[:& lk/link {:action #(st/emit! (rt/nav :auth-login {} params))
|
||||||
|
:class (stl/css :account-link)
|
||||||
:data-test "login-here-link"}
|
:data-test "login-here-link"}
|
||||||
(tr "auth.login-here")]]
|
(tr "auth.login-here")]]
|
||||||
|
|
||||||
(when (contains? cf/flags :demo-users)
|
(when (contains? cf/flags :demo-users)
|
||||||
[:div {:class (stl/css :link-entry :demo-users)}
|
[:*
|
||||||
[:span (tr "auth.create-demo-profile") " "]
|
[:hr {:class (stl/css :separator)}]
|
||||||
[:& lk/link {:action #(st/emit! (du/create-demo-profile))}
|
[:div {:class (stl/css :demo-account)}
|
||||||
(tr "auth.create-demo-account")]])]])
|
[:& lk/link {:action #(st/emit! (du/create-demo-profile))
|
||||||
|
:class (stl/css :demo-account-link)}
|
||||||
|
(tr "auth.create-demo-account")]]])]])
|
||||||
|
|
||||||
;; --- PAGE: register validation
|
;; --- PAGE: register validation
|
||||||
|
|
||||||
|
@ -228,7 +229,8 @@
|
||||||
(rx/subs! on-success
|
(rx/subs! on-success
|
||||||
(partial handle-register-error form))))))]
|
(partial handle-register-error form))))))]
|
||||||
|
|
||||||
[:& fm/form {:on-submit on-submit :form form}
|
[:& fm/form {:on-submit on-submit :form form
|
||||||
|
:class (stl/css :register-validate-form)}
|
||||||
[:div {:class (stl/css :fields-row)}
|
[:div {:class (stl/css :fields-row)}
|
||||||
[:& fm/input {:name :fullname
|
[:& fm/input {:name :fullname
|
||||||
:label (tr "auth.fullname")
|
:label (tr "auth.fullname")
|
||||||
|
@ -258,7 +260,7 @@
|
||||||
|
|
||||||
(mf/defc register-validate-page
|
(mf/defc register-validate-page
|
||||||
[{:keys [params]}]
|
[{:keys [params]}]
|
||||||
[:div {:class (stl/css :auth-form)}
|
[:div {:class (stl/css :auth-form-wrapper)}
|
||||||
[:h1 {:class (stl/css :auth-title)
|
[:h1 {:class (stl/css :auth-title)
|
||||||
:data-test "register-title"} (tr "auth.register-title")]
|
:data-test "register-title"} (tr "auth.register-title")]
|
||||||
[:div {:class (stl/css :auth-subtitle)} (tr "auth.register-subtitle")]
|
[:div {:class (stl/css :auth-subtitle)} (tr "auth.register-subtitle")]
|
||||||
|
@ -268,13 +270,14 @@
|
||||||
[:& register-validate-form {:params params}]
|
[:& register-validate-form {:params params}]
|
||||||
|
|
||||||
[:div {:class (stl/css :links)}
|
[:div {:class (stl/css :links)}
|
||||||
[:div {:class (stl/css :link-entry :go-back)}
|
[:div {:class (stl/css :go-back)}
|
||||||
[:& lk/link {:action #(st/emit! (rt/nav :auth-register {} {}))}
|
[:& lk/link {:action #(st/emit! (rt/nav :auth-register {} {}))
|
||||||
|
:class (stl/css :go-back-link)}
|
||||||
(tr "labels.go-back")]]]])
|
(tr "labels.go-back")]]]])
|
||||||
|
|
||||||
(mf/defc register-success-page
|
(mf/defc register-success-page
|
||||||
[{:keys [params]}]
|
[{:keys [params]}]
|
||||||
[:div {:class (stl/css :auth-form :register-success)}
|
[:div {:class (stl/css :auth-form-wrapper :register-success)}
|
||||||
[:div {:class (stl/css :notification-icon)} i/icon-verify]
|
[:div {:class (stl/css :notification-icon)} i/icon-verify]
|
||||||
[:div {:class (stl/css :notification-text)} (tr "auth.verification-email-sent")]
|
[:div {:class (stl/css :notification-text)} (tr "auth.verification-email-sent")]
|
||||||
[:div {:class (stl/css :notification-text-email)} (:email params "")]
|
[:div {:class (stl/css :notification-text-email)} (:email params "")]
|
||||||
|
|
|
@ -50,7 +50,6 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--modal-title-foreground-color);
|
color: var(--modal-title-foreground-color);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
margin-bottom: $s-8;
|
|
||||||
input {
|
input {
|
||||||
@extend .input-element;
|
@extend .input-element;
|
||||||
color: var(--input-foreground-color-active);
|
color: var(--input-foreground-color-active);
|
||||||
|
@ -144,8 +143,9 @@
|
||||||
|
|
||||||
.hint {
|
.hint {
|
||||||
@include bodySmallTypography;
|
@include bodySmallTypography;
|
||||||
color: var(--modal-text-foreground-color);
|
|
||||||
width: 99%;
|
width: 99%;
|
||||||
|
margin-block-start: $s-8;
|
||||||
|
color: var(--modal-text-foreground-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox {
|
.checkbox {
|
||||||
|
|
|
@ -37,6 +37,7 @@
|
||||||
|
|
||||||
.group-name-input {
|
.group-name-input {
|
||||||
@extend .input-element-label;
|
@extend .input-element-label;
|
||||||
|
margin-bottom: $s-8;
|
||||||
label {
|
label {
|
||||||
@include flexColumn;
|
@include flexColumn;
|
||||||
@include bodySmallTypography;
|
@include bodySmallTypography;
|
||||||
|
|
|
@ -127,6 +127,7 @@
|
||||||
height: $s-32;
|
height: $s-32;
|
||||||
width: calc(100% - $s-24);
|
width: calc(100% - $s-24);
|
||||||
margin-inline-start: $s-24;
|
margin-inline-start: $s-24;
|
||||||
|
margin-block-end: $s-8;
|
||||||
}
|
}
|
||||||
|
|
||||||
// STEP-4
|
// STEP-4
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
|
[app.common.types.component :as ctk]
|
||||||
[app.main.data.viewer :as dv]
|
[app.main.data.viewer :as dv]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.main.ui.workspace.sidebar.layer-item :refer [layer-item-inner]]
|
[app.main.ui.workspace.sidebar.layer-item :refer [layer-item-inner]]
|
||||||
|
@ -30,7 +31,7 @@
|
||||||
item-ref (mf/use-ref nil)
|
item-ref (mf/use-ref nil)
|
||||||
depth (+ depth 1)
|
depth (+ depth 1)
|
||||||
|
|
||||||
component-tree? (or component-child? (:component-root item))
|
component-tree? (or component-child? (ctk/instance-root? item) (ctk/instance-head? item))
|
||||||
|
|
||||||
collapsed-iref
|
collapsed-iref
|
||||||
(mf/use-memo
|
(mf/use-memo
|
||||||
|
|
|
@ -78,19 +78,21 @@
|
||||||
:on-click close} i/close]]
|
:on-click close} i/close]]
|
||||||
|
|
||||||
[:div {:class (stl/css :modal-content)}
|
[:div {:class (stl/css :modal-content)}
|
||||||
|
|
||||||
(case current-section
|
(case current-section
|
||||||
:login
|
:login
|
||||||
[:div {:class (stl/css :form-container)}
|
[:div {:class (stl/css :form-container)}
|
||||||
[:& login-methods {:on-success-callback success-login :origin :viewer}]
|
[:& login-methods {:on-success-callback success-login :origin :viewer}]
|
||||||
[:div {:class (stl/css :links)}
|
[:div {:class (stl/css :links)}
|
||||||
[:div {:class (stl/css :link-entry)}
|
[:div {:class (stl/css :recovery-request)}
|
||||||
[:a {:on-click set-section
|
[:a {:on-click set-section
|
||||||
|
:class (stl/css :recovery-link)
|
||||||
:data-value "recovery-request"}
|
:data-value "recovery-request"}
|
||||||
(tr "auth.forgot-password")]]
|
(tr "auth.forgot-password")]]
|
||||||
[:div {:class (stl/css :link-entry)}
|
[:div {:class (stl/css :register)}
|
||||||
[:span (tr "auth.register") " "]
|
[:span {:class (stl/css :register-text)}
|
||||||
|
(tr "auth.register") " "]
|
||||||
[:a {:on-click set-section
|
[:a {:on-click set-section
|
||||||
|
:class (stl/css :register-link)
|
||||||
:data-value "register"}
|
:data-value "register"}
|
||||||
(tr "auth.register-submit")]]]]
|
(tr "auth.register-submit")]]]]
|
||||||
|
|
||||||
|
@ -98,7 +100,7 @@
|
||||||
[:div {:class (stl/css :form-container)}
|
[:div {:class (stl/css :form-container)}
|
||||||
[:& register-methods {:on-success-callback success-register}]
|
[:& register-methods {:on-success-callback success-register}]
|
||||||
[:div {:class (stl/css :links)}
|
[:div {:class (stl/css :links)}
|
||||||
[:div {:class (stl/css :link-entry)}
|
[:div {:class (stl/css :account)}
|
||||||
[:span (tr "auth.already-have-account") " "]
|
[:span (tr "auth.already-have-account") " "]
|
||||||
[:a {:on-click set-section
|
[:a {:on-click set-section
|
||||||
:data-value "login"}
|
:data-value "login"}
|
||||||
|
@ -109,7 +111,7 @@
|
||||||
[:& register-validate-form {:params {:token @register-token}
|
[:& register-validate-form {:params {:token @register-token}
|
||||||
:on-success-callback success-email-sent}]
|
:on-success-callback success-email-sent}]
|
||||||
[:div {:class (stl/css :links)}
|
[:div {:class (stl/css :links)}
|
||||||
[:div {:class (stl/css :link-entry)}
|
[:div {:class (stl/css :register)}
|
||||||
[:a {:on-click set-section
|
[:a {:on-click set-section
|
||||||
:data-value "register"}
|
:data-value "register"}
|
||||||
(tr "labels.go-back")]]]]
|
(tr "labels.go-back")]]]]
|
||||||
|
|
|
@ -49,6 +49,7 @@
|
||||||
}
|
}
|
||||||
.input-wrapper {
|
.input-wrapper {
|
||||||
@extend .input-with-label;
|
@extend .input-with-label;
|
||||||
|
margin-bottom: $s-8;
|
||||||
}
|
}
|
||||||
.action-buttons {
|
.action-buttons {
|
||||||
@extend .modal-action-btns;
|
@extend .modal-action-btns;
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
[app.common.files.helpers :as cfh]
|
[app.common.files.helpers :as cfh]
|
||||||
|
[app.common.types.component :as ctk]
|
||||||
[app.common.types.container :as ctn]
|
[app.common.types.container :as ctn]
|
||||||
[app.common.types.shape.layout :as ctl]
|
[app.common.types.shape.layout :as ctl]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
|
@ -61,7 +62,7 @@
|
||||||
:class (stl/css-case
|
:class (stl/css-case
|
||||||
:layer-row true
|
:layer-row true
|
||||||
:highlight highlighted?
|
:highlight highlighted?
|
||||||
:component (some? (:component-id item))
|
:component (ctk/instance-head? item)
|
||||||
:masked (:masked-group item)
|
:masked (:masked-group item)
|
||||||
:selected selected?
|
:selected selected?
|
||||||
:type-frame (cfh/frame-shape? item)
|
:type-frame (cfh/frame-shape? item)
|
||||||
|
@ -321,7 +322,7 @@
|
||||||
|
|
||||||
ref (mf/use-ref)
|
ref (mf/use-ref)
|
||||||
depth (+ depth 1)
|
depth (+ depth 1)
|
||||||
component-tree? (or component-child? (:component-root item))
|
component-tree? (or component-child? (ctk/instance-root? item) (ctk/instance-head? item))
|
||||||
|
|
||||||
enable-drag (mf/use-fn #(reset! drag-disabled* false))
|
enable-drag (mf/use-fn #(reset! drag-disabled* false))
|
||||||
disable-drag (mf/use-fn #(reset! drag-disabled* true))]
|
disable-drag (mf/use-fn #(reset! drag-disabled* true))]
|
||||||
|
|
|
@ -58,8 +58,12 @@ msgid "auth.login-submit"
|
||||||
msgstr "Login"
|
msgstr "Login"
|
||||||
|
|
||||||
#: src/app/main/ui/auth/login.cljs
|
#: src/app/main/ui/auth/login.cljs
|
||||||
msgid "auth.login-title"
|
msgid "auth.login-account-title"
|
||||||
msgstr "Great to see you again!"
|
msgstr "Log into my account"
|
||||||
|
|
||||||
|
#: src/app/main/ui/auth/login.cljs
|
||||||
|
msgid "auth.login-tagline"
|
||||||
|
msgstr "Penpot is the free open-source design tool for Design and Code collaboration"
|
||||||
|
|
||||||
#: src/app/main/ui/auth/login.cljs
|
#: src/app/main/ui/auth/login.cljs
|
||||||
msgid "auth.login-with-github-submit"
|
msgid "auth.login-with-github-submit"
|
||||||
|
|
|
@ -61,8 +61,12 @@ msgid "auth.login-submit"
|
||||||
msgstr "Entrar"
|
msgstr "Entrar"
|
||||||
|
|
||||||
#: src/app/main/ui/auth/login.cljs
|
#: src/app/main/ui/auth/login.cljs
|
||||||
msgid "auth.login-title"
|
msgid "auth.login-account-title"
|
||||||
msgstr "¡Un placer verte de nuevo!"
|
msgstr "Entrar en mi cuenta"
|
||||||
|
|
||||||
|
#: src/app/main/ui/auth/login.cljs
|
||||||
|
msgid "auth.login-tagline"
|
||||||
|
msgstr "Penpot es la herramienta de diseño libre y open-source para la colaboración entre Diseño y Código"
|
||||||
|
|
||||||
#: src/app/main/ui/auth/login.cljs
|
#: src/app/main/ui/auth/login.cljs
|
||||||
msgid "auth.login-with-github-submit"
|
msgid "auth.login-with-github-submit"
|
||||||
|
|
|
@ -1471,6 +1471,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@bufbuild/protobuf@npm:^1.0.0":
|
||||||
|
version: 1.7.2
|
||||||
|
resolution: "@bufbuild/protobuf@npm:1.7.2"
|
||||||
|
checksum: 37a968b7d314c1f2e2b996bb287c72dbeaacd5bc0d92e2f706437a51c4e483ff85b97994428e252d6acf99bd7b16435471413ae3af1bd9b416d72ab3f0decd22
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@colors/colors@npm:1.5.0":
|
"@colors/colors@npm:1.5.0":
|
||||||
version: 1.5.0
|
version: 1.5.0
|
||||||
resolution: "@colors/colors@npm:1.5.0"
|
resolution: "@colors/colors@npm:1.5.0"
|
||||||
|
@ -5333,6 +5340,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"buffer-builder@npm:^0.2.0":
|
||||||
|
version: 0.2.0
|
||||||
|
resolution: "buffer-builder@npm:0.2.0"
|
||||||
|
checksum: e50c3a379f4acaea75ade1ee3e8c07ed6d7c5dfc3f98adbcf0159bfe1a4ce8ca1fe3689e861fcdb3fcef0012ebd4345a6112a5b8a1185295452bb66d7b6dc8a1
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"buffer-crc32@npm:~0.2.3":
|
"buffer-crc32@npm:~0.2.3":
|
||||||
version: 0.2.13
|
version: 0.2.13
|
||||||
resolution: "buffer-crc32@npm:0.2.13"
|
resolution: "buffer-crc32@npm:0.2.13"
|
||||||
|
@ -6601,6 +6615,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"dettle@npm:^1.0.1":
|
||||||
|
version: 1.0.1
|
||||||
|
resolution: "dettle@npm:1.0.1"
|
||||||
|
checksum: 116a101aff93b2e1d5e505adbe53c4b898d924bc16f12f5ac629055ed8a8a19c86f916b834b178b7bfb352dd601bbfe01e49ccd56144a5a2f780f4bd374ef112
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"diff-sequences@npm:^29.6.3":
|
"diff-sequences@npm:^29.6.3":
|
||||||
version: 29.6.3
|
version: 29.6.3
|
||||||
resolution: "diff-sequences@npm:29.6.3"
|
resolution: "diff-sequences@npm:29.6.3"
|
||||||
|
@ -7939,13 +7960,16 @@ __metadata:
|
||||||
marked: "npm:^12.0.0"
|
marked: "npm:^12.0.0"
|
||||||
mkdirp: "npm:^3.0.1"
|
mkdirp: "npm:^3.0.1"
|
||||||
mousetrap: "npm:^1.6.5"
|
mousetrap: "npm:^1.6.5"
|
||||||
|
mustache: "npm:^4.2.0"
|
||||||
nodemon: "npm:^3.1.0"
|
nodemon: "npm:^3.1.0"
|
||||||
npm-run-all: "npm:^4.1.5"
|
npm-run-all: "npm:^4.1.5"
|
||||||
opentype.js: "npm:^1.3.4"
|
opentype.js: "npm:^1.3.4"
|
||||||
|
p-limit: "npm:^5.0.0"
|
||||||
postcss: "npm:^8.4.35"
|
postcss: "npm:^8.4.35"
|
||||||
postcss-clean: "npm:^1.2.2"
|
postcss-clean: "npm:^1.2.2"
|
||||||
postcss-modules: "npm:^6.0.0"
|
postcss-modules: "npm:^6.0.0"
|
||||||
prettier: "npm:^3.2.5"
|
prettier: "npm:^3.2.5"
|
||||||
|
pretty-time: "npm:^1.1.0"
|
||||||
prop-types: "npm:^15.8.1"
|
prop-types: "npm:^15.8.1"
|
||||||
randomcolor: "npm:^0.6.2"
|
randomcolor: "npm:^0.6.2"
|
||||||
react: "npm:^18.2.0"
|
react: "npm:^18.2.0"
|
||||||
|
@ -7954,15 +7978,19 @@ __metadata:
|
||||||
rimraf: "npm:^5.0.5"
|
rimraf: "npm:^5.0.5"
|
||||||
rxjs: "npm:8.0.0-alpha.14"
|
rxjs: "npm:8.0.0-alpha.14"
|
||||||
sass: "npm:^1.71.1"
|
sass: "npm:^1.71.1"
|
||||||
|
sass-embedded: "npm:^1.71.1"
|
||||||
sax: "npm:^1.3.0"
|
sax: "npm:^1.3.0"
|
||||||
shadow-cljs: "npm:2.27.4"
|
shadow-cljs: "npm:2.27.4"
|
||||||
source-map-support: "npm:^0.5.21"
|
source-map-support: "npm:^0.5.21"
|
||||||
storybook: "npm:^7.6.17"
|
storybook: "npm:^7.6.17"
|
||||||
|
svg-sprite: "npm:^2.0.2"
|
||||||
tdigest: "npm:^0.1.2"
|
tdigest: "npm:^0.1.2"
|
||||||
typescript: "npm:^5.3.3"
|
typescript: "npm:^5.3.3"
|
||||||
ua-parser-js: "npm:^1.0.37"
|
ua-parser-js: "npm:^1.0.37"
|
||||||
vite: "npm:^5.1.4"
|
vite: "npm:^5.1.4"
|
||||||
vitest: "npm:^1.3.1"
|
vitest: "npm:^1.3.1"
|
||||||
|
watcher: "npm:^2.3.0"
|
||||||
|
workerpool: "npm:^9.1.0"
|
||||||
xregexp: "npm:^5.1.1"
|
xregexp: "npm:^5.1.1"
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
@ -12087,6 +12115,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"pretty-time@npm:^1.1.0":
|
||||||
|
version: 1.1.0
|
||||||
|
resolution: "pretty-time@npm:1.1.0"
|
||||||
|
checksum: ba9d7af19cd43838fb2b147654990949575e400dc2cc24bf71ec4a6c4033a38ba8172b1014b597680c6d4d3c075e94648b2c13a7206c5f0c90b711c7388726f3
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"prettysize@npm:^2.0.0":
|
"prettysize@npm:^2.0.0":
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
resolution: "prettysize@npm:2.0.0"
|
resolution: "prettysize@npm:2.0.0"
|
||||||
|
@ -12122,6 +12157,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"promise-make-naked@npm:^2.1.1":
|
||||||
|
version: 2.1.1
|
||||||
|
resolution: "promise-make-naked@npm:2.1.1"
|
||||||
|
checksum: 97bc0a3eeae59f75e8716d5f511edb4ed7558fa304f93407a7c9de3645a19135abfc87d4bca0b570619d3314fa87db67ea3463c4a5068c4bbe7f8889c6883f1d
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"promise-retry@npm:^2.0.1":
|
"promise-retry@npm:^2.0.1":
|
||||||
version: 2.0.1
|
version: 2.0.1
|
||||||
resolution: "promise-retry@npm:2.0.1"
|
resolution: "promise-retry@npm:2.0.1"
|
||||||
|
@ -13126,7 +13168,7 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"rxjs@npm:^7.8.1":
|
"rxjs@npm:^7.4.0, rxjs@npm:^7.8.1":
|
||||||
version: 7.8.1
|
version: 7.8.1
|
||||||
resolution: "rxjs@npm:7.8.1"
|
resolution: "rxjs@npm:7.8.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -13195,6 +13237,205 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"sass-embedded-android-arm64@npm:1.71.1":
|
||||||
|
version: 1.71.1
|
||||||
|
resolution: "sass-embedded-android-arm64@npm:1.71.1"
|
||||||
|
bin:
|
||||||
|
sass: dart-sass/sass
|
||||||
|
conditions: os=android & cpu=arm64
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"sass-embedded-android-arm@npm:1.71.1":
|
||||||
|
version: 1.71.1
|
||||||
|
resolution: "sass-embedded-android-arm@npm:1.71.1"
|
||||||
|
bin:
|
||||||
|
sass: dart-sass/sass
|
||||||
|
conditions: os=android & cpu=arm
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"sass-embedded-android-ia32@npm:1.71.1":
|
||||||
|
version: 1.71.1
|
||||||
|
resolution: "sass-embedded-android-ia32@npm:1.71.1"
|
||||||
|
bin:
|
||||||
|
sass: dart-sass/sass
|
||||||
|
conditions: os=android & cpu=ia32
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"sass-embedded-android-x64@npm:1.71.1":
|
||||||
|
version: 1.71.1
|
||||||
|
resolution: "sass-embedded-android-x64@npm:1.71.1"
|
||||||
|
bin:
|
||||||
|
sass: dart-sass/sass
|
||||||
|
conditions: os=android & cpu=x64
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"sass-embedded-darwin-arm64@npm:1.71.1":
|
||||||
|
version: 1.71.1
|
||||||
|
resolution: "sass-embedded-darwin-arm64@npm:1.71.1"
|
||||||
|
bin:
|
||||||
|
sass: dart-sass/sass
|
||||||
|
conditions: os=darwin & cpu=arm64
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"sass-embedded-darwin-x64@npm:1.71.1":
|
||||||
|
version: 1.71.1
|
||||||
|
resolution: "sass-embedded-darwin-x64@npm:1.71.1"
|
||||||
|
bin:
|
||||||
|
sass: dart-sass/sass
|
||||||
|
conditions: os=darwin & cpu=x64
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"sass-embedded-linux-arm64@npm:1.71.1":
|
||||||
|
version: 1.71.1
|
||||||
|
resolution: "sass-embedded-linux-arm64@npm:1.71.1"
|
||||||
|
bin:
|
||||||
|
sass: dart-sass/sass
|
||||||
|
conditions: os=linux & cpu=arm64
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"sass-embedded-linux-arm@npm:1.71.1":
|
||||||
|
version: 1.71.1
|
||||||
|
resolution: "sass-embedded-linux-arm@npm:1.71.1"
|
||||||
|
bin:
|
||||||
|
sass: dart-sass/sass
|
||||||
|
conditions: os=linux & cpu=arm
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"sass-embedded-linux-ia32@npm:1.71.1":
|
||||||
|
version: 1.71.1
|
||||||
|
resolution: "sass-embedded-linux-ia32@npm:1.71.1"
|
||||||
|
bin:
|
||||||
|
sass: dart-sass/sass
|
||||||
|
conditions: os=linux & cpu=ia32
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"sass-embedded-linux-musl-arm64@npm:1.71.1":
|
||||||
|
version: 1.71.1
|
||||||
|
resolution: "sass-embedded-linux-musl-arm64@npm:1.71.1"
|
||||||
|
conditions: os=linux & cpu=arm64
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"sass-embedded-linux-musl-arm@npm:1.71.1":
|
||||||
|
version: 1.71.1
|
||||||
|
resolution: "sass-embedded-linux-musl-arm@npm:1.71.1"
|
||||||
|
conditions: os=linux & cpu=arm
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"sass-embedded-linux-musl-ia32@npm:1.71.1":
|
||||||
|
version: 1.71.1
|
||||||
|
resolution: "sass-embedded-linux-musl-ia32@npm:1.71.1"
|
||||||
|
conditions: os=linux & cpu=ia32
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"sass-embedded-linux-musl-x64@npm:1.71.1":
|
||||||
|
version: 1.71.1
|
||||||
|
resolution: "sass-embedded-linux-musl-x64@npm:1.71.1"
|
||||||
|
conditions: os=linux & cpu=x64
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"sass-embedded-linux-x64@npm:1.71.1":
|
||||||
|
version: 1.71.1
|
||||||
|
resolution: "sass-embedded-linux-x64@npm:1.71.1"
|
||||||
|
bin:
|
||||||
|
sass: dart-sass/sass
|
||||||
|
conditions: os=linux & cpu=x64
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"sass-embedded-win32-ia32@npm:1.71.1":
|
||||||
|
version: 1.71.1
|
||||||
|
resolution: "sass-embedded-win32-ia32@npm:1.71.1"
|
||||||
|
bin:
|
||||||
|
sass: dart-sass/sass.bat
|
||||||
|
conditions: os=win32 & cpu=ia32
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"sass-embedded-win32-x64@npm:1.71.1":
|
||||||
|
version: 1.71.1
|
||||||
|
resolution: "sass-embedded-win32-x64@npm:1.71.1"
|
||||||
|
bin:
|
||||||
|
sass: dart-sass/sass.bat
|
||||||
|
conditions: os=win32 & (cpu=arm64 | cpu=x64)
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"sass-embedded@npm:^1.71.1":
|
||||||
|
version: 1.71.1
|
||||||
|
resolution: "sass-embedded@npm:1.71.1"
|
||||||
|
dependencies:
|
||||||
|
"@bufbuild/protobuf": "npm:^1.0.0"
|
||||||
|
buffer-builder: "npm:^0.2.0"
|
||||||
|
immutable: "npm:^4.0.0"
|
||||||
|
rxjs: "npm:^7.4.0"
|
||||||
|
sass-embedded-android-arm: "npm:1.71.1"
|
||||||
|
sass-embedded-android-arm64: "npm:1.71.1"
|
||||||
|
sass-embedded-android-ia32: "npm:1.71.1"
|
||||||
|
sass-embedded-android-x64: "npm:1.71.1"
|
||||||
|
sass-embedded-darwin-arm64: "npm:1.71.1"
|
||||||
|
sass-embedded-darwin-x64: "npm:1.71.1"
|
||||||
|
sass-embedded-linux-arm: "npm:1.71.1"
|
||||||
|
sass-embedded-linux-arm64: "npm:1.71.1"
|
||||||
|
sass-embedded-linux-ia32: "npm:1.71.1"
|
||||||
|
sass-embedded-linux-musl-arm: "npm:1.71.1"
|
||||||
|
sass-embedded-linux-musl-arm64: "npm:1.71.1"
|
||||||
|
sass-embedded-linux-musl-ia32: "npm:1.71.1"
|
||||||
|
sass-embedded-linux-musl-x64: "npm:1.71.1"
|
||||||
|
sass-embedded-linux-x64: "npm:1.71.1"
|
||||||
|
sass-embedded-win32-ia32: "npm:1.71.1"
|
||||||
|
sass-embedded-win32-x64: "npm:1.71.1"
|
||||||
|
supports-color: "npm:^8.1.1"
|
||||||
|
varint: "npm:^6.0.0"
|
||||||
|
dependenciesMeta:
|
||||||
|
sass-embedded-android-arm:
|
||||||
|
optional: true
|
||||||
|
sass-embedded-android-arm64:
|
||||||
|
optional: true
|
||||||
|
sass-embedded-android-ia32:
|
||||||
|
optional: true
|
||||||
|
sass-embedded-android-x64:
|
||||||
|
optional: true
|
||||||
|
sass-embedded-darwin-arm64:
|
||||||
|
optional: true
|
||||||
|
sass-embedded-darwin-x64:
|
||||||
|
optional: true
|
||||||
|
sass-embedded-linux-arm:
|
||||||
|
optional: true
|
||||||
|
sass-embedded-linux-arm64:
|
||||||
|
optional: true
|
||||||
|
sass-embedded-linux-ia32:
|
||||||
|
optional: true
|
||||||
|
sass-embedded-linux-musl-arm:
|
||||||
|
optional: true
|
||||||
|
sass-embedded-linux-musl-arm64:
|
||||||
|
optional: true
|
||||||
|
sass-embedded-linux-musl-ia32:
|
||||||
|
optional: true
|
||||||
|
sass-embedded-linux-musl-x64:
|
||||||
|
optional: true
|
||||||
|
sass-embedded-linux-x64:
|
||||||
|
optional: true
|
||||||
|
sass-embedded-win32-ia32:
|
||||||
|
optional: true
|
||||||
|
sass-embedded-win32-x64:
|
||||||
|
optional: true
|
||||||
|
checksum: 637b00398b92b88db6b6dc8906d1c6e42c6907cd26afbda05ff3cdc19360eb2efeeaa8591c995f14e05aa8a08314bf7af219a4cbe1172a95365ca6b442b799d5
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"sass@npm:^1.71.1":
|
"sass@npm:^1.71.1":
|
||||||
version: 1.71.1
|
version: 1.71.1
|
||||||
resolution: "sass@npm:1.71.1"
|
resolution: "sass@npm:1.71.1"
|
||||||
|
@ -14038,6 +14279,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"stubborn-fs@npm:^1.2.5":
|
||||||
|
version: 1.2.5
|
||||||
|
resolution: "stubborn-fs@npm:1.2.5"
|
||||||
|
checksum: 0676befd9901d4dd4e162700fa0396f11d523998589cd6b61b06d1021db811dc4c1e6713869748c6cfa49d58beb9b6f0dc5b6aca6b075811b949e1602ce1e26f
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"supports-color@npm:^5.3.0, supports-color@npm:^5.4.0, supports-color@npm:^5.5.0":
|
"supports-color@npm:^5.3.0, supports-color@npm:^5.4.0, supports-color@npm:^5.5.0":
|
||||||
version: 5.5.0
|
version: 5.5.0
|
||||||
resolution: "supports-color@npm:5.5.0"
|
resolution: "supports-color@npm:5.5.0"
|
||||||
|
@ -14316,6 +14564,15 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"tiny-readdir@npm:^2.2.0":
|
||||||
|
version: 2.4.0
|
||||||
|
resolution: "tiny-readdir@npm:2.4.0"
|
||||||
|
dependencies:
|
||||||
|
promise-make-naked: "npm:^2.1.1"
|
||||||
|
checksum: 0fd05eb677a9bf25f6ace33ad2eeaeb8555303321e18cd22c7a96391f099c1dd900d745738a1c6ba276540b1dc117f72fbbf60cc47bf1c7a73840745e3ea42f8
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"tinybench@npm:^2.5.1":
|
"tinybench@npm:^2.5.1":
|
||||||
version: 2.5.1
|
version: 2.5.1
|
||||||
resolution: "tinybench@npm:2.5.1"
|
resolution: "tinybench@npm:2.5.1"
|
||||||
|
@ -15071,6 +15328,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"varint@npm:^6.0.0":
|
||||||
|
version: 6.0.0
|
||||||
|
resolution: "varint@npm:6.0.0"
|
||||||
|
checksum: 737fc37088a62ed3bd21466e318d21ca7ac4991d0f25546f518f017703be4ed0f9df1c5559f1dd533dddba4435a1b758fd9230e4772c1a930ef72b42f5c750fd
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"vary@npm:~1.1.2":
|
"vary@npm:~1.1.2":
|
||||||
version: 1.1.2
|
version: 1.1.2
|
||||||
resolution: "vary@npm:1.1.2"
|
resolution: "vary@npm:1.1.2"
|
||||||
|
@ -15311,6 +15575,17 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"watcher@npm:^2.3.0":
|
||||||
|
version: 2.3.0
|
||||||
|
resolution: "watcher@npm:2.3.0"
|
||||||
|
dependencies:
|
||||||
|
dettle: "npm:^1.0.1"
|
||||||
|
stubborn-fs: "npm:^1.2.5"
|
||||||
|
tiny-readdir: "npm:^2.2.0"
|
||||||
|
checksum: 7b1e47321ddf96882ebee6f619211b085f98bc0c3bceb94a58938e8d8d209f83283b30b645bdae148e063c3bc165eeafd73e3a14bdb7c3bfe519bd7536172257
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"watchpack@npm:^2.2.0":
|
"watchpack@npm:^2.2.0":
|
||||||
version: 2.4.0
|
version: 2.4.0
|
||||||
resolution: "watchpack@npm:2.4.0"
|
resolution: "watchpack@npm:2.4.0"
|
||||||
|
@ -15521,6 +15796,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"workerpool@npm:^9.1.0":
|
||||||
|
version: 9.1.0
|
||||||
|
resolution: "workerpool@npm:9.1.0"
|
||||||
|
checksum: 32d0807962be58a98ec22f5630be4a90f779f5faab06d5b4f000d32c11c8d5feb66be9bc5c73fdc49c91519e391db55c9e2e63392854b3df945744b2436a7efd
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0":
|
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0":
|
||||||
version: 7.0.0
|
version: 7.0.0
|
||||||
resolution: "wrap-ansi@npm:7.0.0"
|
resolution: "wrap-ansi@npm:7.0.0"
|
||||||
|
|