minimal sql console API and UI

This commit is contained in:
Gani Georgiev
2026-05-20 16:22:51 +03:00
parent db853850ab
commit b022e138d4
21 changed files with 984 additions and 21 deletions
+1
View File
@@ -48,6 +48,7 @@ func NewRouter(app core.App) (*router.Router[*core.RequestEvent], error) {
bindBatchApi(app, apiGroup) bindBatchApi(app, apiGroup)
bindRealtimeApi(app, apiGroup) bindRealtimeApi(app, apiGroup)
bindHealthApi(app, apiGroup) bindHealthApi(app, apiGroup)
bindSQLApi(app, apiGroup)
// UI routes // UI routes
bindUIExtensions(app) bindUIExtensions(app)
+191
View File
@@ -0,0 +1,191 @@
package apis
import (
"context"
"errors"
"log/slog"
"net/http"
"strings"
"time"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/router"
)
const (
runSQLMaxRows = 1000
runSQLMaxTimeout = 3 * time.Minute
)
// bindSQLApi registers the SQL api endpoints.
func bindSQLApi(app core.App, rg *router.RouterGroup[*core.RequestEvent]) {
subGroup := rg.Group("/sql").Bind(RequireSuperuserAuth())
subGroup.POST("", runSQL)
}
func runSQL(e *core.RequestEvent) error {
// extra precaution in case manually invoked from somewhere else
if !e.HasSuperuserAuth() {
return e.ForbiddenError("", nil)
}
form := runSQLForm{}
err := e.BindBody(&form)
if err != nil {
return firstApiError(err, e.BadRequestError("An error occurred while loading the submitted data.", err))
}
err = form.validate()
if err != nil {
return firstApiError(err, e.BadRequestError("An error occurred while validating the submitted data.", err))
}
result, err := executeQuery(e.App, form.Query, runSQLMaxRows)
if err != nil {
return firstApiError(err, e.BadRequestError("Failed to execute query. Raw error:\n"+err.Error(), nil))
}
return e.JSON(http.StatusOK, result)
}
type runSQLForm struct {
Query string `form:"query" json:"query"`
}
func (form *runSQLForm) validate() error {
return validation.ValidateStruct(form,
validation.Field(&form.Query, validation.Required, validation.Length(0, 3000)),
)
}
type runSQLResultColumn struct {
Name string `json:"name"`
Type string `json:"type"`
Nullable bool `json:"nullable"`
}
type runSQLResult struct {
ExecTime int64 `json:"execTime"`
AffectedRows int64 `json:"affectedRows"`
Columns []runSQLResultColumn `json:"columns"`
Rows [][]any `json:"rows"`
}
var knownWriteQueryPrefixes = []string{"INSERT", "CREATE", "UPDATE", "DELETE", "DROP", "DETACH"}
func executeQuery(app core.App, query string, maxRows int) (*runSQLResult, error) {
query = strings.TrimSpace(query)
if query == "" {
// see https://github.com/mattn/go-sqlite3/issues/950
return nil, errors.New("empty query")
}
var isPossibleWriteQuery bool
// loosely check the query type
ucQuery := strings.ToUpper(query)
if !strings.HasPrefix(ucQuery, "SELECT") {
for _, prefix := range knownWriteQueryPrefixes {
if strings.HasPrefix(ucQuery, prefix) {
isPossibleWriteQuery = true
break
}
}
}
// note: don't extend the request context to minimize the risk of
// causing integrity issues with custom non-transaction mutations
ctx, cancelFunc := context.WithTimeout(context.Background(), runSQLMaxTimeout)
defer cancelFunc()
result := &runSQLResult{
// init empty slices to ensure "[]" serialization
Columns: []runSQLResultColumn{},
Rows: [][]any{},
}
now := time.Now()
defer func() {
result.ExecTime = time.Since(now).Milliseconds()
}()
// assume write/mutation query
// ---------------------------------------------------------------
if isPossibleWriteQuery {
// auto wrap in transaction in case there are multiple inline queries
txErr := app.RunInTransaction(func(txApp core.App) error {
execResult, err := txApp.NonconcurrentDB().NewQuery(query).WithContext(ctx).Execute()
if err != nil {
return err
}
result.AffectedRows, err = execResult.RowsAffected()
if err != nil {
// non-critical error (e.g. not supported by the driver)
txApp.Logger().Debug("Unable to fetch affected rows", slog.String("error", err.Error()))
}
return nil
})
if txErr != nil {
return nil, txErr
}
return result, nil
}
// assume query returning rows
// ---------------------------------------------------------------
rows, err := app.ConcurrentDB().NewQuery(query).WithContext(ctx).Rows()
if err != nil {
return nil, err
}
defer rows.Close()
// populate columns info
// ---
colTypes, err := rows.ColumnTypes()
if err != nil {
return nil, err
}
for _, colType := range colTypes {
col := runSQLResultColumn{
Name: colType.Name(),
Type: colType.DatabaseTypeName(),
}
col.Nullable, _ = colType.Nullable()
result.Columns = append(result.Columns, col)
}
// populate rows
// ---
for rows.Next() {
if len(result.Rows) >= maxRows {
break
}
rowData := make([]any, len(colTypes))
for i := 0; i < len(colTypes); i++ {
var v *string
rowData[i] = &v
}
err := rows.Scan(rowData...)
if err != nil {
return nil, err
}
result.Rows = append(result.Rows, rowData)
}
err = rows.Err()
if err != nil {
return nil, err
}
return result, nil
}
+202
View File
@@ -0,0 +1,202 @@
package apis_test
import (
"net/http"
"strings"
"testing"
"github.com/pocketbase/pocketbase/tests"
)
func TestSQLRun(t *testing.T) {
t.Parallel()
scenarios := []tests.ApiScenario{
{
Name: "guest",
Method: http.MethodPost,
URL: "/api/sql",
Body: strings.NewReader(`{"query":"select 1"}`),
ExpectedStatus: 401,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "regular user",
Method: http.MethodPost,
URL: "/api/sql",
Body: strings.NewReader(`{"query":"select 1"}`),
Headers: map[string]string{
// users, test2@example.com
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6Im9hcDY0MGNvdDR5cnUycyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.GfJo6EHIobgas_AXt-M-tj5IoQendPnrkMSe9ExuSEY",
},
ExpectedStatus: 403,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "superuser",
Method: http.MethodPost,
URL: "/api/sql",
Body: strings.NewReader(`{"query":"select 1"}`),
Headers: map[string]string{
// superusers, test@example.com
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"execTime":`,
`"affectedRows":0`,
`"columns":[{"name":"1","type":"","nullable":true}]`,
`"rows":[["1"]]`,
},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "empty query",
Method: http.MethodPost,
URL: "/api/sql",
Body: strings.NewReader(`{"query":""}`),
Headers: map[string]string{
// superusers, test@example.com
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
},
ExpectedStatus: 400,
ExpectedContent: []string{
`"data":{`,
`"query":{`,
},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "long query",
Method: http.MethodPost,
URL: "/api/sql",
Body: strings.NewReader(`{"query":"` + strings.Repeat("a", 3001) + `"}`),
Headers: map[string]string{
// superusers, test@example.com
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
},
ExpectedStatus: 400,
ExpectedContent: []string{
`"data":{`,
`"query":{`,
},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "invalid query",
Method: http.MethodPost,
URL: "/api/sql",
Body: strings.NewReader(`{"query":"invalid"}`),
Headers: map[string]string{
// superusers, test@example.com
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
},
ExpectedStatus: 400,
ExpectedContent: []string{
`"data":{}`,
`Raw error:`,
`SQL logic error`,
},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "single write query",
Method: http.MethodPost,
URL: "/api/sql",
Body: strings.NewReader(`{"query":"create table test_sql_table(id int primary key)"}`),
Headers: map[string]string{
// superusers, test@example.com
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
},
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
if !app.HasTable("test_sql_table") {
t.Fatalf("Missing expected new %q table", "test_sql_table")
}
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"execTime":`,
`"affectedRows":0`,
`"columns":[]`,
`"rows":[]`,
},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "multiple write queries",
Method: http.MethodPost,
URL: "/api/sql",
Body: strings.NewReader(`{"query":"create table test_sql_table(id int primary key);insert into test_sql_table(id)VALUES(1)"}`),
Headers: map[string]string{
// superusers, test@example.com
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
},
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
var total int
err := app.DB().NewQuery("select count(*) from test_sql_table").Row(&total)
if err != nil {
t.Fatal(err)
}
if total != 1 {
t.Fatalf("Expected exactly 1 row, found: %d", total)
}
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"execTime":`,
`"affectedRows":1`,
`"columns":[]`,
`"rows":[]`,
},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "multiple write queries (transaction rollback)",
Method: http.MethodPost,
URL: "/api/sql",
Body: strings.NewReader(`{"query":"create table test_sql_table(id int primary key);insert into test_sql_table(id)VALUES(1);invalid"}`),
Headers: map[string]string{
// superusers, test@example.com
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
},
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
if app.HasTable("test_sql_table") {
t.Fatalf("Expected table %q to not be created", "test_sql_table")
}
},
ExpectedStatus: 400,
ExpectedContent: []string{
`"data":{}`,
`Raw error:`,
`SQL logic error`,
},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "multiple read queries",
Method: http.MethodPost,
URL: "/api/sql",
Body: strings.NewReader(`{"query":"select 1;select 2"}`),
Headers: map[string]string{
// superusers, test@example.com
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"execTime":`,
`"affectedRows":0`,
// only the result of the last query should be returned
`"columns":[{"name":"2","type":"","nullable":true}]`,
`"rows":[["2"]]`,
},
ExpectedEvents: map[string]int{"*": 0},
},
}
for _, scenario := range scenarios {
scenario.Test(t)
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -13,9 +13,9 @@
<!-- prism --> <!-- prism -->
<script src="./libs/prism/prism.js" data-manual></script> <script src="./libs/prism/prism.js" data-manual></script>
<script type="module" crossorigin src="./assets/index-1u9sxa_M.js"></script> <script type="module" crossorigin src="./assets/index-BRepvmcR.js"></script>
<link rel="modulepreload" crossorigin href="./assets/pocketbase.es-B_4DUNUU.js"> <link rel="modulepreload" crossorigin href="./assets/pocketbase.es-B_4DUNUU.js">
<link rel="stylesheet" crossorigin href="./assets/index-ePwHvxbV.css"> <link rel="stylesheet" crossorigin href="./assets/index-C5MjNoft.css">
</head> </head>
<body> <body>
</body> </body>
+1
View File
@@ -1,3 +1,4 @@
(function(e){let t,n=new Set,r=new Map,i=[],a,o=Symbol(),s=Symbol(),c=Symbol(),l=Symbol(),u=Symbol(),d=Symbol(),f=Symbol(),p=Symbol(),m=Symbol(),h=Symbol();function g(e,n){let d={[o]:`_`+Math.random(),[p]:e,[m]:n};return r.set(d[o],d),d.run=()=>{let e;t&&(e=t,d[s]=t[o],t[c]=t[c]||[],t[c].push(d[o])),d[l]?.forEach(e=>{e.delete(d[o])}),t=d;let n=d[p](d[h],d);d[m]&&(t=null,d[m](n,d[h],d)),d[h]=n,t=e},d.unwatch=function(){d[u]=1,i.push(d[o]),a&&clearTimeout(a),a=setTimeout(()=>{for(let e of i)_(e);i=[],a=null},50)},d.run(),d}function _(e){let t=r.get(e);if(t?.[d]?.(),t?.[c]){for(let e of t[c])_(e);t[s]=null,t[c]=null}if(t?.[l]){for(let n of t[l])n.delete(e);t[l]=null}r.delete(e)}function v(e){return y(e,new Map)}function y(e,n){let r=typeof e==`object`&&!Array.isArray(e)?Object.getOwnPropertyDescriptors(e):{};return new Proxy(e,{get(e,i,a){if(typeof i==`symbol`)return e[i];if(i==`__raw`)return e;if(r[i]?.get&&!e[s]){let n=i;if(i=`@@`+i,!t){let t=r[n].get.call(e);return r[n]._watcher&&(a[i]=t),t}let s=t[o];if(r[n]._refs=r[n]._refs||new Set,!r[n]._refs.has(s)){r[n]._refs.add(s);let e=t[d];t[d]=()=>{e?.(),r[n]._refs.delete(s),!r[n]._refs.size&&r[n]._watcher&&_(r[n]._watcher[o])}}if(!r[n]._watcher){let o=t;t=null;let s=g(r[n].get.bind(e),t=>{e.hasOwnProperty(i)?a[i]=t:Object.defineProperty(e,i,{writable:!0,enumerable:!1,value:t})});r[n]._watcher=s,s[d]=()=>{r[n]._watcher=null},t=o}}let c=e?.[i];if(typeof c==`function`)return c;if(typeof c==`object`&&c&&!c[s]&&(c.constructor?.name==`Object`||c.constructor?.name==`Array`||c.constructor?.name==null)&&(c[s]=[a,i],c=y(c,n),e[i]=c),t){let r=b(e,i),a=t[o],s=[r];t[l]=t[l]||new Set;for(let e of s){let r=n.get(e);r||(r=new Set,n.set(e,r)),r.add(a),t[l].add(r)}}return c},set(e,t,r){if(typeof t==`symbol`)return e[t]=r,!0;let i=e[t];i?.[s]&&(i[f]=!0),r?.[s]&&Array.isArray(e)&&!isNaN(t)&&(r[s][1]=t);let a=!1;return i===void 0&&!e.hasOwnProperty(t)&&(a=!0),e[t]=r,a&&x(e,`toJSON`,n),(r!==i||t===`length`)&&x(e,t,n),!0},deleteProperty(e,t){if(typeof t!=`symbol`){x(e,t,n);let r=b(e,t);for(let e of n)(e[0]==r||e[0].startsWith(r+`/`))&&n.delete(e[0])}return delete e[t]}})}function b(e,t){let n=t,r=e?.[s];for(;r;)n=r[1]+`/`+n,r=r[0][s];return n}function x(e,t,i){let a=b(e,t),o=i.get(a);if(o)for(let e of o)n.delete(e),n.add(e),n.size==1&&queueMicrotask(()=>{let e={},t;for(let i of n)if(t=r.get(i),!(!t||t[u])&&!(t[s]&&n.has(t[s]))){if(e[i]=(e[i]||0)+1,e[i]>250){console.warn(`Possible infinite loop for watcher `+i+`:`,` (function(e){let t,n=new Set,r=new Map,i=[],a,o=Symbol(),s=Symbol(),c=Symbol(),l=Symbol(),u=Symbol(),d=Symbol(),f=Symbol(),p=Symbol(),m=Symbol(),h=Symbol();function g(e,n){let d={[o]:`_`+Math.random(),[p]:e,[m]:n};return r.set(d[o],d),d.run=()=>{let e;t&&(e=t,d[s]=t[o],t[c]=t[c]||[],t[c].push(d[o])),d[l]?.forEach(e=>{e.delete(d[o])}),t=d;let n=d[p](d[h],d);d[m]&&(t=null,d[m](n,d[h],d)),d[h]=n,t=e},d.unwatch=function(){d[u]=1,i.push(d[o]),a&&clearTimeout(a),a=setTimeout(()=>{for(let e of i)_(e);i=[],a=null},50)},d.run(),d}function _(e){let t=r.get(e);if(t?.[d]?.(),t?.[c]){for(let e of t[c])_(e);t[s]=null,t[c]=null}if(t?.[l]){for(let n of t[l])n.delete(e);t[l]=null}r.delete(e)}function v(e){return y(e,new Map)}function y(e,n){let r=typeof e==`object`&&!Array.isArray(e)?Object.getOwnPropertyDescriptors(e):{};return new Proxy(e,{get(e,i,a){if(typeof i==`symbol`)return e[i];if(i==`__raw`)return e;if(r[i]?.get&&!e[s]){let n=i;if(i=`@@`+i,!t){let t=r[n].get.call(e);return r[n]._watcher&&(a[i]=t),t}let s=t[o];if(r[n]._refs=r[n]._refs||new Set,!r[n]._refs.has(s)){r[n]._refs.add(s);let e=t[d];t[d]=()=>{e?.(),r[n]._refs.delete(s),!r[n]._refs.size&&r[n]._watcher&&_(r[n]._watcher[o])}}if(!r[n]._watcher){let o=t;t=null;let s=g(r[n].get.bind(e),t=>{e.hasOwnProperty(i)?a[i]=t:Object.defineProperty(e,i,{writable:!0,enumerable:!1,value:t})});r[n]._watcher=s,s[d]=()=>{r[n]._watcher=null},t=o}}let c=e?.[i];if(typeof c==`function`)return c;if(typeof c==`object`&&c&&!c[s]&&(c.constructor?.name==`Object`||c.constructor?.name==`Array`||c.constructor?.name==null)&&(c[s]=[a,i],c=y(c,n),e[i]=c),t){let r=b(e,i),a=t[o],s=[r];t[l]=t[l]||new Set;for(let e of s){let r=n.get(e);r||(r=new Set,n.set(e,r)),r.add(a),t[l].add(r)}}return c},set(e,t,r){if(typeof t==`symbol`)return e[t]=r,!0;let i=e[t];i?.[s]&&(i[f]=!0),r?.[s]&&Array.isArray(e)&&!isNaN(t)&&(r[s][1]=t);let a=!1;return i===void 0&&!e.hasOwnProperty(t)&&(a=!0),e[t]=r,a&&x(e,`toJSON`,n),(r!==i||t===`length`)&&x(e,t,n),!0},deleteProperty(e,t){if(typeof t!=`symbol`){x(e,t,n);let r=b(e,t);for(let e of n)(e[0]==r||e[0].startsWith(r+`/`))&&n.delete(e[0])}return delete e[t]}})}function b(e,t){let n=t,r=e?.[s];for(;r;)n=r[1]+`/`+n,r=r[0][s];return n}function x(e,t,i){let a=b(e,t),o=i.get(a);if(o)for(let e of o)n.delete(e),n.add(e),n.size==1&&queueMicrotask(()=>{let e={},t;for(let i of n)if(t=r.get(i),!(!t||t[u])&&!(t[s]&&n.has(t[s]))){if(e[i]=(e[i]||0)+1,e[i]>250){console.warn(`Possible infinite loop for watcher `+i+`:`,`
watch(`+t[p]?.toString()+(t[m]?`, `+t[m].toString():``)+`)`);continue}t.run()}n.clear()})}let S=new Proxy({},{get(e,t){return function(){return w(),O.call(void 0,t,...arguments)}}}),C=!1;function w(){if(C)return;C=!0;function e(t,n){for(let r of n)r[t]&&r[t](r),r.childNodes&&e(t,r.childNodes)}new MutationObserver(t=>{for(let n of t)e(`onmount`,n.addedNodes),e(`onunmount`,n.removedNodes)}).observe(document,{childList:!0,subtree:!0})}let T=Symbol(),E=Symbol(),D=Symbol();function O(e,t={},...n){let r=document.createElement(e);if(t)for(let e in t){let n=t[e],i=!1;e.length>5&&e.startsWith(`html-`)&&(i=!0,e=e.substring(5)),n===void 0?r.removeAttribute(e):typeof n!=`function`||e.length>2&&e.startsWith(`on`)?i?r.setAttribute(e,n):r[e]=n:(e==`rid`&&console.warn(`rid is provided as reactive function and will not have effect for `+r+`. watch(`+t[p]?.toString()+(t[m]?`, `+t[m].toString():``)+`)`);continue}t.run()}n.clear()})}let S=new Proxy({},{get(e,t){return function(){return w(),O.call(void 0,t,...arguments)}}}),C=!1;function w(){if(C)return;C=!0;function e(t,n){for(let r of n)r[t]&&r[t](r),r.childNodes&&e(t,r.childNodes)}new MutationObserver(t=>{for(let n of t)e(`onmount`,n.addedNodes),e(`onunmount`,n.removedNodes)}).observe(document,{childList:!0,subtree:!0})}let T=Symbol(),E=Symbol(),D=Symbol();function O(e,t={},...n){let r=document.createElement(e);if(t)for(let e in t){let n=t[e],i=!1;e.length>5&&e.startsWith(`html-`)&&(i=!0,e=e.substring(5)),n===void 0?r.removeAttribute(e):typeof n!=`function`||e.length>2&&e.startsWith(`on`)?i?r.setAttribute(e,n):r[e]=n:(e==`rid`&&console.warn(`rid is provided as reactive function and will not have effect for `+r+`.
Consider using a plain number, string or object/array reference instead.`),r[T]=r[T]||[],r[T].push(()=>{if(!r)return;let t=n(r,e);t===void 0?(r[e]!==void 0&&(r[e]=void 0),r.removeAttribute(e)):i?r.setAttribute(e,t):r[e]=t}))}let i=r.onmount;r.onmount=()=>{if(!r[D]){if(r[D]=!0,r[T]){r[E]=r[E]||[];for(let e of r[T])r[E].push(g(e))}i?.(r)}};let a=r.onunmount;return r.onunmount=()=>{if(r[D]){if(r[D]=!1,r[E])for(let e of r[E])e.unwatch();r[E]=null,a?.(r)}},k(r,n),r}function k(e,t){t=N(t);for(let n of t)if(typeof n==`function`)A(e,n);else{let t=P(n);Array.isArray(t)?k(e,t):t&&e.appendChild(t)}}function A(e,t){let n=document.createComment(``);e.appendChild(n);let r=[],i=new Map,a=e.moveBefore||e.insertBefore;e[T]=e[T]||[],e[T].push(()=>{if(!e)return;let o=N(t(e)),s=o.length,c=new Map;if(!r?.length){let t=document.createDocumentFragment();for(let e=0;e<s;e++){o[e]=P(o[e],!0),t.appendChild(o[e]);let n=o[e].rid;n!==void 0&&(c.has(n)?console.warn(`Duplicated rid:`,n,o[e]):c.set(n,e))}e.insertBefore(t,n),t=null,r=o,i=c;return}let l=[],u=[],d=new Set,f=[];for(let e=0;e<s;e++){o[e]=P(o[e],!0);let t=o[e].rid;if(t!==void 0){c.has(t)?console.warn(`Duplicated rid:`,t,o[e]):c.set(t,e);let n=i.get(t);if(n>=0){d.add(r[n]),o[e]=r[n],f.push(n);continue}}u.push({child:o[e],prev:o[e-1]})}let p=j(f);f.length!=p.length&&f.forEach((e,t)=>{p.has(e)||l.push({child:r[e],targetPos:t})});let m,h,g;for(let t of l)m=r.findIndex(e=>e===t.child),h=t.targetPos,m<t.targetPos&&(h=t.targetPos+1),g=r[h]||n,M(r,m,t.targetPos),a.call(e,t.child,g);for(let e of u)e.prev?e.prev.after(e.child):(r[0]||n).before(e.child);for(let e=0;e<r.length;e++)d.has(r[e])||r[e].remove?.();r=o,i=c,o=null,c=null,d=null,l=null,u=null})}function j(e){let t=[],n=[];for(let r=0;r<e.length;r++){let i=e[r],a=0,o=0,s=t.length;for(;a<s;)o=Math.floor((a+s)/2),e[t[o]]>=i?s=o:a=o+1;a>0&&(n[r]=t[a-1]),t[a]=r}let r=new Set,i=t[t.length-1];for(;i!==void 0;)r.add(e[i]),i=n[i];return r}function M(e,t,n){if(t==n)return e;let r=t>n?-1:1,i=e[t];for(let i=t;i!=n;i+=r)e[i]=e[i+r];e[n]=i}function N(e){return e==null?[]:Array.isArray(e)?e:[e]}function P(e,t=!1){return t&&e==null&&(e=``),typeof e==`string`||typeof e==`number`||typeof e==`boolean`?document.createTextNode(e):e?.__raw===void 0?e:e.__raw}function F(e,t={fallbackPath:`#/`,transition:!0}){let n=L(e),r,i=()=>{let e=window.location.hash,i=I(n,e);if(!i){if(t.fallbackPath!=e){window.location.hash=t.fallbackPath;return}console.warn(`missing route:`,e);return}let a=async()=>{try{await r?.(),r=await i.handler(i)}catch(e){console.warn(`route navigation failed:`,e)}};t.transition&&document.startViewTransition?document.startViewTransition(a):a()};return window.addEventListener(`hashchange`,i),i(),()=>{window.removeEventListener(`hashchange`,i)}}function I(e,t){for(let n of e){let e=t.match(n.regex);if(!e)continue;let r={},i=t.split(`?`)?.[1];if(i){let e=new URLSearchParams(i);for(let[t,n]of e.entries())Array.isArray(r[t])||(r[t]=r[t]?[r[t]]:[]),r[t].push(n)}return Object.assign({path:t,query:r,params:e.groups||{}},n)}}function L(e){let t=[];for(let n in e){let r=n.split(`/`);for(let e in r)r[e].length>2&&r[e].startsWith(`{`)&&r[e].endsWith(`}`)?r[e]=`(?<`+r[e].substring(1,r[e].length-1)+`>[^\\/#?]+)`:r[e]=RegExp.escape(r[e]);t.push({regex:RegExp(`^`+r.join(`\\/`)+`(?:[?#].*)?$`),pattern:n,handler:e[n]})}return t}e.router=F,e.store=v,e.t=S,e.watch=g})(this.window=this.window||{}); Consider using a plain number, string or object/array reference instead.`),r[T]=r[T]||[],r[T].push(()=>{if(!r)return;let t=n(r,e);t===void 0?(r[e]!==void 0&&(r[e]=void 0),r.removeAttribute(e)):i?r.setAttribute(e,t):r[e]=t}))}let i=r.onmount;r.onmount=()=>{if(!r[D]){if(r[D]=!0,r[T]){r[E]=r[E]||[];for(let e of r[T])r[E].push(g(e))}i?.(r)}};let a=r.onunmount;return r.onunmount=()=>{if(r[D]){if(r[D]=!1,r[E])for(let e of r[E])e.unwatch();r[E]=null,a?.(r)}},k(r,n),r}function k(e,t){t=N(t);for(let n of t)if(typeof n==`function`)A(e,n);else{let t=P(n);Array.isArray(t)?k(e,t):t&&e.appendChild(t)}}function A(e,t){let n=document.createComment(``);e.appendChild(n);let r=[],i=new Map,a=e.moveBefore||e.insertBefore;e[T]=e[T]||[],e[T].push(()=>{if(!e)return;let o=N(t(e)),s=o.length,c=new Map;if(!r?.length){let t=document.createDocumentFragment();for(let e=0;e<s;e++){o[e]=P(o[e],!0),t.appendChild(o[e]);let n=o[e].rid;n!==void 0&&(c.has(n)?console.warn(`Duplicated rid:`,n,o[e]):c.set(n,e))}e.insertBefore(t,n),t=null,r=o,i=c;return}let l=[],u=[],d=new Set,f=[];for(let e=0;e<s;e++){o[e]=P(o[e],!0);let t=o[e].rid;if(t!==void 0){c.has(t)?console.warn(`Duplicated rid:`,t,o[e]):c.set(t,e);let n=i.get(t);if(n>=0){d.add(r[n]),o[e]=r[n],f.push(n);continue}}u.push({child:o[e],prev:o[e-1]})}let p=j(f);f.length!=p.length&&f.forEach((e,t)=>{p.has(e)||l.push({child:r[e],targetPos:t})});let m,h,g;for(let t of l)m=r.findIndex(e=>e===t.child),h=t.targetPos,m<t.targetPos&&(h=t.targetPos+1),g=r[h]||n,M(r,m,t.targetPos),a.call(e,t.child,g);for(let e of u)e.prev?e.prev.after(e.child):(r[0]||n).before(e.child);for(let e=0;e<r.length;e++)d.has(r[e])||r[e].remove?.();r=o,i=c,o=null,c=null,d=null,l=null,u=null})}function j(e){let t=[],n=[];for(let r=0;r<e.length;r++){let i=e[r],a=0,o=0,s=t.length;for(;a<s;)o=Math.floor((a+s)/2),e[t[o]]>=i?s=o:a=o+1;a>0&&(n[r]=t[a-1]),t[a]=r}let r=new Set,i=t[t.length-1];for(;i!==void 0;)r.add(e[i]),i=n[i];return r}function M(e,t,n){if(t==n)return e;let r=t>n?-1:1,i=e[t];for(let i=t;i!=n;i+=r)e[i]=e[i+r];e[n]=i}function N(e){return e==null?[]:Array.isArray(e)?e:[e]}function P(e,t=!1){return t&&e==null&&(e=``),typeof e==`string`||typeof e==`number`||typeof e==`boolean`?document.createTextNode(e):e?.__raw===void 0?e:e.__raw}function F(e,t={fallbackPath:`#/`,transition:!0}){let n=L(e),r,i=()=>{let e=window.location.hash,i=I(n,e);if(!i){if(t.fallbackPath!=e){window.location.hash=t.fallbackPath;return}console.warn(`missing route:`,e);return}let a=async()=>{try{await r?.(),r=await i.handler(i)}catch(e){console.warn(`route navigation failed:`,e)}};t.transition&&document.startViewTransition?document.startViewTransition(a):a()};return window.addEventListener(`hashchange`,i),i(),()=>{window.removeEventListener(`hashchange`,i)}}function I(e,t){for(let n of e){let e=t.match(n.regex);if(!e)continue;let r={},i=t.split(`?`)?.[1];if(i){let e=new URLSearchParams(i);for(let[t,n]of e.entries())Array.isArray(r[t])||(r[t]=r[t]?[r[t]]:[]),r[t].push(n)}return Object.assign({path:t,query:r,params:e.groups||{}},n)}}function L(e){let t=[];for(let n in e){let r=n.split(`/`);for(let e in r)r[e].length>2&&r[e].startsWith(`{`)&&r[e].endsWith(`}`)?r[e]=`(?<`+r[e].substring(1,r[e].length-1)+`>[^\\/#?]+)`:r[e]=RegExp.escape(r[e]);t.push({regex:RegExp(`^`+r.join(`\\/`)+`(?:[?#].*)?$`),pattern:n,handler:e[n]})}return t}e.router=F,e.store=v,e.t=S,e.watch=g})(this.window=this.window||{});
//# sourceMappingURL=shablon.iife.js.map
+1
View File
@@ -1,3 +1,4 @@
(function(e){let t,n=new Set,r=new Map,i=[],a,o=Symbol(),s=Symbol(),c=Symbol(),l=Symbol(),u=Symbol(),d=Symbol(),f=Symbol(),p=Symbol(),m=Symbol(),h=Symbol();function g(e,n){let d={[o]:`_`+Math.random(),[p]:e,[m]:n};return r.set(d[o],d),d.run=()=>{let e;t&&(e=t,d[s]=t[o],t[c]=t[c]||[],t[c].push(d[o])),d[l]?.forEach(e=>{e.delete(d[o])}),t=d;let n=d[p](d[h],d);d[m]&&(t=null,d[m](n,d[h],d)),d[h]=n,t=e},d.unwatch=function(){d[u]=1,i.push(d[o]),a&&clearTimeout(a),a=setTimeout(()=>{for(let e of i)_(e);i=[],a=null},50)},d.run(),d}function _(e){let t=r.get(e);if(t?.[d]?.(),t?.[c]){for(let e of t[c])_(e);t[s]=null,t[c]=null}if(t?.[l]){for(let n of t[l])n.delete(e);t[l]=null}r.delete(e)}function v(e){return y(e,new Map)}function y(e,n){let r=typeof e==`object`&&!Array.isArray(e)?Object.getOwnPropertyDescriptors(e):{};return new Proxy(e,{get(e,i,a){if(typeof i==`symbol`)return e[i];if(i==`__raw`)return e;if(r[i]?.get&&!e[s]){let n=i;if(i=`@@`+i,!t){let t=r[n].get.call(e);return r[n]._watcher&&(a[i]=t),t}let s=t[o];if(r[n]._refs=r[n]._refs||new Set,!r[n]._refs.has(s)){r[n]._refs.add(s);let e=t[d];t[d]=()=>{e?.(),r[n]._refs.delete(s),!r[n]._refs.size&&r[n]._watcher&&_(r[n]._watcher[o])}}if(!r[n]._watcher){let o=t;t=null;let s=g(r[n].get.bind(e),t=>{e.hasOwnProperty(i)?a[i]=t:Object.defineProperty(e,i,{writable:!0,enumerable:!1,value:t})});r[n]._watcher=s,s[d]=()=>{r[n]._watcher=null},t=o}}let c=e?.[i];if(typeof c==`function`)return c;if(typeof c==`object`&&c&&!c[s]&&(c.constructor?.name==`Object`||c.constructor?.name==`Array`||c.constructor?.name==null)&&(c[s]=[a,i],c=y(c,n),e[i]=c),t){let r=b(e,i),a=t[o],s=[r];t[l]=t[l]||new Set;for(let e of s){let r=n.get(e);r||(r=new Set,n.set(e,r)),r.add(a),t[l].add(r)}}return c},set(e,t,r){if(typeof t==`symbol`)return e[t]=r,!0;let i=e[t];i?.[s]&&(i[f]=!0),r?.[s]&&Array.isArray(e)&&!isNaN(t)&&(r[s][1]=t);let a=!1;return i===void 0&&!e.hasOwnProperty(t)&&(a=!0),e[t]=r,a&&x(e,`toJSON`,n),(r!==i||t===`length`)&&x(e,t,n),!0},deleteProperty(e,t){if(typeof t!=`symbol`){x(e,t,n);let r=b(e,t);for(let e of n)(e[0]==r||e[0].startsWith(r+`/`))&&n.delete(e[0])}return delete e[t]}})}function b(e,t){let n=t,r=e?.[s];for(;r;)n=r[1]+`/`+n,r=r[0][s];return n}function x(e,t,i){let a=b(e,t),o=i.get(a);if(o)for(let e of o)n.delete(e),n.add(e),n.size==1&&queueMicrotask(()=>{let e={},t;for(let i of n)if(t=r.get(i),!(!t||t[u])&&!(t[s]&&n.has(t[s]))){if(e[i]=(e[i]||0)+1,e[i]>250){console.warn(`Possible infinite loop for watcher `+i+`:`,` (function(e){let t,n=new Set,r=new Map,i=[],a,o=Symbol(),s=Symbol(),c=Symbol(),l=Symbol(),u=Symbol(),d=Symbol(),f=Symbol(),p=Symbol(),m=Symbol(),h=Symbol();function g(e,n){let d={[o]:`_`+Math.random(),[p]:e,[m]:n};return r.set(d[o],d),d.run=()=>{let e;t&&(e=t,d[s]=t[o],t[c]=t[c]||[],t[c].push(d[o])),d[l]?.forEach(e=>{e.delete(d[o])}),t=d;let n=d[p](d[h],d);d[m]&&(t=null,d[m](n,d[h],d)),d[h]=n,t=e},d.unwatch=function(){d[u]=1,i.push(d[o]),a&&clearTimeout(a),a=setTimeout(()=>{for(let e of i)_(e);i=[],a=null},50)},d.run(),d}function _(e){let t=r.get(e);if(t?.[d]?.(),t?.[c]){for(let e of t[c])_(e);t[s]=null,t[c]=null}if(t?.[l]){for(let n of t[l])n.delete(e);t[l]=null}r.delete(e)}function v(e){return y(e,new Map)}function y(e,n){let r=typeof e==`object`&&!Array.isArray(e)?Object.getOwnPropertyDescriptors(e):{};return new Proxy(e,{get(e,i,a){if(typeof i==`symbol`)return e[i];if(i==`__raw`)return e;if(r[i]?.get&&!e[s]){let n=i;if(i=`@@`+i,!t){let t=r[n].get.call(e);return r[n]._watcher&&(a[i]=t),t}let s=t[o];if(r[n]._refs=r[n]._refs||new Set,!r[n]._refs.has(s)){r[n]._refs.add(s);let e=t[d];t[d]=()=>{e?.(),r[n]._refs.delete(s),!r[n]._refs.size&&r[n]._watcher&&_(r[n]._watcher[o])}}if(!r[n]._watcher){let o=t;t=null;let s=g(r[n].get.bind(e),t=>{e.hasOwnProperty(i)?a[i]=t:Object.defineProperty(e,i,{writable:!0,enumerable:!1,value:t})});r[n]._watcher=s,s[d]=()=>{r[n]._watcher=null},t=o}}let c=e?.[i];if(typeof c==`function`)return c;if(typeof c==`object`&&c&&!c[s]&&(c.constructor?.name==`Object`||c.constructor?.name==`Array`||c.constructor?.name==null)&&(c[s]=[a,i],c=y(c,n),e[i]=c),t){let r=b(e,i),a=t[o],s=[r];t[l]=t[l]||new Set;for(let e of s){let r=n.get(e);r||(r=new Set,n.set(e,r)),r.add(a),t[l].add(r)}}return c},set(e,t,r){if(typeof t==`symbol`)return e[t]=r,!0;let i=e[t];i?.[s]&&(i[f]=!0),r?.[s]&&Array.isArray(e)&&!isNaN(t)&&(r[s][1]=t);let a=!1;return i===void 0&&!e.hasOwnProperty(t)&&(a=!0),e[t]=r,a&&x(e,`toJSON`,n),(r!==i||t===`length`)&&x(e,t,n),!0},deleteProperty(e,t){if(typeof t!=`symbol`){x(e,t,n);let r=b(e,t);for(let e of n)(e[0]==r||e[0].startsWith(r+`/`))&&n.delete(e[0])}return delete e[t]}})}function b(e,t){let n=t,r=e?.[s];for(;r;)n=r[1]+`/`+n,r=r[0][s];return n}function x(e,t,i){let a=b(e,t),o=i.get(a);if(o)for(let e of o)n.delete(e),n.add(e),n.size==1&&queueMicrotask(()=>{let e={},t;for(let i of n)if(t=r.get(i),!(!t||t[u])&&!(t[s]&&n.has(t[s]))){if(e[i]=(e[i]||0)+1,e[i]>250){console.warn(`Possible infinite loop for watcher `+i+`:`,`
watch(`+t[p]?.toString()+(t[m]?`, `+t[m].toString():``)+`)`);continue}t.run()}n.clear()})}let S=new Proxy({},{get(e,t){return function(){return w(),O.call(void 0,t,...arguments)}}}),C=!1;function w(){if(C)return;C=!0;function e(t,n){for(let r of n)r[t]&&r[t](r),r.childNodes&&e(t,r.childNodes)}new MutationObserver(t=>{for(let n of t)e(`onmount`,n.addedNodes),e(`onunmount`,n.removedNodes)}).observe(document,{childList:!0,subtree:!0})}let T=Symbol(),E=Symbol(),D=Symbol();function O(e,t={},...n){let r=document.createElement(e);if(t)for(let e in t){let n=t[e],i=!1;e.length>5&&e.startsWith(`html-`)&&(i=!0,e=e.substring(5)),n===void 0?r.removeAttribute(e):typeof n!=`function`||e.length>2&&e.startsWith(`on`)?i?r.setAttribute(e,n):r[e]=n:(e==`rid`&&console.warn(`rid is provided as reactive function and will not have effect for `+r+`. watch(`+t[p]?.toString()+(t[m]?`, `+t[m].toString():``)+`)`);continue}t.run()}n.clear()})}let S=new Proxy({},{get(e,t){return function(){return w(),O.call(void 0,t,...arguments)}}}),C=!1;function w(){if(C)return;C=!0;function e(t,n){for(let r of n)r[t]&&r[t](r),r.childNodes&&e(t,r.childNodes)}new MutationObserver(t=>{for(let n of t)e(`onmount`,n.addedNodes),e(`onunmount`,n.removedNodes)}).observe(document,{childList:!0,subtree:!0})}let T=Symbol(),E=Symbol(),D=Symbol();function O(e,t={},...n){let r=document.createElement(e);if(t)for(let e in t){let n=t[e],i=!1;e.length>5&&e.startsWith(`html-`)&&(i=!0,e=e.substring(5)),n===void 0?r.removeAttribute(e):typeof n!=`function`||e.length>2&&e.startsWith(`on`)?i?r.setAttribute(e,n):r[e]=n:(e==`rid`&&console.warn(`rid is provided as reactive function and will not have effect for `+r+`.
Consider using a plain number, string or object/array reference instead.`),r[T]=r[T]||[],r[T].push(()=>{if(!r)return;let t=n(r,e);t===void 0?(r[e]!==void 0&&(r[e]=void 0),r.removeAttribute(e)):i?r.setAttribute(e,t):r[e]=t}))}let i=r.onmount;r.onmount=()=>{if(!r[D]){if(r[D]=!0,r[T]){r[E]=r[E]||[];for(let e of r[T])r[E].push(g(e))}i?.(r)}};let a=r.onunmount;return r.onunmount=()=>{if(r[D]){if(r[D]=!1,r[E])for(let e of r[E])e.unwatch();r[E]=null,a?.(r)}},k(r,n),r}function k(e,t){t=N(t);for(let n of t)if(typeof n==`function`)A(e,n);else{let t=P(n);Array.isArray(t)?k(e,t):t&&e.appendChild(t)}}function A(e,t){let n=document.createComment(``);e.appendChild(n);let r=[],i=new Map,a=e.moveBefore||e.insertBefore;e[T]=e[T]||[],e[T].push(()=>{if(!e)return;let o=N(t(e)),s=o.length,c=new Map;if(!r?.length){let t=document.createDocumentFragment();for(let e=0;e<s;e++){o[e]=P(o[e],!0),t.appendChild(o[e]);let n=o[e].rid;n!==void 0&&(c.has(n)?console.warn(`Duplicated rid:`,n,o[e]):c.set(n,e))}e.insertBefore(t,n),t=null,r=o,i=c;return}let l=[],u=[],d=new Set,f=[];for(let e=0;e<s;e++){o[e]=P(o[e],!0);let t=o[e].rid;if(t!==void 0){c.has(t)?console.warn(`Duplicated rid:`,t,o[e]):c.set(t,e);let n=i.get(t);if(n>=0){d.add(r[n]),o[e]=r[n],f.push(n);continue}}u.push({child:o[e],prev:o[e-1]})}let p=j(f);f.length!=p.length&&f.forEach((e,t)=>{p.has(e)||l.push({child:r[e],targetPos:t})});let m,h,g;for(let t of l)m=r.findIndex(e=>e===t.child),h=t.targetPos,m<t.targetPos&&(h=t.targetPos+1),g=r[h]||n,M(r,m,t.targetPos),a.call(e,t.child,g);for(let e of u)e.prev?e.prev.after(e.child):(r[0]||n).before(e.child);for(let e=0;e<r.length;e++)d.has(r[e])||r[e].remove?.();r=o,i=c,o=null,c=null,d=null,l=null,u=null})}function j(e){let t=[],n=[];for(let r=0;r<e.length;r++){let i=e[r],a=0,o=0,s=t.length;for(;a<s;)o=Math.floor((a+s)/2),e[t[o]]>=i?s=o:a=o+1;a>0&&(n[r]=t[a-1]),t[a]=r}let r=new Set,i=t[t.length-1];for(;i!==void 0;)r.add(e[i]),i=n[i];return r}function M(e,t,n){if(t==n)return e;let r=t>n?-1:1,i=e[t];for(let i=t;i!=n;i+=r)e[i]=e[i+r];e[n]=i}function N(e){return e==null?[]:Array.isArray(e)?e:[e]}function P(e,t=!1){return t&&e==null&&(e=``),typeof e==`string`||typeof e==`number`||typeof e==`boolean`?document.createTextNode(e):e?.__raw===void 0?e:e.__raw}function F(e,t={fallbackPath:`#/`,transition:!0}){let n=L(e),r,i=()=>{let e=window.location.hash,i=I(n,e);if(!i){if(t.fallbackPath!=e){window.location.hash=t.fallbackPath;return}console.warn(`missing route:`,e);return}let a=async()=>{try{await r?.(),r=await i.handler(i)}catch(e){console.warn(`route navigation failed:`,e)}};t.transition&&document.startViewTransition?document.startViewTransition(a):a()};return window.addEventListener(`hashchange`,i),i(),()=>{window.removeEventListener(`hashchange`,i)}}function I(e,t){for(let n of e){let e=t.match(n.regex);if(!e)continue;let r={},i=t.split(`?`)?.[1];if(i){let e=new URLSearchParams(i);for(let[t,n]of e.entries())Array.isArray(r[t])||(r[t]=r[t]?[r[t]]:[]),r[t].push(n)}return Object.assign({path:t,query:r,params:e.groups||{}},n)}}function L(e){let t=[];for(let n in e){let r=n.split(`/`);for(let e in r)r[e].length>2&&r[e].startsWith(`{`)&&r[e].endsWith(`}`)?r[e]=`(?<`+r[e].substring(1,r[e].length-1)+`>[^\\/#?]+)`:r[e]=RegExp.escape(r[e]);t.push({regex:RegExp(`^`+r.join(`\\/`)+`(?:[?#].*)?$`),pattern:n,handler:e[n]})}return t}e.router=F,e.store=v,e.t=S,e.watch=g})(this.window=this.window||{}); Consider using a plain number, string or object/array reference instead.`),r[T]=r[T]||[],r[T].push(()=>{if(!r)return;let t=n(r,e);t===void 0?(r[e]!==void 0&&(r[e]=void 0),r.removeAttribute(e)):i?r.setAttribute(e,t):r[e]=t}))}let i=r.onmount;r.onmount=()=>{if(!r[D]){if(r[D]=!0,r[T]){r[E]=r[E]||[];for(let e of r[T])r[E].push(g(e))}i?.(r)}};let a=r.onunmount;return r.onunmount=()=>{if(r[D]){if(r[D]=!1,r[E])for(let e of r[E])e.unwatch();r[E]=null,a?.(r)}},k(r,n),r}function k(e,t){t=N(t);for(let n of t)if(typeof n==`function`)A(e,n);else{let t=P(n);Array.isArray(t)?k(e,t):t&&e.appendChild(t)}}function A(e,t){let n=document.createComment(``);e.appendChild(n);let r=[],i=new Map,a=e.moveBefore||e.insertBefore;e[T]=e[T]||[],e[T].push(()=>{if(!e)return;let o=N(t(e)),s=o.length,c=new Map;if(!r?.length){let t=document.createDocumentFragment();for(let e=0;e<s;e++){o[e]=P(o[e],!0),t.appendChild(o[e]);let n=o[e].rid;n!==void 0&&(c.has(n)?console.warn(`Duplicated rid:`,n,o[e]):c.set(n,e))}e.insertBefore(t,n),t=null,r=o,i=c;return}let l=[],u=[],d=new Set,f=[];for(let e=0;e<s;e++){o[e]=P(o[e],!0);let t=o[e].rid;if(t!==void 0){c.has(t)?console.warn(`Duplicated rid:`,t,o[e]):c.set(t,e);let n=i.get(t);if(n>=0){d.add(r[n]),o[e]=r[n],f.push(n);continue}}u.push({child:o[e],prev:o[e-1]})}let p=j(f);f.length!=p.length&&f.forEach((e,t)=>{p.has(e)||l.push({child:r[e],targetPos:t})});let m,h,g;for(let t of l)m=r.findIndex(e=>e===t.child),h=t.targetPos,m<t.targetPos&&(h=t.targetPos+1),g=r[h]||n,M(r,m,t.targetPos),a.call(e,t.child,g);for(let e of u)e.prev?e.prev.after(e.child):(r[0]||n).before(e.child);for(let e=0;e<r.length;e++)d.has(r[e])||r[e].remove?.();r=o,i=c,o=null,c=null,d=null,l=null,u=null})}function j(e){let t=[],n=[];for(let r=0;r<e.length;r++){let i=e[r],a=0,o=0,s=t.length;for(;a<s;)o=Math.floor((a+s)/2),e[t[o]]>=i?s=o:a=o+1;a>0&&(n[r]=t[a-1]),t[a]=r}let r=new Set,i=t[t.length-1];for(;i!==void 0;)r.add(e[i]),i=n[i];return r}function M(e,t,n){if(t==n)return e;let r=t>n?-1:1,i=e[t];for(let i=t;i!=n;i+=r)e[i]=e[i+r];e[n]=i}function N(e){return e==null?[]:Array.isArray(e)?e:[e]}function P(e,t=!1){return t&&e==null&&(e=``),typeof e==`string`||typeof e==`number`||typeof e==`boolean`?document.createTextNode(e):e?.__raw===void 0?e:e.__raw}function F(e,t={fallbackPath:`#/`,transition:!0}){let n=L(e),r,i=()=>{let e=window.location.hash,i=I(n,e);if(!i){if(t.fallbackPath!=e){window.location.hash=t.fallbackPath;return}console.warn(`missing route:`,e);return}let a=async()=>{try{await r?.(),r=await i.handler(i)}catch(e){console.warn(`route navigation failed:`,e)}};t.transition&&document.startViewTransition?document.startViewTransition(a):a()};return window.addEventListener(`hashchange`,i),i(),()=>{window.removeEventListener(`hashchange`,i)}}function I(e,t){for(let n of e){let e=t.match(n.regex);if(!e)continue;let r={},i=t.split(`?`)?.[1];if(i){let e=new URLSearchParams(i);for(let[t,n]of e.entries())Array.isArray(r[t])||(r[t]=r[t]?[r[t]]:[]),r[t].push(n)}return Object.assign({path:t,query:r,params:e.groups||{}},n)}}function L(e){let t=[];for(let n in e){let r=n.split(`/`);for(let e in r)r[e].length>2&&r[e].startsWith(`{`)&&r[e].endsWith(`}`)?r[e]=`(?<`+r[e].substring(1,r[e].length-1)+`>[^\\/#?]+)`:r[e]=RegExp.escape(r[e]);t.push({regex:RegExp(`^`+r.join(`\\/`)+`(?:[?#].*)?$`),pattern:n,handler:e[n]})}return t}e.router=F,e.store=v,e.t=S,e.watch=g})(this.window=this.window||{});
//# sourceMappingURL=shablon.iife.js.map
+1 -1
View File
@@ -197,7 +197,7 @@ export function pageCollections(route) {
t.div( t.div(
{ className: "page-content full-height" }, { className: "page-content full-height" },
t.header( t.header(
{ className: "page-header compact flex-nowrap" }, { className: "page-header flex-nowrap" },
t.nav( t.nav(
{ className: "breadcrumbs" }, { className: "breadcrumbs" },
t.div(null, "Collections"), t.div(null, "Collections"),
+1
View File
@@ -35,3 +35,4 @@
@import "./collectionsOverview.css"; @import "./collectionsOverview.css";
@import "./erd.css"; @import "./erd.css";
@import "./hideControls.css"; @import "./hideControls.css";
@import "./sqlConsole.css";
+1
View File
@@ -919,6 +919,7 @@ hr {
.alert { .alert {
display: block; display: block;
width: 100%; width: 100%;
flex-shrink: 0;
align-content: center; align-content: center;
min-height: var(--lgBtnHeight); min-height: var(--lgBtnHeight);
padding: max(5px, calc(var(--inputPadding) - 5px)) max(10px, var(--inputPadding)); padding: max(5px, calc(var(--inputPadding) - 5px)) max(10px, var(--inputPadding));
+1
View File
@@ -1131,6 +1131,7 @@ button {
&:empty::before { &:empty::before {
content: attr(data-placeholder); content: attr(data-placeholder);
color: var(--surfaceTxtDisabledColor); color: var(--surfaceTxtDisabledColor);
white-space: normal;
} }
} }
+5 -10
View File
@@ -174,16 +174,14 @@
column-gap: var(--smSpacing); column-gap: var(--smSpacing);
row-gap: calc(var(--smSpacing) - 5px); row-gap: calc(var(--smSpacing) - 5px);
width: 100%; width: 100%;
flex-shrink: 0;
align-items: center; align-items: center;
margin: -5px 0 calc(var(--spacing) - 5px); min-height: var(--btnHeight);
margin: -10px 0 calc(var(--spacing) - 10px);
.searchbar { .searchbar {
width: auto; width: auto;
flex-grow: 1; flex-grow: 1;
} }
&.compact {
margin-top: -10px;
margin-bottom: calc(var(--spacing) - 10px);
}
.page-header-secondary-btns, .page-header-secondary-btns,
.page-header-primary-btns { .page-header-primary-btns {
display: inline-flex; display: inline-flex;
@@ -206,10 +204,6 @@
} }
@media (max-width: 950px) { @media (max-width: 950px) {
gap: 5px; gap: 5px;
flex-grow: 1;
.flex-nowrap & {
flex-grow: 0;
}
.btn { .btn {
flex-grow: 1; flex-grow: 1;
flex-basis: 0; flex-basis: 0;
@@ -219,7 +213,6 @@
} }
@media (max-width: 550px) { @media (max-width: 550px) {
gap: 5px; gap: 5px;
flex-grow: 0;
.api-preview-btn { .api-preview-btn {
display: none; display: none;
} }
@@ -228,6 +221,7 @@
width: auto; width: auto;
aspect-ratio: 1 / 1; aspect-ratio: 1 / 1;
border-radius: 50%; border-radius: 50%;
min-width: 0;
i + .txt { i + .txt {
display: none; display: none;
} }
@@ -241,6 +235,7 @@
align-items: center; align-items: center;
width: auto; width: auto;
z-index: 10; z-index: 10;
gap: var(--smSpacing);
color: var(--surfaceTxtHintColor); color: var(--surfaceTxtHintColor);
font-size: var(--smFontSize); font-size: var(--smFontSize);
line-height: var(--smLineHeight); line-height: var(--smLineHeight);
+32
View File
@@ -0,0 +1,32 @@
.field.sql-console-field {
.input {
resize: vertical;
height: 250px;
min-height: 150px;
max-height: 400px;
@media (max-height: 650px) {
height: 150px;
}
}
}
.sql-console-history-dropdown {
width: 450px;
&.no-items {
width: 300px;
}
.dropdown-item {
gap: 5px;
padding: 5px;
font-size: 0.9em;
line-height: 1.2;
font-family: var(--monospaceFontFamily);
.query {
display: -webkit-box;
overflow: hidden;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
}
}
}
+17
View File
@@ -317,6 +317,22 @@ table {
min-width: 900px; min-width: 900px;
border-radius: 0; border-radius: 0;
} }
table > :first-child > tr:first-child {
> :first-child {
border-top-left-radius: 0;
}
> :last-child {
border-top-right-radius: 0;
}
}
table > :last-child > tr:last-child {
> :first-child {
border-bottom-left-radius: 0;
}
> :last-child {
border-bottom-right-radius: 0;
}
}
tr { tr {
animation: fadeIn var(--animationSpeed); animation: fadeIn var(--animationSpeed);
} }
@@ -342,6 +358,7 @@ table {
td, td,
th { th {
height: 59px; height: 59px;
border-radius: 0;
} }
} }
-1
View File
@@ -15,7 +15,6 @@
line-height: var(--smLineHeight); line-height: var(--smLineHeight);
font-family: var(--baseFontFamily); font-family: var(--baseFontFamily);
scrollbar-width: thin; scrollbar-width: thin;
margin: 3px;
padding: 3px 5px; padding: 3px 5px;
pointer-events: none; pointer-events: none;
opacity: 0; opacity: 0;
+2
View File
@@ -165,6 +165,8 @@ export function logsList(logsSettings) {
return; // nothing to download return; // nothing to download
} }
console.log(data.bulkSelected);
if (selected.length == 1) { if (selected.length == 1) {
return app.utils.downloadJSON( return app.utils.downloadJSON(
selected[0], selected[0],
+2
View File
@@ -5,6 +5,7 @@ import { pageApplicationSettings } from "@/settings/application/pageApplicationS
import { pageBackupsSettings } from "@/settings/backups/pageBackupsSettings"; import { pageBackupsSettings } from "@/settings/backups/pageBackupsSettings";
import { pageCronsSettings } from "@/settings/crons/pageCronsSettings"; import { pageCronsSettings } from "@/settings/crons/pageCronsSettings";
import { pageMailSettings } from "@/settings/mail/pageMailSettings"; import { pageMailSettings } from "@/settings/mail/pageMailSettings";
import { pageSQLConsole } from "@/settings/sql/pageSQLConsole";
import { pageStorageSettings } from "@/settings/storage/pageStorageSettings"; import { pageStorageSettings } from "@/settings/storage/pageStorageSettings";
import { pageExportCollections } from "@/settings/sync/pageExportCollections"; import { pageExportCollections } from "@/settings/sync/pageExportCollections";
import { pageImportCollections } from "@/settings/sync/pageImportCollections"; import { pageImportCollections } from "@/settings/sync/pageImportCollections";
@@ -170,3 +171,4 @@ app.routes.superuserOnly("#/settings/backups", pageBackupsSettings);
app.routes.superuserOnly("#/settings/crons", pageCronsSettings); app.routes.superuserOnly("#/settings/crons", pageCronsSettings);
app.routes.superuserOnly("#/settings/export-collections", pageExportCollections); app.routes.superuserOnly("#/settings/export-collections", pageExportCollections);
app.routes.superuserOnly("#/settings/import-collections", pageImportCollections); app.routes.superuserOnly("#/settings/import-collections", pageImportCollections);
app.routes.superuserOnly("#/settings/sql", pageSQLConsole);
+486
View File
@@ -0,0 +1,486 @@
import { settingsSidebar } from "../settingsSidebar";
const SQL_HISTORY_STORAGE_KEY = "pbSQLConsoleHistory";
export function pageSQLConsole(route) {
app.store.title = "SQL console";
const uniqueId = "sql_console_" + app.utils.randomString();
const editorId = uniqueId + "editor";
const requestKey = uniqueId + "executeSQL";
const defaultMaxRows = 250;
const pageData = store({
askedForConfirmationAtLeastOnce: false,
isExecuting: false,
maxRows: defaultMaxRows,
query: "",
result: {},
sort: {},
errorMsg: "",
executedHistory: app.utils.getLocalHistory(SQL_HISTORY_STORAGE_KEY, []),
get sortedResultRows() {
// no client-side sort
if (typeof pageData.sort?.index == "undefined") {
const rows = pageData.result?.rows || [];
return pageData.maxRows >= rows.length ? rows : rows.slice(0, pageData.maxRows);
}
const isAsc = !!pageData.sort?.asc;
const sorted = pageData.result?.rows?.toSorted((rowA, rowB) => {
const valA = rowA[pageData.sort.index];
const valB = rowB[pageData.sort.index];
if (isAsc) {
if (valA == valB) {
return 0;
}
if (valA == null) {
return -1;
}
if (valB == null) {
return 1;
}
return valA.localeCompare(valB);
}
if (valA == valB) {
return 0;
}
if (valA == null) {
return 1;
}
if (valB == null) {
return -1;
}
return valB.localeCompare(valA);
}) || [];
return pageData.maxRows >= sorted.length ? sorted : sorted.slice(0, pageData.maxRows);
},
get totalRemainingRows() {
return (pageData.result?.rows?.length << 0) - pageData.sortedResultRows.length;
},
});
function needConfirmation(query) {
if (!query?.trim()) {
return false;
}
// if the hideControls option is enabled ask for confirmation
// at least once no matter of the query
if (
!pageData.askedForConfirmationAtLeastOnce
&& !!app.store.settings?.meta?.hideControls
) {
return true;
}
query = query?.replace(/[\s\;]/gm, " ").toUpperCase() + " ";
return !![
"INSERT ",
"CREATE ",
"UPDATE ",
"DELETE ",
"DROP ",
"DETACH ",
"PRAGMA ",
].find((p) => query.includes(p));
}
async function executeSQLWithConfirm() {
if (!needConfirmation(pageData.query)) {
return executeSQL();
}
pageData.askedForConfirmationAtLeastOnce = true;
return app.modals.confirm(
t.div(
{ className: "txt-center" },
t.h6(
null,
"Be careful and continue only if you really know what you are doing because, depending on the query, the operation could break your application and may not be reversible.",
),
),
() => executeSQL(),
null,
{ yesButton: "Execute", noButton: "Cancel" },
);
}
async function executeSQL() {
pageData.isExecuting = true;
pageData.maxRows = defaultMaxRows;
pageData.result = {};
pageData.sort = {};
pageData.errorMsg = "";
const query = pageData.query.trim();
if (!query) {
app.pb.cancelRequest(requestKey);
pageData.isExecuting = false;
return;
}
try {
// @todo add method to JS SDK
pageData.result = await app.pb.send("/api/sql", {
method: "POST",
body: { query },
requestKey: requestKey,
});
addToHistory(query);
pageData.isExecuting = false;
} catch (err) {
if (!err?.isAbort) {
pageData.isExecuting = false;
pageData.errorMsg = err?.response?.message || err?.message || "Failed to execute query.";
}
}
}
function removeFromHistory(query) {
function looseNormalize(str) {
return str.replace(/[\s\;]/gm, "").toUpperCase();
}
const normalized = looseNormalize(query);
for (let i = pageData.executedHistory.length - 1; i >= 0; i--) {
if (
pageData.executedHistory[i] == query
|| looseNormalize(pageData.executedHistory[i]) == normalized
) {
pageData.executedHistory.splice(i, 1);
}
}
}
function addToHistory(query) {
removeFromHistory(query);
pageData.executedHistory.unshift(pageData.query);
if (pageData.executedHistory.length > 10) {
pageData.executedHistory.splice(10);
}
}
function downloadCSV() {
if (!pageData.sortedResultRows.length) {
return;
}
const data = [pageData.result.columns.map((c) => c.name)].concat(pageData.sortedResultRows);
const name = "export_" + app.utils.toLocalDatetime(new Date()).replace(/[\-\:\. ]/g, "_") + ".csv";
app.utils.downloadCSV(data, name);
}
function toggleSort(index) {
if (pageData.sort?.index == index) {
pageData.sort = {
index: index,
asc: !pageData.sort.asc,
};
} else {
pageData.sort = {
index: index,
asc: true,
};
}
}
const watchers = [
watch(() => JSON.stringify(pageData.executedHistory), (newVal, oldVal) => {
if (typeof oldVal == "undefined") {
return;
}
window.localStorage.setItem(SQL_HISTORY_STORAGE_KEY, newVal);
}),
];
return t.div(
{
pbEvent: "pageSQLConsole",
className: "page",
onunmount: () => {
app.pb.cancelRequest(requestKey);
watchers.forEach((w) => w?.unwatch());
},
},
settingsSidebar(),
t.div(
{ className: "page-content full-height" },
t.header(
{ className: "page-header" },
t.nav(
{ className: "breadcrumbs" },
t.div({ className: "breadcrumb-item" }, "Settings"),
t.div({ className: "breadcrumb-item" }, () => app.store.title),
),
t.div(
{ className: "page-header-secondary-btns" },
t.button(
{
type: "button",
className: "btn circle transparent secondary",
ariaDescription: app.attrs.tooltip("Recently executed queries", "right"),
"html-popovertarget": "sql-console-history-dropdown",
},
t.i({ className: "ri-history-line", ariaHidden: true }),
),
t.div(
{
id: "sql-console-history-dropdown",
className: () =>
`dropdown left sql-console-history-dropdown ${
!pageData.executedHistory.length ? "no-items" : ""
}`,
popover: "auto",
},
() => {
if (!pageData.executedHistory.length) {
return t.span({ className: "txt txt-hint p-5" }, "No recently executed queries.");
}
return pageData.executedHistory.map((item) => {
return t.button(
{
role: "button",
className: "dropdown-item",
onclick: (e) => {
e.target.closest(".dropdown").hidePopover();
pageData.query = item;
document.getElementById(editorId)?.click();
},
},
t.span(
{ className: "query" },
() => app.utils.truncate(item, 500),
),
t.small(
{
role: "button",
className: "remove-btn link-hint m-l-auto p-l-5 p-r-5",
title: "Clear",
onauxclick: (e) => {
e.stopPropagation();
return false;
},
onclick: (e) => {
e.stopPropagation();
removeFromHistory(item);
return false;
},
},
t.i({ className: "ri-close-line", ariaHidden: true }),
),
);
});
},
),
),
t.div(
{ className: "page-header-primary-btns" },
t.button(
{
type: "button",
className: () => `btn expanded-lg ${pageData.isExecuting ? "loading" : ""}`,
disabled: () => pageData.isExecuting,
onclick: () => executeSQLWithConfirm(),
},
t.i({ className: "ri-play-large-line", ariaHidden: true }),
t.span({ className: "txt" }, "Execute"),
),
),
),
t.div(
{ className: "field sql-console-field" },
app.components.codeEditor({
id: editorId,
language: "sql",
required: true,
name: "query",
placeholder: "e.g. EXPLAIN QUERY PLAN SELECT * from users WHERE verified=true",
value: () => pageData.query,
oninput: (val) => pageData.query = val,
onblur: (val) => pageData.query = val.trim(),
}),
),
t.div(
{ className: "flex field-help m-b-sm" },
t.button(
{
type: "button",
className: "link-hint m-l-auto",
"html-popovertarget": uniqueId + "caveats_dropdown",
},
() => "SQL console caveats",
),
t.div(
{
id: uniqueId + "caveats_dropdown",
className: "dropdown sm query-caveats-dropdown",
popover: "auto",
},
t.ul(
null,
t.li(null, "The returned rows are limited up to 1000."),
t.li(null, "The executed queries have a max timeout of 3 minutes."),
t.li(null, "The data is returned as byte strings without any additional formatting."),
t.li(null, "Multiple queries are supported but only the result of the last one is returned."),
),
),
),
// error alert
t.div(
{
hidden: () => pageData.isExecuting || !pageData.errorMsg,
className: "alert danger m-b-sm",
},
t.pre(null, () => pageData.errorMsg),
),
// success alert
t.div(
{
hidden: () =>
pageData.isExecuting || pageData.errorMsg || pageData.result?.columns?.length
|| app.utils.isEmpty(pageData.result),
className: "alert success m-b-sm",
},
t.p({ className: "txt-bold" }, "Query executed successfully!"),
t.p(null, "Affected rows: ", () => pageData.result?.affectedRows || 0),
),
// rows
t.div(
{
hidden: () => pageData.isExecuting || !pageData.result?.columns?.length,
className: "page-table-wrapper",
},
t.table(
{ className: "sql-console-table responsive-table optimize" },
t.thead(
{ className: "sticky" },
t.tr(null, () => {
return pageData.result?.columns?.map((col, i) => {
return t.th({
textContent: col.name,
className: () => {
let classes = "sort-handle";
if (pageData.sort?.index == i) {
classes += pageData.sort.asc ? " asc" : " desc";
}
return classes;
},
onclick: () => toggleSort(i),
});
});
}),
),
t.tbody(
null,
() => {
if (!pageData.sortedResultRows.length) {
return t.tr(
null,
t.td(
{ colSpan: pageData.result?.columns?.length || 1, className: "txt-center" },
t.span({ className: "txt-hint" }, "No rows found."),
),
);
}
return pageData.sortedResultRows.map((rowData) => {
return t.tr(
null,
() => {
return pageData.result?.columns?.map((col, j) => {
const val = rowData[j];
return t.td(
{ "html-data-name": col.name },
val == null ? "NULL" : app.utils.truncate(val, 2000),
);
});
},
);
});
},
// load more btn
t.tr(
{
hidden: () => (
pageData.isExecuting
|| !pageData.result?.rows?.length
|| pageData.result.rows.length <= pageData.sortedResultRows.length
),
},
t.td(
{ colSpan: 99 },
t.button(
{
type: "button",
className: "btn lg secondary load-more-btn",
onclick: () => {
pageData.maxRows = pageData.result?.rows?.length || defaultMaxRows;
},
},
t.span({
className: "txt",
textContent: () => `Load remaining (${pageData.totalRemainingRows})`,
}),
),
),
),
),
),
),
t.footer(
{ className: "page-footer" },
t.span(
{
className: () => `exec-time ${pageData.isExecuting ? "faded" : ""}`,
},
"Time: ",
() => (pageData.result?.execTime || 0) + "ms",
),
t.span(
{
hidden: () => !pageData.result?.columns?.length,
className: () => `total-count ${pageData.isExecuting ? "faded" : ""}`,
},
"Rows: ",
() => pageData.result?.rows?.length || 0,
() => {
if (!pageData.result?.rows?.length) {
return;
}
return [
" (",
t.span({
role: "button",
className: "link-hint",
textContent: "Export as CSV",
onclick: downloadCSV,
}),
")",
];
},
),
app.components.credits(),
),
),
);
}
+8 -1
View File
@@ -115,6 +115,13 @@ window.app.store = store({
label: "Import collections", label: "Import collections",
}, },
], ],
Debug: [
{
href: "#/settings/sql",
icon: "ri-terminal-box-line",
label: "SQL console",
},
],
}, },
predefinedAccentColors: [ predefinedAccentColors: [
@@ -205,7 +212,7 @@ window.app.store = store({
addOrUpdateCollection(collection) { addOrUpdateCollection(collection) {
const index = app.store.collections.findIndex((c) => c.id == collection.id); const index = app.store.collections.findIndex((c) => c.id == collection.id);
if (index >= 0) { if (index >= 0) {
if (app.store.activeCollection.id == collection.id) { if (app.store.activeCollection?.id == collection.id) {
app.store._activeCollectionIdOrName = collection.id; app.store._activeCollectionIdOrName = collection.id;
} }
+23
View File
@@ -844,6 +844,29 @@ const utils = {
utils.download(url, name); utils.download(url, name);
}, },
/**
* Downloads a CSV file created from the provide array data.
*
* @param {Array} arr The JS array to serialize as CSV
* @param {string} name The result file name.
*/
downloadCSV(arr, name) {
name = name.endsWith(".csv") ? name : name + ".csv";
// seriaze data
let content = arr.map((row) => {
return row.map((v) => "\"" + ("" + v).replaceAll("\"", "\"\"") + "\"").join(",");
}).join("\n");
const blob = new Blob([content], {
type: "text/csv;charset=utf-8;",
});
const url = window.URL.createObjectURL(blob);
utils.download(url, name);
},
/** /**
* Returns a normalized API URL address that is used in the API example docs. * Returns a normalized API URL address that is used in the API example docs.
* *