Sync with 1.4.x

This commit is contained in:
Matej Bačo
2023-08-10 01:41:08 +02:00
151 changed files with 6826 additions and 284 deletions
+2
View File
@@ -13,3 +13,5 @@ node_modules
node_modules/
dist/
.vercel
*.swp
+342 -13
View File
@@ -14,11 +14,13 @@
"@popperjs/core": "^2.11.6",
"@sentry/svelte": "^7.44.2",
"@sentry/tracing": "^7.44.2",
"ai": "^2.1.15",
"analytics": "^0.8.1",
"dayjs": "^1.11.9",
"dotenv": "^16.0.3",
"echarts": "^5.4.1",
"logrocket": "^3.0.1",
"nanoid": "^4.0.2",
"pretty-bytes": "^6.1.0",
"prismjs": "^1.29.0",
"svelte-confetti": "^1.2.2",
@@ -544,7 +546,6 @@
"version": "7.22.7",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.7.tgz",
"integrity": "sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==",
"dev": true,
"peer": true,
"bin": {
"parser": "bin/babel-parser.js"
@@ -2307,6 +2308,118 @@
"pretty-format": "^27.5.1"
}
},
"node_modules/@vue/compiler-core": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.4.tgz",
"integrity": "sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==",
"peer": true,
"dependencies": {
"@babel/parser": "^7.21.3",
"@vue/shared": "3.3.4",
"estree-walker": "^2.0.2",
"source-map-js": "^1.0.2"
}
},
"node_modules/@vue/compiler-dom": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.4.tgz",
"integrity": "sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==",
"peer": true,
"dependencies": {
"@vue/compiler-core": "3.3.4",
"@vue/shared": "3.3.4"
}
},
"node_modules/@vue/compiler-sfc": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.4.tgz",
"integrity": "sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==",
"peer": true,
"dependencies": {
"@babel/parser": "^7.20.15",
"@vue/compiler-core": "3.3.4",
"@vue/compiler-dom": "3.3.4",
"@vue/compiler-ssr": "3.3.4",
"@vue/reactivity-transform": "3.3.4",
"@vue/shared": "3.3.4",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.0",
"postcss": "^8.1.10",
"source-map-js": "^1.0.2"
}
},
"node_modules/@vue/compiler-ssr": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz",
"integrity": "sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.3.4",
"@vue/shared": "3.3.4"
}
},
"node_modules/@vue/reactivity": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.4.tgz",
"integrity": "sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==",
"peer": true,
"dependencies": {
"@vue/shared": "3.3.4"
}
},
"node_modules/@vue/reactivity-transform": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz",
"integrity": "sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==",
"peer": true,
"dependencies": {
"@babel/parser": "^7.20.15",
"@vue/compiler-core": "3.3.4",
"@vue/shared": "3.3.4",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.0"
}
},
"node_modules/@vue/runtime-core": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.4.tgz",
"integrity": "sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==",
"peer": true,
"dependencies": {
"@vue/reactivity": "3.3.4",
"@vue/shared": "3.3.4"
}
},
"node_modules/@vue/runtime-dom": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.4.tgz",
"integrity": "sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==",
"peer": true,
"dependencies": {
"@vue/runtime-core": "3.3.4",
"@vue/shared": "3.3.4",
"csstype": "^3.1.1"
}
},
"node_modules/@vue/server-renderer": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.4.tgz",
"integrity": "sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==",
"peer": true,
"dependencies": {
"@vue/compiler-ssr": "3.3.4",
"@vue/shared": "3.3.4"
},
"peerDependencies": {
"vue": "3.3.4"
}
},
"node_modules/@vue/shared": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz",
"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==",
"peer": true
},
"node_modules/abab": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
@@ -2365,6 +2478,71 @@
"node": ">= 6.0.0"
}
},
"node_modules/ai": {
"version": "2.1.32",
"resolved": "https://registry.npmjs.org/ai/-/ai-2.1.32.tgz",
"integrity": "sha512-G+mk7OfSxVR5s2cmpni89olZjXo5XbxPzzG8DAZThoJA2725hczfvO/gqLYGUToq92vzr0XgCHP6JJFK9so5fg==",
"dependencies": {
"eventsource-parser": "1.0.0",
"nanoid": "3.3.6",
"solid-swr-store": "0.10.7",
"sswr": "2.0.0",
"swr": "2.2.0",
"swr-store": "0.10.6",
"swrv": "1.0.4"
},
"engines": {
"node": ">=14.6"
},
"peerDependencies": {
"react": "^18.2.0",
"solid-js": "^1.7.7",
"svelte": "^3.0.0 || ^4.0.0",
"vue": "^3.3.4"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"solid-js": {
"optional": true
},
"svelte": {
"optional": true
},
"vue": {
"optional": true
}
}
},
"node_modules/ai/node_modules/nanoid": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/ai/node_modules/sswr": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/sswr/-/sswr-2.0.0.tgz",
"integrity": "sha512-mV0kkeBHcjcb0M5NqKtKVg/uTIYNlIIniyDfSGrSfxpEdM9C365jK0z55pl9K0xAkNTJi2OAOVFQpgMPUk+V0w==",
"dependencies": {
"swrev": "^4.0.0"
},
"peerDependencies": {
"svelte": "^4.0.0"
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -3109,6 +3287,12 @@
"node": ">=14"
}
},
"node_modules/csstype": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==",
"peer": true
},
"node_modules/data-urls": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz",
@@ -3238,6 +3422,14 @@
"node": ">=0.4.0"
}
},
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
"engines": {
"node": ">=6"
}
},
"node_modules/detect-indent": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz",
@@ -3735,6 +3927,12 @@
"node": ">=4.0"
}
},
"node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"peer": true
},
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
@@ -3744,6 +3942,14 @@
"node": ">=0.10.0"
}
},
"node_modules/eventsource-parser": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-1.0.0.tgz",
"integrity": "sha512-9jgfSCa3dmEme2ES3mPByGXfgZ87VbP97tng1G2nWwWx6bV2nYxm2AWCrbQjXToSe+yYlqaZNtxffR9IeQr95g==",
"engines": {
"node": ">=14.18"
}
},
"node_modules/execa": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
@@ -5700,8 +5906,7 @@
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"node_modules/js-yaml": {
"version": "4.1.0",
@@ -5908,6 +6113,18 @@
"resolved": "https://registry.npmjs.org/logrocket/-/logrocket-3.0.1.tgz",
"integrity": "sha512-jOWG+jEzobKVxGytzZ+4KGm2kiMQMRTHab2uDWQyVZcHfEF38BlCH1yjQVY4LCmuQUwZitP9biMzJZnyUQ0dtQ=="
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"peer": true,
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/loupe": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz",
@@ -6132,10 +6349,9 @@
"dev": true
},
"node_modules/nanoid": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
"dev": true,
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-4.0.2.tgz",
"integrity": "sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==",
"funding": [
{
"type": "github",
@@ -6143,10 +6359,10 @@
}
],
"bin": {
"nanoid": "bin/nanoid.cjs"
"nanoid": "bin/nanoid.js"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
"node": "^14 || ^16 || >=18"
}
},
"node_modules/natural-compare": {
@@ -6493,8 +6709,7 @@
"node_modules/picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
"dev": true
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
},
"node_modules/picomatch": {
"version": "2.3.1",
@@ -6614,7 +6829,6 @@
"version": "8.4.26",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.26.tgz",
"integrity": "sha512-jrXHFF8iTloAenySjM/ob3gSj7pCu0Ji49hnjqzsgSRa50hkWCKD0HQ+gMNJkW38jBI68MpAAg7ZWwHwX8NMMw==",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -6638,6 +6852,23 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/postcss/node_modules/nanoid": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/pre-commit": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/pre-commit/-/pre-commit-1.2.2.tgz",
@@ -6883,6 +7114,18 @@
}
]
},
"node_modules/react": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
@@ -7214,6 +7457,15 @@
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
"node_modules/seroval": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/seroval/-/seroval-0.5.1.tgz",
"integrity": "sha512-ZfhQVB59hmIauJG5Ydynupy8KHyr5imGNtdDhbZG68Ufh1Ynkv9KOYOAABf71oVbQxJ8VkWnMHAjEHE7fWkH5g==",
"peer": true,
"engines": {
"node": ">=10"
}
},
"node_modules/set-cookie-parser": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz",
@@ -7326,6 +7578,28 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/solid-js": {
"version": "1.7.10",
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.7.10.tgz",
"integrity": "sha512-myc4UeC1DeQl4jfcA1bF0X4fUn3tDfHz7QGFMN9PJ77ldG7zzD0CtDz1a2u27gPBSFNjyNL/mJ0N9/6uCuh63A==",
"peer": true,
"dependencies": {
"csstype": "^3.1.0",
"seroval": "^0.5.0"
}
},
"node_modules/solid-swr-store": {
"version": "0.10.7",
"resolved": "https://registry.npmjs.org/solid-swr-store/-/solid-swr-store-0.10.7.tgz",
"integrity": "sha512-A6d68aJmRP471aWqKKPE2tpgOiR5fH4qXQNfKIec+Vap+MGQm3tvXlT8n0I8UgJSlNAsSAUuw2VTviH2h3Vv5g==",
"engines": {
"node": ">=10"
},
"peerDependencies": {
"solid-js": "^1.2",
"swr-store": "^0.10"
}
},
"node_modules/sorcery": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz",
@@ -7354,7 +7628,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -7736,6 +8009,41 @@
"node": ">=12"
}
},
"node_modules/swr": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/swr/-/swr-2.2.0.tgz",
"integrity": "sha512-AjqHOv2lAhkuUdIiBu9xbuettzAzWXmCEcLONNKJRba87WAefz8Ca9d6ds/SzrPc235n1IxWYdhJ2zF3MNUaoQ==",
"dependencies": {
"use-sync-external-store": "^1.2.0"
},
"peerDependencies": {
"react": "^16.11.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/swr-store": {
"version": "0.10.6",
"resolved": "https://registry.npmjs.org/swr-store/-/swr-store-0.10.6.tgz",
"integrity": "sha512-xPjB1hARSiRaNNlUQvWSVrG5SirCjk2TmaUyzzvk69SZQan9hCJqw/5rG9iL7xElHU784GxRPISClq4488/XVw==",
"dependencies": {
"dequal": "^2.0.3"
},
"engines": {
"node": ">=10"
}
},
"node_modules/swrev": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/swrev/-/swrev-4.0.0.tgz",
"integrity": "sha512-LqVcOHSB4cPGgitD1riJ1Hh4vdmITOp+BkmfmXRh4hSF/t7EnS4iD+SOTmq7w5pPm/SiPeto4ADbKS6dHUDWFA=="
},
"node_modules/swrv": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/swrv/-/swrv-1.0.4.tgz",
"integrity": "sha512-zjEkcP8Ywmj+xOJW3lIT65ciY/4AL4e/Or7Gj0MzU3zBJNMdJiT8geVZhINavnlHRMMCcJLHhraLTAiDOTmQ9g==",
"peerDependencies": {
"vue": ">=3.2.26 < 4"
}
},
"node_modules/symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
@@ -8020,6 +8328,14 @@
"requires-port": "^1.0.0"
}
},
"node_modules/use-sync-external-store": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -8217,6 +8533,19 @@
}
}
},
"node_modules/vue": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.3.4.tgz",
"integrity": "sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.3.4",
"@vue/compiler-sfc": "3.3.4",
"@vue/runtime-dom": "3.3.4",
"@vue/server-renderer": "3.3.4",
"@vue/shared": "3.3.4"
}
},
"node_modules/w3c-xmlserializer": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz",
+2
View File
@@ -26,11 +26,13 @@
"@popperjs/core": "^2.11.6",
"@sentry/svelte": "^7.44.2",
"@sentry/tracing": "^7.44.2",
"ai": "^2.1.15",
"analytics": "^0.8.1",
"dayjs": "^1.11.9",
"dotenv": "^16.0.3",
"echarts": "^5.4.1",
"logrocket": "^3.0.1",
"nanoid": "^4.0.2",
"pretty-bytes": "^6.1.0",
"prismjs": "^1.29.0",
"svelte-confetti": "^1.2.2",
+42
View File
@@ -0,0 +1,42 @@
import { tick } from 'svelte';
import type { Action } from 'svelte/action';
export type PortalConfig = string | HTMLElement | undefined;
export const portal: Action<HTMLElement, PortalConfig> = (el, target = 'body') => {
let targetEl;
async function update(newTarget: PortalConfig) {
target = newTarget;
if (typeof target === 'string') {
targetEl = document.querySelector(target);
if (targetEl === null) {
await tick();
targetEl = document.querySelector(target);
}
if (targetEl === null) {
throw new Error(`No element found matching css selector: "${target}"`);
}
} else if (target instanceof HTMLElement) {
targetEl = target;
} else {
throw new TypeError(
`Unknown portal target type: ${
target === null ? 'null' : typeof target
}. Allowed types: string (CSS selector) or HTMLElement.`
);
}
targetEl.appendChild(el);
el.hidden = false;
}
function destroy() {
el.remove();
}
update(target);
return {
update,
destroy
};
};
+142
View File
@@ -0,0 +1,142 @@
<script lang="ts" context="module">
type Context = Readable<{
isInitialPanel: boolean;
open: boolean;
}>;
type ReadableValue<T> = T extends Readable<infer U> ? U : never;
const contextKey = 'command-center';
export const getCommandCenterCtx = () => getContext<Context>(contextKey);
const setCommandCenterCtx = (value: ReadableValue<Context>) => {
const store = writable(value);
setContext(contextKey, store);
return store;
};
export const toggleCommandCenter = () => {
if (get(subPanels).length > 0) {
clearSubPanels();
} else {
addSubPanel(RootPanel);
}
};
</script>
<script lang="ts">
import { dev } from '$app/environment';
import { portal } from '$lib/actions/portal';
import { last } from '$lib/helpers/array';
import { debounce } from '$lib/helpers/debounce';
import { getContext, setContext } from 'svelte';
import { get, writable, type Readable } from 'svelte/store';
import { fade } from 'svelte/transition';
import { commandCenterKeyDownHandler, disableCommands, registerCommands } from './commands';
import { RootPanel } from './panels';
import { addSubPanel, clearSubPanels, subPanels } from './subPanels';
$: $registerCommands([
{
callback: toggleCommandCenter,
keys: ['k'],
ctrl: true,
forceEnable: true
}
]);
$: openSubPanel = last($subPanels) ?? null;
$: $disableCommands(!!openSubPanel);
$: if (openSubPanel) {
document.documentElement.classList.add('u-overflow-hidden');
} else {
document.documentElement.classList.remove('u-overflow-hidden');
}
let dialog: HTMLDivElement;
function handleBlur(event: MouseEvent) {
if (event.target === dialog) {
clearSubPanels();
}
}
const ctx = setCommandCenterCtx({
isInitialPanel: true,
open: false
});
$: if (!openSubPanel) {
$ctx.isInitialPanel = true;
}
$: $subPanels.length > 1 && ($ctx.isInitialPanel = false);
$: $ctx.open = !!openSubPanel;
let keys: string[] = [];
const resetKeys = debounce(() => {
keys = [];
}, 1000);
function isInputEvent(event: KeyboardEvent) {
return ['INPUT', 'TEXTAREA', 'SELECT'].includes((event.target as HTMLElement).tagName);
}
const handleKeydown = (e) => {
if (!$subPanels.length) {
if (isInputEvent(e)) return;
keys = [...keys, e.key].slice(-10);
resetKeys();
}
$commandCenterKeyDownHandler(e);
};
</script>
<svelte:window on:mousedown={handleBlur} on:keydown={handleKeydown} />
{#if openSubPanel}
<div class="dialog" bind:this={dialog} transition:fade={{ duration: 100 }}>
<svelte:component this={openSubPanel.component} />
</div>
{/if}
{#if dev}
<div class="debug-keys" use:portal>
{#each keys as key, i (i)}
<kbd class="kbd" transition:fade|local={{ duration: 150 }}>
{key.length === 1 ? key.toUpperCase() : key}
</kbd>
{/each}
</div>
{/if}
<style lang="scss">
.dialog {
padding: 0.5rem;
position: fixed;
inset: 0;
background-color: hsl(var(--color-neutral-500) / 0.5);
z-index: 9999;
}
.debug-keys {
position: fixed;
bottom: 10%;
left: 50%;
transform: translateX(-50%);
padding: 0.5rem;
// background-color: hsl(var(--color-neutral-500) / 0.5);
z-index: 9999;
display: flex;
gap: 1rem;
font-size: 2rem;
.kbd {
padding-inline: 0.5rem;
padding-block: 1.5rem;
}
}
</style>
+349
View File
@@ -0,0 +1,349 @@
import { debounce } from '$lib/helpers/debounce';
import { isMac } from '$lib/helpers/platform';
import { wizard } from '$lib/stores/wizard';
import { onMount } from 'svelte';
import { derived, writable } from 'svelte/store';
import { nanoid } from 'nanoid/non-secure';
import { trackEvent } from '$lib/actions/analytics';
const groups = [
'ungrouped',
'navigation',
'projects',
'organizations',
'auth',
'help',
'account',
'platforms',
'databases',
'functions',
'storage',
'domains',
'webhooks',
'integrations',
'migrations',
'users',
'collections',
'attributes',
'indexes',
'documents',
'teams',
'security',
'buckets',
'files',
'misc'
] as const;
export type CommandGroup = (typeof groups)[number];
type BaseCommand = {
callback: () => void;
label?: string;
disabled?: boolean;
forceEnable?: boolean;
group?: CommandGroup;
icon?: string;
rank?: number;
nested?: boolean;
keepOpen?: boolean;
};
type KeyedCommand = BaseCommand & {
keys: string[];
/* Ctrl on Windows/Linux, Meta on Mac */
ctrl?: boolean;
shift?: boolean;
/* Alt on Windows/Linux, Option on Mac */
alt?: boolean;
};
function isKeyedCommand(command: Command): command is KeyedCommand {
return 'keys' in command;
}
export type Command = KeyedCommand | BaseCommand;
export const commandMap = writable<Map<string, Command[]>>(new Map());
export const disabledMap = writable<Map<string, boolean>>(new Map());
// Derived stores
export const commands = derived(commandMap, ($commandMap) => {
return Array.from($commandMap.values()).flat();
});
const commandsEnabled = derived(disabledMap, ($disabledMap) => {
// If there's an item on the disabledMap that's true, then disable the command center
return Array.from($disabledMap.values()).every((disabled) => !disabled);
});
function isInputEvent(event: KeyboardEvent) {
return ['INPUT', 'TEXTAREA', 'SELECT'].includes((event.target as HTMLElement).tagName);
}
function getCommandRank(command: KeyedCommand) {
const { keys, ctrl: meta, shift, alt } = command;
const modifiers = [meta, shift, alt].filter(Boolean).length;
return keys.length + modifiers * 10;
}
function hasDisputing(command: KeyedCommand, allCommands: Command[]) {
return allCommands.some((otherCommand) => {
if (command === otherCommand) {
return false;
}
if (!isKeyedCommand(otherCommand)) {
return false;
}
const keysString = command.keys.join('+');
const otherKeysString = otherCommand.keys.join('+');
const cmdRank = getCommandRank(command);
const otherCmdRank = getCommandRank(otherCommand);
return (
(keysString.includes(otherKeysString) || otherKeysString.includes(keysString)) &&
cmdRank <= otherCmdRank
);
});
}
export const commandCenterKeyDownHandler = derived(
[commandMap, commandsEnabled, wizard],
([$commandMap, enabled, $wizard]) => {
const commandsArr = Array.from($commandMap.values()).flat();
let recentKeyCodes: number[] = [];
let validCommands: KeyedCommand[] = [];
const reset = debounce(() => {
recentKeyCodes = [];
validCommands = [];
}, 1000);
const getHighestPriorityCommand = () => {
if (!validCommands.length) return;
if (validCommands.length === 1) {
return validCommands[0];
}
// Rank commands by how many keys and modifiers they have.
// Each key is worth 1 point, each modifier is worth 10 points.
// The command with the highest score wins.
const rankedCommands = validCommands.map((command) => {
return { command, score: getCommandRank(command) };
});
const highestScore = Math.max(...rankedCommands.map(({ score }) => score));
const highestScoreCommands = rankedCommands.filter(
({ score }) => score === highestScore
);
if (highestScoreCommands.length === 1) {
return highestScoreCommands[0].command;
}
// If there's still a tie, the command with the most modifiers wins.
// And if even that's a tie, the first command wins.
const mostModifiers = Math.max(
...highestScoreCommands.map(({ command }) => {
const { ctrl: meta, shift, alt } = command;
return [meta, shift, alt].filter(Boolean).length;
})
);
const mostModifiersCommands = highestScoreCommands.filter(({ command }) => {
const { ctrl: meta, shift, alt } = command;
return [meta, shift, alt].filter(Boolean).length === mostModifiers;
});
return mostModifiersCommands[0]?.command;
};
const rankAndExecute = debounce(() => {
const command = getHighestPriorityCommand();
command?.callback();
reset.immediate();
}, 200);
const execute = (command: KeyedCommand) => {
if (hasDisputing(command, commandsArr)) {
validCommands.push(command);
rankAndExecute();
} else {
command.callback();
reset.immediate();
}
};
return (event: KeyboardEvent) => {
recentKeyCodes.push(event.keyCode);
reset();
for (const command of commandsArr) {
if (!isKeyedCommand(command)) continue;
if (!command.forceEnable) {
if (command.disabled || !enabled || isInputEvent(event) || $wizard.show) {
continue;
}
}
const { keys, ctrl: meta, shift, alt } = command;
const isMetaPressed = meta
? isMac()
? event.metaKey
: event.ctrlKey
: !(isMac() ? event.metaKey : event.ctrlKey);
const isShiftPressed = shift ? event.shiftKey : !event.shiftKey;
const isAltPressed = alt ? event.altKey : !event.altKey;
const commandKeyCodes = keys.map((key) => key.toUpperCase().charCodeAt(0));
const allKeysPressed = recentKeyCodes.join('').includes(commandKeyCodes.join(''));
if (allKeysPressed && isMetaPressed && isShiftPressed && isAltPressed) {
event.preventDefault();
execute(command);
}
}
};
}
);
// Methods
export const registerCommands = {
subscribe(runner: (cb: (newCommands: Command[]) => void) => void) {
const uuid = nanoid();
runner((newCommands: Command[]) => {
commandMap.update((curr) => {
const commandsWithTracking = newCommands.map((command) => {
const trackingCallback = () => {
if (command.label) {
trackEvent('command', { label: command.label, group: command.group });
}
command.callback();
};
return { ...command, callback: trackingCallback };
});
curr.set(uuid, commandsWithTracking);
return curr;
});
});
return () => {
commandMap.update((curr) => {
curr.delete(uuid);
return curr;
});
};
}
};
export const disableCommands = {
subscribe(runner: (cb: (disabled: boolean) => void) => void) {
const uuid = nanoid();
runner((disabled: boolean) => {
disabledMap.update((curr) => {
curr.set(uuid, disabled);
return curr;
});
});
return () => {
disabledMap.update((curr) => {
curr.delete(uuid);
return curr;
});
};
}
};
type CommandGroupRanks = Partial<Record<CommandGroup, number>>;
type GroupRanksMap = Map<string, CommandGroupRanks>;
const groupRanksMap = writable<GroupRanksMap>(new Map());
export const updateCommandGroupRanks = {
subscribe(runner: (cb: (updater: CommandGroupRanks) => void) => void) {
const uuid = nanoid();
runner((groupRank: CommandGroupRanks) => {
groupRanksMap.update((curr) => {
curr.set(uuid, groupRank);
return curr;
});
});
return () => {
groupRanksMap.update((curr) => {
curr.delete(uuid);
return curr;
});
};
}
};
export const commandGroupRanks = derived(groupRanksMap, ($groupRankTransformations) => {
const initialRanks = {
...Object.fromEntries(groups.map((group) => [group, 0])),
ungrouped: 9999,
databases: 3,
users: 2,
teams: 1,
navigation: -10,
help: -20,
misc: -30
} as CommandGroupRanks;
const transformations = Array.from($groupRankTransformations.values());
return transformations.reduce((prev, curr) => ({ ...prev, ...curr }), initialRanks);
});
export type Searcher = (query: string) => Promise<Command[]>;
const searchersMap = writable<Map<string, Searcher[]>>(new Map());
export const registerSearchers = {
subscribe(runner: (cb: (...searchers: Searcher[]) => void) => void) {
const uuid = nanoid();
runner((...searchers: Searcher[]) => {
searchersMap.update((curr) => {
curr.set(uuid, [...searchers]);
return curr;
});
});
return () => {
searchersMap.update((curr) => {
curr.delete(uuid);
return curr;
});
};
}
};
export const searchers = derived(searchersMap, ($searchersMap) => {
return Array.from($searchersMap.values()).flat();
});
export const initSearcher = (searcher: Searcher) => {
const search = writable('');
const results = writable<Command[]>([]);
const searcherDebounced = debounce(async (query: string) => {
results.set(await searcher(query));
}, 500);
onMount(() => {
searcherDebounced.immediate('');
return search.subscribe((query) => {
searcherDebounced(query);
});
});
return {
search,
results
};
};
+4
View File
@@ -0,0 +1,4 @@
export * from './commands';
export * from './subPanels';
import CommandCenter from './commandCenter.svelte';
export { CommandCenter };
+227
View File
@@ -0,0 +1,227 @@
<script lang="ts">
import Template from './template.svelte';
import { AvatarInitials, Code } from '$lib/components';
import { user } from '$lib/stores/user';
import { useCompletion } from 'ai/svelte';
import { subPanels } from '../subPanels';
import { isLanguage, type Language } from '$lib/components/code.svelte';
import CoolerAppwrite from '$lib/images/appwrite-cooler.svg';
import { VARS } from '$lib/system';
const { input, handleSubmit, completion, isLoading, complete } = useCompletion({
api: VARS.ASSISTANT_ENDPOINT
});
const examples = [
'How to add platform in the console?',
'How can I manage users, permissions, and access control in Appwrite?',
'How can I set up database collections and documents in Appwrite?',
'How do I configure and manage server-side functions in Appwrite?',
'How to add custom domain in the console?'
];
type Answer = Array<
| {
type: 'code';
language: Language;
value: string;
}
| {
type: 'text';
value: string;
}
>;
function parseCompletion(c: string): Answer | null {
if (!c) return null;
const answer: Answer = [];
// get all code matches
const codeMatches = Array.from(c.matchAll(/```([a-z]*)(.*?)(```|$)/gs));
let i = 0;
let codeIdx = 0;
while (i < c.length) {
const nextCodeMatch = codeMatches[codeIdx] ?? null;
if (!nextCodeMatch) {
answer.push({
type: 'text',
value: c.slice(i)
});
break;
} else if (i >= nextCodeMatch.index) {
let language = nextCodeMatch[1];
if (language === 'javascript') language = 'js';
answer.push({
type: 'code',
value: nextCodeMatch[2],
language: isLanguage(language) ? language : 'js'
});
i = nextCodeMatch.index + nextCodeMatch[0].length;
codeIdx++;
} else {
answer.push({
type: 'text',
value: c.slice(i, nextCodeMatch.index)
});
i = nextCodeMatch.index;
}
}
return answer;
}
$: answer = parseCompletion($completion);
</script>
<Template
options={$isLoading || answer
? undefined
: examples.map((e) => {
return {
label: e,
callback: () => {
$input = e;
complete($input);
},
group: 'Examples'
};
})}
clearOnCallback={false}
fullheight
--command-panel-max-height="40rem">
<div slot="search" />
<div slot="option" let:option class="u-flex u-cross-center u-gap-8">
<i class="icon-question-mark-circle" />
<span>{option.label}</span>
</div>
{#if $isLoading || answer}
<div class="content">
<div class="u-flex u-gap-8">
<div class="logo">
<img src={CoolerAppwrite} alt="Appwrite logo" />
</div>
<div class="answer">
{#if $isLoading && !$completion}
<p>...</p>
{:else}
{#each answer as part}
{#if part.type === 'text'}
<p>{part.value}</p>
{:else if part.type === 'code'}
{#key part.value}
<Code language={part.language} code={part.value} />
{/key}
{/if}
{/each}
{/if}
</div>
</div>
</div>
{/if}
<div class="footer" slot="footer">
<div class="u-flex u-cross-center u-gap-4">
<AvatarInitials size={32} name={$user.name} />
<form
class="input-text-wrapper u-width-full-line"
style="--amount-of-buttons: 1;"
on:submit|preventDefault={handleSubmit}>
<!-- svelte-ignore a11y-autofocus -->
<input
type="text"
class="input-text"
placeholder="Ask a question..."
autofocus
bind:value={$input}
disabled={$isLoading} />
<div class="options-list">
<button
class="options-list-button"
aria-label="ask AI"
type="submit"
disabled={!$input.trim() || $isLoading}>
<span class="icon-arrow-sm-right" aria-hidden="true" />
</button>
</div>
</form>
</div>
<div class="u-flex u-main-end u-cross-center u-gap-16 u-margin-block-start-16">
<div class="u-flex u-cross-center u-gap-4">
<kbd class="kbd">Enter</kbd>
<span>to search</span>
</div>
<div class="sep" />
<div class="u-flex u-cross-center u-gap-4">
<kbd class="kbd">Esc</kbd>
<span>to {$subPanels.length === 1 ? 'close' : 'go back'}</span>
</div>
</div>
</div>
</Template>
<style lang="scss">
.content {
overflow-y: auto;
padding: 1rem;
.logo {
display: flex;
width: 1.5rem;
height: 1.5rem;
justify-content: center;
align-items: center;
flex-shrink: 0;
border-radius: 0.25rem;
background: #282a3b;
}
.answer {
overflow: hidden;
p {
white-space: pre-wrap;
}
}
h2 {
color: hsl(var(--color-neutral-70));
}
.examples {
display: flex;
flex-direction: column;
li {
padding: 0.59375rem 0.5rem;
button {
&:hover {
opacity: 0.75;
}
i {
color: hsl(var(--color-neutral-70));
}
}
}
}
}
.footer {
.sep {
width: 1px;
height: 1.5rem;
background-color: hsl(var(--color-neutral-150));
}
}
</style>
@@ -0,0 +1,9 @@
<script lang="ts">
import { initSearcher } from '../commands';
import { bucketSearcher } from '../searchers';
import Template from './template.svelte';
const { search, results } = initSearcher(bucketSearcher);
</script>
<Template options={$results} bind:search={$search} />
@@ -0,0 +1,9 @@
<script lang="ts">
import { initSearcher } from '../commands';
import { collectionsSearcher } from '../searchers';
import Template from './template.svelte';
const { search, results } = initSearcher(collectionsSearcher);
</script>
<Template options={$results} bind:search={$search} />
@@ -0,0 +1,28 @@
<script lang="ts">
import { initCreateAttribute } from '$routes/console/project-[project]/databases/database-[database]/collection-[collection]/+layout.svelte';
import { attributeOptions } from '$routes/console/project-[project]/databases/database-[database]/collection-[collection]/attributes/store';
import Template from './template.svelte';
let search = '';
let options = attributeOptions.map((option) => {
return {
label: option.name,
icon: option.icon,
callback() {
initCreateAttribute(option.name);
}
};
});
$: filteredOptions = options.filter((option) => {
return option.label.toLowerCase().includes(search.toLowerCase());
});
</script>
<Template options={filteredOptions} bind:search>
<div class="u-flex u-cross-center u-gap-8" slot="option" let:option>
<i class="icon-{option.icon}" />
<span>{option.label}</span>
</div>
</Template>
@@ -0,0 +1,9 @@
<script lang="ts">
import { initSearcher } from '../commands';
import { dbSearcher } from '../searchers';
import Template from './template.svelte';
const { search, results } = initSearcher(dbSearcher);
</script>
<Template options={$results} bind:search={$search} />
@@ -0,0 +1,9 @@
<script lang="ts">
import { initSearcher } from '../commands';
import { fileSearcher } from '../searchers';
import Template from './template.svelte';
const { search, results } = initSearcher(fileSearcher);
</script>
<Template options={$results} bind:search={$search} />
@@ -0,0 +1,9 @@
<script lang="ts">
import { initSearcher } from '../commands';
import { functionsSearcher } from '../searchers';
import Template from './template.svelte';
const { search, results } = initSearcher(functionsSearcher);
</script>
<Template options={$results} bind:search={$search} />
+80
View File
@@ -0,0 +1,80 @@
import type { SubPanel } from '../subPanels';
export { default as Template } from './template.svelte';
import Root from './root.svelte';
export const RootPanel: SubPanel = {
name: 'root',
component: Root
};
import AI from './ai.svelte';
export const AIPanel: SubPanel = {
name: 'Ask the AI',
component: AI
};
import Projects from './projects.svelte';
export const ProjectsPanel: SubPanel = {
name: 'Projects',
component: Projects
};
import Organizations from './organizations.svelte';
export const OrganizationsPanel: SubPanel = {
name: 'Organizations',
component: Organizations
};
import Platforms from './platforms.svelte';
export const PlatformsPanel: SubPanel = {
name: 'Platforms',
component: Platforms
};
import Databases from './databases.svelte';
export const DatabasesPanel: SubPanel = {
name: 'Databases',
component: Databases
};
import Collections from './collections.svelte';
export const CollectionsPanel: SubPanel = {
name: 'Collections',
component: Collections
};
import CreateAttribute from './createAttribute.svelte';
export const CreateAttributePanel: SubPanel = {
name: 'Create Attribute',
component: CreateAttribute
};
import Users from './users.svelte';
export const UsersPanel: SubPanel = {
name: 'Users',
component: Users
};
import Teams from './teams.svelte';
export const TeamsPanel: SubPanel = {
name: 'Teams',
component: Teams
};
import Functions from './functions.svelte';
export const FunctionsPanel: SubPanel = {
name: 'Functions',
component: Functions
};
import Buckets from './buckets.svelte';
export const BucketsPanel: SubPanel = {
name: 'Buckets',
component: Buckets
};
import Files from './files.svelte';
export const FilesPanel: SubPanel = {
name: 'Files',
component: Files
};
@@ -0,0 +1,9 @@
<script lang="ts">
import { initSearcher } from '../commands';
import { orgSearcher } from '../searchers';
import Template from './template.svelte';
const { results, search } = initSearcher(orgSearcher);
</script>
<Template options={$results} bind:search={$search} />
@@ -0,0 +1,55 @@
<script lang="ts">
import {
Platform,
addPlatform
} from '$routes/console/project-[project]/overview/platforms/+page.svelte';
import Template from './template.svelte';
let search = '';
let platforms = [
{
label: 'Web',
icon: 'code',
group: 'platforms',
callback: () => {
addPlatform(Platform.Web);
}
},
{
label: 'Flutter',
icon: 'flutter',
group: 'platforms',
callback: () => {
addPlatform(Platform.Flutter);
}
},
{
label: 'Android',
icon: 'android',
group: 'platforms',
callback: () => {
addPlatform(Platform.Android);
}
},
{
label: 'Apple',
icon: 'apple',
group: 'platforms',
callback: () => {
addPlatform(Platform.Apple);
}
}
] as const;
$: filteredPlatforms = platforms.filter((platform) => {
return platform.label.toLowerCase().includes(search.toLowerCase());
});
</script>
<Template options={filteredPlatforms} bind:search>
<div class="u-flex u-cross-center u-gap-8" slot="option" let:option>
<i class="icon-{option.icon}" />
<span>{option.label}</span>
</div>
</Template>
@@ -0,0 +1,9 @@
<script lang="ts">
import { initSearcher } from '../commands';
import { projectsSearcher } from '../searchers';
import Template from './template.svelte';
const { search, results } = initSearcher(projectsSearcher);
</script>
<Template options={$results} bind:search={$search} />
+87
View File
@@ -0,0 +1,87 @@
<!--
This is the root command panel. It precedes all other command panels.
-->
<script lang="ts">
import { debounce } from '$lib/helpers/debounce';
import { isMac } from '$lib/helpers/platform';
import { commands, searchers, type Command } from '../commands';
import Template from './template.svelte';
let search = '';
let searchResults: Omit<Command, 'keys'>[] = [];
const executeSearch = debounce(async (s: string) => {
$searchers.forEach(async (searcher) => {
const results = await searcher(s);
searchResults = [...searchResults, ...results];
});
}, 500);
$: {
if (search) {
searchResults = [];
executeSearch(search);
} else {
searchResults = [];
executeSearch.cancel();
}
}
$: results = [
...$commands.filter((command) => {
return (
!command.disabled &&
command.label &&
command.label.toLowerCase().includes(search.toLowerCase())
);
}),
...searchResults
] as Array<Command | Omit<Command, 'keys'>>;
const hasCtrl = (command: Command) => {
return 'ctrl' in command && command.ctrl;
};
const hasShift = (command: Command) => {
return 'shift' in command && command.shift;
};
const hasAlt = (command: Command) => {
return 'alt' in command && command.alt;
};
</script>
<Template options={results} bind:search searchPlaceholder="Search for commands or content...">
<div slot="option" class="u-flex u-main-space-between content" let:option={command}>
<div class="u-flex u-gap-8 u-cross-center">
<i class="icon-{command.icon ?? 'arrow-sm-right'}" />
<span>
{command.label}
</span>
</div>
<div class="u-flex u-gap-4 u-cross-center">
{#if hasCtrl(command)}
<kbd class="kbd"> {isMac() ? '⌘' : 'Ctrl'} </kbd>
{/if}
{#if hasShift(command)}
<kbd class="kbd"> {isMac() ? '⇧' : 'Shift'} </kbd>
{/if}
{#if hasAlt(command)}
<kbd class="kbd"> {isMac() ? '⌥' : 'Alt'} </kbd>
{/if}
{#if 'keys' in command}
{#each command.keys as key, i}
{@const hasNext = command.keys.length - 1 !== i}
<kbd class="kbd">
{key.toUpperCase()}
</kbd>
{#if hasNext}
<span class="u-margin-inline-4" style:opacity={0.5}>then</span>
{/if}
{/each}
{/if}
</div>
</div>
<svelte:fragment slot="no-options">No commands found</svelte:fragment>
</Template>
@@ -0,0 +1,9 @@
<script lang="ts">
import { initSearcher } from '../commands';
import { teamSearcher } from '../searchers';
import Template from './template.svelte';
const { search, results } = initSearcher(teamSearcher);
</script>
<Template options={$results} bind:search={$search} />
@@ -0,0 +1,515 @@
<script lang="ts">
import { commandGroupRanks, type CommandGroup, type Command } from '../commands';
// This is the template for all panels used in the command center.
// Use this component when you want to create a new panel.
import { tick } from 'svelte';
import { getCommandCenterCtx } from '../commandCenter.svelte';
import { clearSubPanels, popSubPanel, subPanels } from '../subPanels';
type Option = $$Generic<Command>;
export let options: Option[] | null = null;
export let search = '';
export let searchPlaceholder = 'Search...';
export let fullheight = false;
export let clearOnCallback = true;
let selected = 0;
let contentEl: HTMLElement;
async function triggerOption(option: Option) {
const prevPanels = $subPanels.length;
option.callback();
if (prevPanels === $subPanels.length && clearOnCallback && !option.keepOpen) {
clearSubPanels();
}
}
function handleKeyDown(event: KeyboardEvent) {
if (!open) return;
if (options) {
if (event.key === 'ArrowDown') {
event.preventDefault();
if (event.metaKey) {
selected = options.length - 1;
} else {
selected = selected === options.length - 1 ? options.length - 1 : selected + 1;
}
} else if (event.key === 'ArrowUp') {
event.preventDefault();
if (event.metaKey) {
selected = 0;
} else {
selected = selected === 0 ? 0 : selected - 1;
}
} else if (event.key === 'Enter') {
const option = groupsAndOptions.find(
(item) => 'index' in item && item.index === selected
) as IndexedOption;
if (!option) return;
event.preventDefault();
triggerOption(option);
} else if (event.key === 'Home') {
event.preventDefault();
selected = 0;
} else if (event.key === 'End') {
event.preventDefault();
selected = options.length - 1;
}
tick().then(() => {
if (!cardEl) return;
const resultEls = Array.from(contentEl.querySelectorAll('.result'));
const selectedEl = contentEl.querySelector('[data-selected]');
const selectedIdx = resultEls.indexOf(selectedEl);
if (selectedIdx === 0) {
contentEl.scrollTo({
top: 0
});
} else if (selectedIdx === resultEls.length - 1) {
contentEl.scrollTo({
top: contentEl.scrollHeight
});
} else if (selectedEl) {
selectedEl.scrollIntoView({
block: 'nearest'
});
}
});
}
if (event.key === 'Escape') {
event.preventDefault();
popSubPanel();
}
}
$: {
options;
selected = 0;
}
const commandCenterCtx = getCommandCenterCtx();
let cardEl: HTMLElement;
type Group = { type: 'group'; name: string };
type IndexedOption = Option & { index: number };
function isGroup(item: Option | Group): item is Group {
return !!item && 'type' in item && item.type === 'group';
}
const getGroupsAndOptions = (options: Option[]) => {
if (!options) return null;
const groupedOptions = new Map<string, Option[]>();
groupedOptions.set('ungrouped', []);
for (const option of options) {
if (!option.group) {
groupedOptions.set('ungrouped', [...groupedOptions.get('ungrouped'), option]);
} else {
groupedOptions.set(option.group, [
...(groupedOptions.get(option.group) || []),
option
]);
}
}
// Organize groups
const sortedOptions = [...groupedOptions.entries()]
.sort(([a], [b]) => {
const aRank = $commandGroupRanks[a] || 0;
const bRank = $commandGroupRanks[b] || 0;
if (aRank < bRank) {
return 1;
} else if (aRank > bRank) {
return -1;
} else {
return 0;
}
})
.map(([groupName, options]) => {
return [
groupName,
options.sort((a, b) => {
const aRank = a.rank || 0;
const bRank = b.rank || 0;
if (aRank < bRank) {
return 1;
} else if (aRank > bRank) {
return -1;
} else {
return 0;
}
})
] as [CommandGroup, Option[]];
});
// return a flat array of groups and indexed options
let optionIndex = 0;
const groupsAndOptions: (Group | IndexedOption)[] = [];
for (const [groupName, options] of sortedOptions) {
if (groupName !== 'ungrouped') {
groupsAndOptions.push({ type: 'group', name: groupName });
}
for (const option of options) {
groupsAndOptions.push({ ...option, index: optionIndex++ });
}
}
return groupsAndOptions;
};
$: groupsAndOptions = getGroupsAndOptions(options);
const getOptionClickHandler = (option: IndexedOption) => () => {
triggerOption(option);
};
const getOptionFocusHandler = (option: IndexedOption) => () => {
selected = option.index;
};
const castOption = (option: IndexedOption) => option as Option;
$: breadcrumbs = $subPanels.filter((panel) => panel.name !== 'root').map((panel) => panel.name);
const handleCrumbClick = (index: number) => {
if (index === breadcrumbs.length - 1) {
popSubPanel();
} else {
const numCrumbsToPop = breadcrumbs.length - index - 1;
for (let i = 0; i < numCrumbsToPop; i++) {
popSubPanel();
}
}
};
const isFirstNested = (index: number) => {
const prevItem = groupsAndOptions[index - 1];
const item = groupsAndOptions[index];
if (isGroup(item)) return false;
if (!item.nested) return false;
return !isGroup(prevItem) && !prevItem?.nested;
};
const isLastNested = (index: number) => {
const nextItem = groupsAndOptions[index + 1];
const item = groupsAndOptions[index];
if (isGroup(item)) return false;
if (!item.nested) return false;
return isGroup(nextItem) || !nextItem?.nested;
};
</script>
<svelte:window on:keydown={handleKeyDown} />
<div
class="card"
class:fullheight
bind:this={cardEl}
class:press={!$commandCenterCtx.isInitialPanel}
class:scale-up={$commandCenterCtx.isInitialPanel && $commandCenterCtx.open}>
<div class="search-wrapper">
{#each breadcrumbs as crumb, i}
{@const isLast = i === breadcrumbs.length - 1}
<button class="crumb" on:click={() => handleCrumbClick(i)}>
<span>{crumb}</span>
<i class="icon-x" />
</button>
{#if !isLast}
<span style="opacity: 50%">/</span>
{/if}
{/each}
<slot name="search">
<div class="u-flex default-search u-width-full-line">
<!-- svelte-ignore a11y-autofocus -->
<input type="text" placeholder={searchPlaceholder} autofocus bind:value={search} />
</div>
</slot>
</div>
<div class="content" bind:this={contentEl}>
{#if groupsAndOptions}
<ul class="options">
{#each groupsAndOptions as item, i}
{@const isSelected = !isGroup(item) && item.index === selected}
{#if isGroup(item)}
<li class="group eyebrow-heading-3">
{item.name}
</li>
{:else}
<li
class="result"
data-selected={isSelected ? true : undefined}
class:nested={item.nested}
class:first-nested={isFirstNested(i)}
class:last-nested={isLastNested(i)}>
{#if isSelected}
<div class="bg" />
{/if}
<button
class="option"
on:click={getOptionClickHandler(item)}
on:mouseover={getOptionFocusHandler(item)}
on:focus={getOptionFocusHandler(item)}>
<slot name="option" option={castOption(item)}>
<div class="u-flex u-gap-8 u-cross-center">
<i class="icon-{item.icon ?? 'arrow-sm-right'}" />
<span>
{item.label}
</span>
</div>
</slot>
</button>
</li>
{/if}
{:else}
<li class="result">
<slot name="no-options">
<span class="text">No options found</span>
</slot>
</li>
{/each}
</ul>
{:else}
<slot />
{/if}
</div>
<div class="footer">
<slot name="footer">
<div class=" u-flex u-flex u-cross-center u-main-space-between">
<div class="u-flex u-cross-center u-gap-4">
<kbd class="kbd">Enter</kbd> <span>to select</span>
</div>
<div class="u-flex u-cross-center u-gap-4">
<kbd class="kbd">Esc</kbd>
<span>to {$subPanels.length > 1 ? 'go back' : 'close'}</span>
</div>
</div>
</slot>
</div>
</div>
<style lang="scss">
// Animations
@keyframes press {
0% {
scale: 1;
}
30% {
scale: 0.985;
}
100% {
scale: 1;
}
}
.press {
animation: press 250ms ease;
}
@keyframes scale-up {
0% {
scale: 0.95;
}
100% {
scale: 1;
}
}
.scale-up {
animation: scale-up 150ms cubic-bezier(0.5, 1, 0.89, 1);
}
// Theme
:global(.theme-light) .card {
--cmd-center-bg: hsl(var(--color-neutral-0));
--cmd-center-border: hsl(var(--color-neutral-10));
--cmd-center-shadow: 0px 16px 32px 0px rgba(55, 59, 77, 0.04);
--kbd-bg: hsl(var(--color-neutral-30));
--crumb-bg: hsl(var(--color-neutral-10));
--crumb-color: hsl(var(--color-neutral-100));
--result-bg: hsl(var(--color-neutral-10));
--footer-bg: linear-gradient(180deg, #fff 0%, #e8e9f0 100%);
}
:global(.theme-dark) .card {
--cmd-center-bg: rgba(27, 27, 40, 0.8);
--cmd-center-border: hsl(var(--color-neutral-150));
--cmd-center-shadow: 0px 16px 32px 0px #14141f;
--kbd-bg: hsl(var(--color-neutral-150));
--crumb-bg: hsl(var(--color-neutral-150));
--crumb-color: hsl(var(--color-neutral-30));
--result-bg: hsl(var(--color-neutral-200));
--footer-bg: linear-gradient(180deg, #1b1b28 0%, #282a3b 100%);
}
// Elements
.card {
display: flex;
flex-direction: column;
width: var(--command-panel-width, 42.5rem);
max-width: 100%;
max-height: var(--command-panel-max-height, 32rem);
overflow: hidden;
padding: 0;
position: absolute;
top: clamp(128px, 15vh, 400px);
left: 50%;
translate: -50%;
border-radius: 0.5rem;
border: 1px solid var(--cmd-center-border);
background: var(--cmd-center-bg);
box-shadow: var(--cmd-center-shadow);
backdrop-filter: blur(6px);
&.fullheight {
height: var(--command-panel-max-height, 32rem);
}
:global(.kbd) {
background-color: var(--kbd-bg);
padding-inline: 0.25rem;
}
}
.search-wrapper {
display: flex;
gap: 0.25rem;
align-items: center;
width: 100%;
border-bottom: 1px solid hsl(var(--color-border));
font-size: 16px;
padding: 1rem;
.default-search {
input {
margin: -1rem;
padding: 1rem;
border: none;
background-color: transparent;
}
}
}
.crumb {
display: flex;
padding: 0.09375rem 0.25rem;
align-items: center;
gap: 0.25rem;
border-radius: 0.25rem;
background: var(--crumb-bg);
color: var(--crumb-color);
text-align: center;
font-family: Inter;
font-size: 0.75rem;
font-style: normal;
font-weight: 400;
line-height: normal;
white-space: nowrap;
&:hover {
opacity: 0.75;
}
i {
font-size: 10px;
}
}
.content {
overflow-y: auto;
flex-grow: 1;
.options {
padding: 1rem;
.group {
color: hsl(var(--color-neutral-70));
margin-inline-start: 0.25rem;
margin-block-end: 0.25rem;
position: relative;
z-index: 10;
&:not(:first-child) {
margin-block-start: 1rem;
}
}
.result {
position: relative;
.bg {
position: absolute;
inset: 0;
background-color: var(--result-bg);
border-radius: 0.75rem;
translate: 0 -1px;
}
.option {
padding: 0.5rem 0.75rem;
position: relative;
z-index: 10;
width: 100%;
box-shadow: none !important;
}
&.nested {
margin-left: 30px;
&.first-nested::before {
top: 8px;
}
&.last-nested::before {
height: calc(100% - 8px);
}
&::before {
content: '';
position: absolute;
left: -8px;
border-left: 1px solid hsl(var(--color-border));
height: 100%;
}
}
}
}
}
.footer {
background: var(--footer-bg);
border-top: 1px solid hsl(var(--color-border));
padding: 0.5rem 1rem;
}
</style>
@@ -0,0 +1,9 @@
<script lang="ts">
import { initSearcher } from '../commands';
import { userSearcher } from '../searchers';
import Template from './template.svelte';
const { search, results } = initSearcher(userSearcher);
</script>
<Template options={$results} bind:search={$search} />
@@ -0,0 +1,81 @@
import { goto } from '$app/navigation';
import { sdk } from '$lib/stores/sdk';
import { project } from '$routes/console/project-[project]/store';
import { Query, type Models } from '@appwrite.io/console';
import { get } from 'svelte/store';
import type { Command, Searcher } from '../commands';
import { addSubPanel } from '../subPanels';
import { FilesPanel } from '../panels';
const getBucketCommand = (bucket: Models.Bucket, projectId: string) => {
return {
label: `${bucket.name}`,
callback() {
goto(`/console/project-${projectId}/storage/bucket-${bucket.$id}`);
},
group: 'buckets',
icon: 'folder'
} satisfies Command;
};
export const bucketSearcher = (async (query: string) => {
const { buckets } = await sdk.forProject.storage.listBuckets([Query.orderDesc('$createdAt')]);
const $project = get(project);
const filtered = buckets.filter((bucket) => bucket.name.includes(query));
if (filtered.length === 1) {
const bucket = filtered[0];
return [
getBucketCommand(bucket, $project.$id),
{
label: 'Find files',
async callback() {
await goto(`/console/project-${$project.$id}/storage/bucket-${bucket.$id}`);
addSubPanel(FilesPanel);
},
group: 'buckets',
nested: true,
icon: 'search',
keepOpen: true
},
{
label: 'Permissions',
async callback() {
await goto(
`/console/project-${$project.$id}/storage/bucket-${bucket.$id}/settings#permissions`
);
scrollBy({ top: -100 });
},
group: 'buckets',
nested: true,
icon: 'key'
},
{
label: 'Extensions',
async callback() {
await goto(
`/console/project-${$project.$id}/storage/bucket-${bucket.$id}/settings#extensions`
);
},
group: 'buckets',
nested: true,
icon: 'puzzle'
},
{
label: 'File Security',
async callback() {
await goto(
`/console/project-${$project.$id}/storage/bucket-${bucket.$id}/settings#file-security`
);
scrollBy({ top: -100 });
},
group: 'buckets',
nested: true,
icon: 'lock-closed'
}
];
}
return filtered.map((bucket) => getBucketCommand(bucket, $project.$id));
}) satisfies Searcher;
@@ -0,0 +1,27 @@
import { goto } from '$app/navigation';
import { database } from '$routes/console/project-[project]/databases/database-[database]/store';
import { project } from '$routes/console/project-[project]/store';
import { get } from 'svelte/store';
import type { Searcher } from '../commands';
import { sdk } from '$lib/stores/sdk';
export const collectionsSearcher = (async (query: string) => {
const databaseId = get(database).$id;
const { collections } = await sdk.forProject.databases.listCollections(databaseId);
const projectId = get(project).$id;
return collections
.filter((col) => col.name.toLowerCase().includes(query.toLowerCase()))
.map(
(col) =>
({
group: 'collections',
label: col.name,
callback: () => {
goto(
`/console/project-${projectId}/databases/database-${databaseId}/collection-${col.$id}`
);
}
} as const)
);
}) satisfies Searcher;
@@ -0,0 +1,23 @@
import { goto } from '$app/navigation';
import { project } from '$routes/console/project-[project]/store';
import { get } from 'svelte/store';
import type { Searcher } from '../commands';
import { sdk } from '$lib/stores/sdk';
export const dbSearcher = (async (query: string) => {
const { databases } = await sdk.forProject.databases.list();
return databases
.filter((db) => db.name.toLowerCase().includes(query.toLowerCase()))
.map(
(db) =>
({
group: 'databases',
label: db.name,
callback: () => {
goto(`/console/project-${get(project).$id}/databases/database-${db.$id}`);
},
icon: 'database'
} as const)
);
}) satisfies Searcher;
+27
View File
@@ -0,0 +1,27 @@
import { sdk } from '$lib/stores/sdk';
import { get } from 'svelte/store';
import type { Searcher } from '../commands';
import { bucket } from '$routes/console/project-[project]/storage/bucket-[bucket]/store';
import { Query } from '@appwrite.io/console';
import { goto } from '$app/navigation';
import { project } from '$routes/console/project-[project]/store';
export const fileSearcher = (async (query: string) => {
const $bucket = get(bucket);
const $project = get(project);
const { files } = await sdk.forProject.storage.listFiles(
$bucket.$id,
[Query.orderDesc('')],
query || undefined
);
return files.map((file) => ({
label: file.name,
callback: () => {
goto(`/console/project-${$project.$id}/storage/bucket-${$bucket.$id}/file-${file.$id}`);
},
icon: 'document',
group: 'files'
}));
}) satisfies Searcher;
@@ -0,0 +1,81 @@
import { goto } from '$app/navigation';
import { sdk } from '$lib/stores/sdk';
import { project } from '$routes/console/project-[project]/store';
import { get } from 'svelte/store';
import type { Searcher } from '../commands';
import type { Models } from '@appwrite.io/console';
import { page } from '$app/stores';
import { showCreateDeployment } from '$routes/console/project-[project]/functions/function-[function]/+page.svelte';
const getFunctionCommand = (fn: Models.Function, projectId: string) => {
return {
label: fn.name,
callback: () => {
goto(`/console/project-${projectId}/functions/function-${fn.$id}`);
},
group: 'functions',
icon: 'lightning-bolt'
} as const;
};
export const functionsSearcher = (async (query: string) => {
const { functions } = await sdk.forProject.functions.list();
const projectId = get(project).$id;
const filtered = functions.filter((fn) => fn.name.toLowerCase().includes(query.toLowerCase()));
if (filtered.length === 1) {
const func = filtered[0];
return [
getFunctionCommand(func, projectId),
{
label: 'Create deployment',
nested: true,
async callback() {
const $page = get(page);
if (!$page.url.pathname.endsWith(func.$id)) {
await goto(`/console/project-${projectId}/functions/function-${func.$id}`);
}
showCreateDeployment();
},
group: 'functions',
icon: 'plus'
},
{
label: 'Go to deployments',
nested: true,
callback() {
goto(`/console/project-${projectId}/functions/function-${func.$id}`);
},
group: 'functions'
},
{
label: 'Go to usage',
nested: true,
callback() {
goto(`/console/project-${projectId}/functions/function-${func.$id}/usage`);
},
group: 'functions'
},
{
label: 'Go to executions',
nested: true,
callback() {
goto(`/console/project-${projectId}/functions/function-${func.$id}/executions`);
},
group: 'functions'
},
{
label: 'Go to settings',
nested: true,
callback() {
goto(`/console/project-${projectId}/functions/function-${func.$id}/settings`);
},
group: 'functions'
}
];
}
return filtered.map((fn) => getFunctionCommand(fn, projectId));
}) satisfies Searcher;
+9
View File
@@ -0,0 +1,9 @@
export * from './databases';
export * from './users';
export * from './organizations';
export * from './collections';
export * from './projects';
export * from './teams';
export * from './functions';
export * from './buckets';
export * from './files';
@@ -0,0 +1,18 @@
import { goto } from '$app/navigation';
import { sdk } from '$lib/stores/sdk';
import type { Searcher } from '../commands';
export const orgSearcher = (async (query: string) => {
const { teams } = await sdk.forConsole.teams.list();
return teams
.filter((organization) => organization.name.toLowerCase().includes(query.toLowerCase()))
.map((organization) => {
return {
label: organization.name,
callback: () => {
goto(`/console/organization-${organization.$id}`);
},
group: 'organizations'
} as const;
});
}) satisfies Searcher;
@@ -0,0 +1,25 @@
import { goto } from '$app/navigation';
import { organization } from '$lib/stores/organization';
import { sdk } from '$lib/stores/sdk';
import { Query } from '@appwrite.io/console';
import { get } from 'svelte/store';
import type { Searcher } from '../commands';
export const projectsSearcher = (async (query: string) => {
const { projects } = await sdk.forConsole.projects.list([
Query.equal('teamId', get(organization).$id),
Query.orderDesc('')
]);
return projects
.filter((project) => project.name.toLowerCase().includes(query.toLowerCase()))
.map((project) => {
return {
label: project.name,
callback: () => {
goto(`/console/project-${project.$id}`);
},
group: 'projects'
} as const;
});
}) satisfies Searcher;
+45
View File
@@ -0,0 +1,45 @@
import { goto } from '$app/navigation';
import { sdk } from '$lib/stores/sdk';
import { project } from '$routes/console/project-[project]/store';
import { get } from 'svelte/store';
import type { Command, Searcher } from '../commands';
import type { Models } from '@appwrite.io/console';
const getTeamCommand = (team: Models.Team<Models.Preferences>, projectId: string) =>
({
label: team.name,
callback: () => {
goto(`/console/project-${projectId}/auth/teams/team-${team.$id}`);
},
group: 'teams',
icon: 'user-circle'
} satisfies Command);
export const teamSearcher = (async (query: string) => {
const { teams } = await sdk.forProject.teams.list([], query);
const projectId = get(project).$id;
if (teams.length === 1) {
return [
getTeamCommand(teams[0], projectId),
{
label: 'Go to members',
callback: () => {
goto(`/console/project-${projectId}/auth/teams/team-${teams[0].$id}/members`);
},
group: 'teams',
nested: true
},
{
label: 'Go to activity',
callback: () => {
goto(`/console/project-${projectId}/auth/teams/team-${teams[0].$id}/activity`);
},
group: 'teams',
nested: true
}
];
}
return teams.map((team) => getTeamCommand(team, projectId));
}) satisfies Searcher;
+62
View File
@@ -0,0 +1,62 @@
import { goto } from '$app/navigation';
import { sdk } from '$lib/stores/sdk';
import { project } from '$routes/console/project-[project]/store';
import { get } from 'svelte/store';
import type { Command, Searcher } from '../commands';
import type { Models } from '@appwrite.io/console';
import { promptDeleteUser } from '$routes/console/project-[project]/auth/user-[user]/dangerZone.svelte';
const getUserCommand = (user: Models.User<Models.Preferences>, projectId: string) =>
({
label: user.name,
callback: () => {
goto(`/console/project-${projectId}/auth/user-${user.$id}`);
},
group: 'users',
icon: 'user-circle'
} satisfies Command);
export const userSearcher = (async (query: string) => {
const { users } = await sdk.forProject.users.list([], query || undefined);
const projectId = get(project).$id;
if (users.length === 1) {
return [
getUserCommand(users[0], projectId),
{
label: 'Delete user',
callback: () => {
promptDeleteUser(users[0].$id);
},
group: 'users',
nested: true,
icon: 'trash'
},
{
label: 'Go to activity',
callback: () => {
goto(`/console/project-${projectId}/auth/user-${users[0].$id}/activity`);
},
group: 'users',
nested: true
},
{
label: 'Go to sessions',
callback: () => {
goto(`/console/project-${projectId}/auth/user-${users[0].$id}/sessions`);
},
group: 'users',
nested: true
},
{
label: 'Go to memberships',
callback: () => {
goto(`/console/project-${projectId}/auth/user-${users[0].$id}/memberships`);
},
group: 'users',
nested: true
}
];
}
return users.map((user) => getUserCommand(user, projectId));
}) satisfies Searcher;
+31
View File
@@ -0,0 +1,31 @@
import type { SvelteComponentDev } from 'svelte/internal';
import { writable } from 'svelte/store';
export type SubPanel = {
name: string;
component: typeof SvelteComponentDev;
};
type CastSubPanel = Omit<SubPanel, 'component'> & {
component: unknown;
};
export const subPanels = writable<Array<SubPanel>>([]);
export function addSubPanel(subPanel: CastSubPanel) {
subPanels.update((curr) => {
curr.push(subPanel as SubPanel);
return curr;
});
}
export function popSubPanel() {
subPanels.update((curr) => {
curr = curr.slice(0, -1);
return curr;
});
}
export function clearSubPanels() {
subPanels.set([]);
}
+1
View File
@@ -5,6 +5,7 @@
export let isInline = false;
export let dismissible = false;
export let type: 'info' | 'success' | 'warning' | 'error' = 'info';
export let standalone = false;
export let buttons: Buttons[] = [];
export let isAction = false;
export let isStandalone = false;
+21
View File
@@ -0,0 +1,21 @@
<script lang="ts">
export let direction: 'up' | 'down' | 'left' | 'right' = 'right';
$: angle = {
down: 0,
left: 90,
up: 180,
right: 270
}[direction];
</script>
<svg
width="16"
height="17"
viewBox="0 0 16 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
style:transform="rotate({angle}deg)">
<path
d="M7.64645 15.8622C7.84171 16.0574 8.15829 16.0574 8.35355 15.8622L11.5355 12.6802C11.7308 12.4849 11.7308 12.1683 11.5355 11.9731C11.3403 11.7778 11.0237 11.7778 10.8284 11.9731L8 14.8015L5.17157 11.9731C4.97631 11.7778 4.65973 11.7778 4.46447 11.9731C4.2692 12.1683 4.2692 12.4849 4.46447 12.6802L7.64645 15.8622ZM7.5 1V15.5086H8.5V1H7.5Z"
fill="#C4C6D7" />
</svg>
+1 -1
View File
@@ -1,4 +1,4 @@
<script>
<script lang="ts">
import { Card } from './';
export let danger = false;
+9 -1
View File
@@ -1,3 +1,12 @@
<script lang="ts" context="module">
const langArr = ['js', 'html', 'dart', 'kotlin', 'json', 'sh', 'yml', 'swift'] as const;
export type Language = (typeof langArr)[number];
export function isLanguage(str: string): str is Language {
return langArr.includes(str as Language);
}
</script>
<script lang="ts">
import { Pill } from '$lib/elements';
import Prism from 'prismjs';
@@ -47,7 +56,6 @@
</Copy>
{/if}
</div>
<pre class={`language-${language}`} class:line-numbers={withLineNumbers}><code
>{code}</code></pre>
</section>
+12 -2
View File
@@ -79,16 +79,26 @@
event.target === element ||
element.contains(event.target as Node) ||
event.target === tooltip ||
tooltip.contains(event.target as Node)
tooltip.contains(event.target as Node) ||
// Avoid deleted elements triggering blur
!document.body.contains(event.target as Node)
)
) {
show = false;
dispatch('blur');
}
};
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape' && show) {
event.preventDefault();
show = false;
dispatch('blur');
}
};
</script>
<svelte:window on:click={onBlur} />
<svelte:window on:click={onBlur} on:keydown={onKeyDown} />
<div class:drop-wrapper={!noStyle} class:u-cross-child-start={childStart} bind:this={element}>
<slot />
+21
View File
@@ -0,0 +1,21 @@
<script lang="ts">
type NumSize = 1 | 2 | 3;
type Size = NumSize | `${NumSize}`;
export let tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
export let size: Size;
export let trimmed = true;
export let id: string = null;
export let style: string = undefined;
let classes: string = undefined;
export { classes as class };
</script>
<svelte:element
this={tag}
class="eyebrow-heading-{size} {classes}"
class:u-trim-1={trimmed}
{id}
{style}>
<slot />
</svelte:element>
@@ -0,0 +1,37 @@
<script lang="ts">
import { portal } from '$lib/actions/portal';
import { fly } from 'svelte/transition';
export let show = false;
</script>
{#if show}
<div class="floating-action-bar" transition:fly|local={{ y: '6rem' }} use:portal>
<slot />
</div>
{/if}
<style lang="scss">
.floating-action-bar {
position: fixed;
bottom: 2rem;
left: 50%;
transform: translateX(-50%);
z-index: 100;
border-radius: 0.5rem;
padding: 0.75rem 1rem;
}
:global(.theme-dark) .floating-action-bar {
border: 1px solid hsl(var(--color-neutral-200));
background: hsl(var(--color-neutral-300));
box-shadow: 0px 6px 16px 8px #14141f;
}
:global(.theme-light) .floating-action-bar {
border: 1px solid hsl(var(--color-neutral-30));
background: hsl(var(--color-neutral-0));
box-shadow: 0px 6px 16px 0px rgba(55, 59, 77, 0.14);
}
</style>
+4 -1
View File
@@ -1,6 +1,9 @@
<script lang="ts">
type NumSize = 1 | 2 | 3 | 4 | 5 | 6 | 7;
type Size = NumSize | `${NumSize}`;
export let tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
export let size: '1' | '2' | '3' | '4' | '5' | '6' | '7';
export let size: Size;
export let trimmed = true;
export let id: string = null;
</script>
+5
View File
@@ -1,3 +1,4 @@
export { default as Arrow } from './arrow.svelte';
export { default as Modal } from './modal.svelte';
export { default as Pagination } from './pagination.svelte';
export { default as PaginationInline } from './paginationInline.svelte';
@@ -53,3 +54,7 @@ export { default as ClickableListItem } from './clickableListItem.svelte';
export { default as Id } from './id.svelte';
export { default as NumericList } from './numericList.svelte';
export { default as NumericListItem } from './numericListItem.svelte';
export { default as EyebrowHeading } from './eyebrowHeading.svelte';
export { default as SvgIcon } from './svgIcon.svelte';
export { default as MigrationBox } from './migrationBox.svelte';
export { default as FloatingActionBar } from './floatingActionBar.svelte';
+101
View File
@@ -0,0 +1,101 @@
<script lang="ts" context="module">
import { last } from '$lib/helpers/array';
import { debounce } from '$lib/helpers/debounce';
import { parseIfString } from '$lib/helpers/object';
import { sdk } from '$lib/stores/sdk';
import type { Models } from '@appwrite.io/console';
import { onMount } from 'svelte';
import { writable } from 'svelte/store';
export const showMigrationBox = writable(false);
</script>
<script lang="ts">
let migration: Models.Migration;
type Counter = {
pending: number;
error: number;
success: number;
processing: number;
skip: number;
warning: number;
};
type TotalCounter = {
done: number;
processing: number;
};
$: percentage = (function getPercentage() {
if (!migration) return 0;
if (migration.status === 'failed') return 100;
if (migration.status === 'completed') return 100;
const statusCounters = parseIfString(migration.statusCounters) as Record<string, Counter>;
const totalCounter: TotalCounter = Object.values(statusCounters).reduce(
(curr, acc) => {
return {
done: curr.done + acc.success + acc.error + acc.skip + acc.warning,
processing: curr.processing + acc.processing + acc.pending
};
},
{ done: 0, processing: 0 } as TotalCounter
);
const res = Math.round(
(totalCounter.done / (totalCounter.done + totalCounter.processing)) * 100
);
return Number.isNaN(res) ? 0 : res;
})();
const fetchMigrations = debounce(async () => {
const { migrations } = await sdk.forProject.migrations.list();
migration = last(migrations);
}, 1000);
fetchMigrations();
onMount(async () => {
return sdk.forConsole.client.subscribe(['project', 'console'], async (response) => {
if (response.events.includes('migrations.*')) {
fetchMigrations();
}
});
});
</script>
{#if $showMigrationBox && migration}
<section class="upload-box is-float">
<header class="upload-box-header">
<h4 class="upload-box-title">
<span class="text">Importing Data</span>
</h4>
<button
class="upload-box-button"
aria-label="close migration box"
on:click={() => ($showMigrationBox = false)}>
<span class="icon-x" aria-hidden="true" />
</button>
</header>
<div class="upload-box-content is-open">
<section class="progress-bar">
<div class="progress-bar-top-line u-flex u-gap-8 u-main-space-between">
<span>{percentage}%</span>
</div>
<div
class="progress-bar-container"
class:is-danger={migration.status === 'failed'}
style="--graph-size:{percentage}%" />
</section>
</div>
</section>
{/if}
<style>
.upload-box-content {
padding: 1.5rem;
min-width: 400px;
max-width: 100vw;
}
</style>
+4 -1
View File
@@ -3,6 +3,7 @@
import { Alert } from '$lib/components';
import { trackEvent } from '$lib/actions/analytics';
import { Form } from '$lib/elements/forms';
import { disableCommands } from '$lib/commandCenter';
export let show = false;
export let size: 'small' | 'big' = null;
@@ -11,7 +12,7 @@
export let error: string = null;
export let closable = true;
export let headerDivider = true;
export let onSubmit: () => Promise<void> | void = function () {
export let onSubmit: (e: SubmitEvent) => Promise<void> | void = function () {
return;
};
@@ -70,6 +71,8 @@
closeModal();
}
$: $disableCommands(show);
$: if (error) {
alert?.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' });
}
+10
View File
@@ -0,0 +1,10 @@
<script lang="ts">
import { iconPath } from '$lib/stores/app';
export let name: string;
export let type: 'color' | 'grayscale' = 'color';
let className = '';
export { className as class };
</script>
<img class={className} src={$iconPath(name, type)} alt={name} />
+2 -1
View File
@@ -23,6 +23,7 @@
export let hideView = false;
export let hideColumns = false;
export let allowNoColumns = false;
export let showColsTextMobile = false;
let showSelectColumns = false;
@@ -84,7 +85,7 @@
class="icon-view-boards u-opacity-50"
aria-hidden="true"
aria-label="columns" />
<span class="text">Columns</span>
<span class="text {showColsTextMobile ? '' : 'is-only-desktop'}">Columns</span>
<span class="inline-tag">{selectedColumnsNumber}</span>
</Button>
<svelte:fragment slot="list">
+13 -1
View File
@@ -38,7 +38,9 @@ export enum Dependencies {
KEYS = 'dependency:keys',
DOMAINS = 'dependency:domains',
WEBHOOK = 'dependency:webhook',
WEBHOOKS = 'dependency:webhooks'
WEBHOOKS = 'dependency:webhooks',
MIGRATIONS = 'dependency:migrations',
COLLECTIONS = 'dependency:collections'
}
export const scopes: {
@@ -173,6 +175,16 @@ export const scopes: {
scope: 'health.read',
description: "Access to read your project's health status",
category: 'Other'
},
{
scope: 'migrations.read',
description: "Access to read your project's migration status",
category: 'Other'
},
{
scope: 'migrations.write',
description: 'Access to create migrations',
category: 'Other'
}
];
+21 -19
View File
@@ -17,6 +17,8 @@
export let ariaLabel: string = null;
export let noMargin = false;
export let event: string = null;
let classes: string = undefined;
export { classes as class };
const isSubmitting = hasContext('form')
? getContext<FormContext>('form').isSubmitting
@@ -33,6 +35,21 @@
from: 'button'
});
}
$: resolvedClasses = [
'button',
disabled && 'is-disabled',
round && 'is-only-icon',
secondary && 'is-secondary',
github && 'is-github',
text && 'is-text',
danger && 'is-danger',
fullWidth && 'is-full-width',
noMargin && 'u-padding-inline-0',
classes
]
.filter(Boolean)
.join(' ');
</script>
{#if href}
@@ -41,15 +58,7 @@
{href}
target={external ? '_blank' : ''}
rel={external ? 'noopener noreferrer' : ''}
class="button"
class:is-disabled={disabled}
class:is-only-icon={round}
class:is-secondary={secondary}
class:is-github={github}
class:is-text={text}
class:is-danger={danger}
class:is-full-width={fullWidth}
class:u-padding-inline-0={noMargin}
class={resolvedClasses}
aria-label={ariaLabel}>
<slot />
</a>
@@ -58,16 +67,9 @@
on:click
on:click={track}
disabled={internalDisabled}
class="button"
class:is-only-icon={round}
class:is-secondary={secondary}
class:is-github={github}
class:is-danger={danger}
class:is-text={text}
class:is-full-width={fullWidth}
class:u-padding-inline-0={noMargin}
type={submit ? 'submit' : 'button'}
aria-label={ariaLabel}>
class={resolvedClasses}
aria-label={ariaLabel}
type={submit === false ? 'button' : undefined}>
<slot />
</button>
{/if}
+3 -3
View File
@@ -13,15 +13,15 @@
export let noMargin = false;
export let noStyle = false;
export let isModal = false;
export let onSubmit: () => Promise<void> | void;
export let onSubmit: (e: SubmitEvent) => Promise<void> | void;
const { isSubmitting } = setContext<FormContext>('form', {
isSubmitting: writable(false)
});
async function submit() {
async function submit(e: SubmitEvent) {
isSubmitting.set(true);
await onSubmit();
await onSubmit(e);
isSubmitting.set(false);
}
</script>
+3 -1
View File
@@ -1,10 +1,12 @@
<script lang="ts">
export let isCommonSection = false;
export let gap = 24;
let classes: string = undefined;
export { classes as class };
</script>
<ul
class="form-list"
class="form-list {classes}"
class:common-section={isCommonSection}
style={`--form-list-gap: ${gap / 16}rem;`}>
<slot />
+10 -5
View File
@@ -1,12 +1,13 @@
<script lang="ts">
import { FormItem, Helper, Label } from '.';
export let label: string;
export let label: string | undefined = undefined;
export let optionalText: string | undefined = undefined;
export let tooltip: string = null;
export let showLabel = true;
export let id: string;
export let value = false;
export let indeterminate = false;
export let required = false;
export let disabled = false;
@@ -28,19 +29,23 @@
</script>
<FormItem>
<Label {required} {tooltip} {optionalText} hide={!showLabel} for={id}>
{label}
</Label>
{#if label}
<Label {required} {tooltip} {optionalText} hide={!showLabel} for={id}>
{label}
</Label>
{/if}
<div class="input-text-wrapper">
<input
{id}
{disabled}
{required}
{indeterminate}
type="checkbox"
bind:this={element}
bind:checked={value}
on:invalid={handleInvalid} />
on:invalid={handleInvalid}
on:click />
</div>
{#if error}
<Helper type="warning">{error}</Helper>
+6 -4
View File
@@ -3,7 +3,7 @@
import { FormItem, Helper, Label } from '.';
import NullCheckbox from './nullCheckbox.svelte';
export let label: string;
export let label: string | undefined = undefined;
export let optionalText: string | undefined = undefined;
export let showLabel = true;
export let id: string;
@@ -65,9 +65,11 @@
</script>
<FormItem>
<Label {required} {optionalText} hide={!showLabel} for={id}>
{label}
</Label>
{#if label}
<Label {required} {optionalText} hide={!showLabel} for={id}>
{label}
</Label>
{/if}
<div class="input-text-wrapper">
<input
+6 -4
View File
@@ -2,7 +2,7 @@
import { FormItem, Helper, Label } from '.';
export let id: string;
export let label: string;
export let label: string | undefined = undefined;
export let optionalText: string | undefined = undefined;
export let showLabel = true;
export let value: string | number | boolean;
@@ -45,9 +45,11 @@
</script>
<FormItem>
<Label {required} {optionalText} hide={!showLabel} for={id}>
{label}
</Label>
{#if label}
<Label {required} {optionalText} hide={!showLabel} for={id}>
{label}
</Label>
{/if}
<div class="select">
<select
+19 -7
View File
@@ -4,10 +4,11 @@
import NullCheckbox from './nullCheckbox.svelte';
import TextCounter from './textCounter.svelte';
export let label: string;
export let label: string = undefined;
export let optionalText: string | undefined = undefined;
export let showLabel = true;
export let id: string;
export let name: string = id;
export let value = '';
export let placeholder = '';
export let required = false;
@@ -40,8 +41,11 @@
error = element.validationMessage;
};
$: if (value) {
error = null;
$: {
value;
if (element?.validity?.valid) {
error = null;
}
}
let prevValue = '';
@@ -57,16 +61,23 @@
$: showTextCounter = !!maxlength;
$: showNullCheckbox = nullable && !required;
type $$Events = {
input: Event & { target: HTMLInputElement };
};
</script>
<FormItem {fullWidth}>
<Label {required} {tooltip} {optionalText} hide={!showLabel} for={id}>
{label}
</Label>
{#if label}
<Label {required} {tooltip} {optionalText} hide={!showLabel} for={id}>
{label}
</Label>
{/if}
<div class="input-text-wrapper">
<input
{id}
{name}
{placeholder}
{disabled}
{readonly}
@@ -78,7 +89,8 @@
bind:value
class:u-padding-inline-end-56={typeof maxlength === 'number'}
bind:this={element}
on:invalid={handleInvalid} />
on:invalid={handleInvalid}
on:input />
{#if showTextCounter || showNullCheckbox}
<ul
class="buttons-list u-cross-center u-gap-8 u-position-absolute u-inset-block-start-8 u-inset-block-end-8 u-inset-inline-end-12">
@@ -8,6 +8,7 @@
export let label: string;
export let showLabel = true;
export let id: string;
export let name: string = id;
export let value = '';
export let placeholder = '';
export let required = false;
@@ -64,6 +65,7 @@
<div class="input-text-wrapper" on:click on:keyup={clickOnEnter}>
<textarea
{id}
{name}
{placeholder}
{disabled}
{readonly}
+29
View File
@@ -0,0 +1,29 @@
<script lang="ts">
import { toggle } from '$lib/helpers/array';
import { isHTMLInputElement } from '$lib/helpers/types';
import { TableCell } from '.';
import { InputCheckbox } from '../forms';
export let id: string;
export let selectedIds: string[] = [];
</script>
<TableCell>
<InputCheckbox
id="select-{id}"
value={selectedIds.includes(id)}
on:click={(e) => {
// Prevent the link from being followed
e.preventDefault();
const el = e.currentTarget;
if (!isHTMLInputElement(el)) return;
selectedIds = toggle(selectedIds, id);
// Hack to make sure the checkbox is checked, independent of the
// preventDefault() call above
window.setTimeout(() => {
el.checked = selectedIds.includes(id);
});
}} />
</TableCell>
@@ -0,0 +1,33 @@
<script lang="ts">
import { isHTMLInputElement } from '$lib/helpers/types';
import { TableCellHead } from '.';
import { InputCheckbox } from '../forms';
export let selected: string[] = [];
export let pageItemsIds: string[] = [];
function handleClick(e: MouseEvent) {
if (!isHTMLInputElement(e.target)) return;
if (e.target.checked) {
const set = new Set(selected);
pageItemsIds.forEach((id) => set.add(id));
selected = Array.from(set);
} else {
selected = selected.filter((id) => {
return !pageItemsIds.includes(id);
});
}
}
$: someSelected = pageItemsIds.some((id) => selected.includes(id));
$: allSelected = pageItemsIds.every((id) => selected.includes(id));
</script>
<TableCellHead width={10}>
<InputCheckbox
id="select-all"
indeterminate={someSelected && !allSelected}
value={allSelected}
on:click={handleClick} />
</TableCellHead>
+2
View File
@@ -9,6 +9,8 @@ export { default as TableRowLink } from './rowLink.svelte';
export { default as TableRowButton } from './rowButton.svelte';
export { default as TableCell } from './cell.svelte';
export { default as TableCellHead } from './cellHead.svelte';
export { default as TableCellHeadCheck } from './cellHeadCheck.svelte';
export { default as TableCellLink } from './cellLink.svelte';
export { default as TableCellAvatar } from './cellAvatar.svelte';
export { default as TableCellText } from './cellText.svelte';
export { default as TableCellCheck } from './cellCheck.svelte';
+20
View File
@@ -76,3 +76,23 @@ export function last<T>(array: readonly T[]): T;
export function last<T>(array: readonly T[]): T | undefined {
return at(array, -1);
}
export function excludeArray<T, E extends T>(
array: readonly T[],
exclude: readonly E[]
): Exclude<T, E>[] {
return array.filter((item) => !exclude.includes(item as E)) as Exclude<T, E>[];
}
// Function that, given an array, checks if it includes all the elements of another array
export function includesAll<T>(arr1: T[], arr2: T[]): boolean {
return arr2.every((elem) => arr1.includes(elem));
}
export function toggle<T>(arr: T[], elem: T): T[] {
if (arr.includes(elem)) {
return arr.filter((e) => e !== elem);
}
arr.push(elem);
return arr;
}
+20
View File
@@ -0,0 +1,20 @@
export function debounce<F extends (...params: any[]) => void>(fn: F, delay: number) {
let timeoutID: ReturnType<typeof setTimeout> = null;
const debounceFn = function (this: any, ...args: any[]) {
clearTimeout(timeoutID);
timeoutID = window.setTimeout(() => fn.apply(this, args), delay) as unknown as ReturnType<
typeof setTimeout
>;
} as F & { immediate: F; cancel: () => void };
debounceFn.immediate = ((...args: Parameters<F>) => {
clearTimeout(timeoutID);
return fn(args);
}) as F;
debounceFn.cancel = () => {
clearTimeout(timeoutID);
};
return debounceFn;
}
+14
View File
@@ -0,0 +1,14 @@
export function getFormData<T extends Record<string, unknown> = Record<string, unknown>>(
e: SubmitEvent
): T {
const form = e.target as HTMLFormElement;
const formData = new FormData(form);
const data = {};
formData.forEach((value, key) => {
data[key] = value;
});
// TODO: Add validation? Maybe with zod
return data as T;
}
+34
View File
@@ -36,3 +36,37 @@ export function deepEqual<T>(obj1: T, obj2: T): boolean {
return true;
}
export type DeepObj<T> = {
[K in keyof T]: T[K] extends object ? DeepObj<T[K]> : T[K];
};
export function deepMap<T, U>(
obj: DeepObj<T>,
fn: (value: T, key: string, parentKey?: string) => unknown,
parentKey?: string
): U {
const entries = objectEntries(obj);
const result = {} as U;
for (const [key, value] of entries) {
if (typeof value === 'object' && value !== null) {
result[key as any] = deepMap(value as any, fn, key as string) as any;
} else {
result[key as any] = fn(value as any, key as any, parentKey);
}
}
return result;
}
export function parseIfString(value: unknown): any {
if (typeof value === 'string') {
try {
return JSON.parse(value);
} catch {
return value;
}
}
return value;
}
+16
View File
@@ -0,0 +1,16 @@
import type { Writable } from 'svelte/store';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type DeepKeys<T extends Record<string, any>> = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[K in keyof T]: T[K] extends Record<string, any> ? `${K}.${DeepKeys<T[K]>}` : K;
}[keyof T];
export type WritableValue<T> = T extends Writable<infer U> ? U : never;
export function isHTMLElement(el: unknown): el is HTMLElement {
return el instanceof HTMLElement;
}
export function isHTMLInputElement(el: unknown): el is HTMLInputElement {
return el instanceof HTMLInputElement;
}
+31
View File
@@ -0,0 +1,31 @@
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="12" viewBox="0 0 18 12" fill="none">
<path
d="M6.15638 3.92787C6.14928 3.9455 6.05752 4.30532 5.95902 4.73224C5.85654 5.15904 5.69426 5.83283 5.60268 6.23135C5.4262 6.96159 5.32036 7.44133 5.32036 7.50475C5.32036 7.52232 5.42981 7.53659 5.56369 7.53659H5.80726L5.91647 7.04968C5.98008 6.78523 6.1213 6.1784 6.23406 5.7022C6.347 5.22601 6.48443 4.64746 6.53744 4.41454C6.59033 4.18173 6.64328 3.97029 6.65387 3.9455C6.66446 3.91379 6.60441 3.90332 6.42095 3.90332C6.28334 3.90332 6.16336 3.91379 6.15638 3.92787ZM4.262 5.4552L3.9375 5.80804L4.03287 5.92093C4.08564 5.98435 4.23047 6.14314 4.35383 6.27377L4.57965 6.51343H5.22174L4.91823 6.18544C4.75234 6.00926 4.61479 5.83975 4.61479 5.81869C4.61479 5.79396 4.74181 5.63535 4.89717 5.46591C5.05229 5.29322 5.17932 5.14484 5.17932 5.12727C5.17932 5.11306 5.04531 5.10253 4.88309 5.10253H4.59024L4.262 5.4552ZM6.73131 5.12371C6.73131 5.13437 6.79137 5.20122 6.86538 5.27535C7.14072 5.55051 7.33465 5.78343 7.324 5.8291C7.31708 5.85401 7.1829 6.01967 7.02068 6.19248L6.72794 6.51343H7.05594L7.38399 6.51L7.6837 6.18189C7.84972 5.99861 7.98372 5.83614 7.98372 5.81508C7.98372 5.79751 7.84262 5.63168 7.66614 5.44479L7.34891 5.10253H7.0418C6.86893 5.10253 6.73131 5.11306 6.73131 5.12371Z"
fill="url(#paint0_linear_598_58741)" fill-opacity="0.8" />
<mask id="path-2-inside-1_598_58741" fill="white">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M8.99832 11.1877C9.8811 11.6997 10.9066 11.9929 12.0006 11.9929C15.3102 11.9929 17.993 9.31022 17.993 6.00105C17.993 2.69188 15.3102 0.00927734 12.0006 0.00927734C10.9071 0.00927734 9.88211 0.302156 8.99959 0.813692C5.36865 -1.31465 0.583686 0.92861 0.0457609 5.26024C-0.218391 7.38758 0.67407 9.49392 2.38612 10.784C4.45312 12.3436 7.03283 12.3191 8.99832 11.1877ZM10.4211 8.08168C10.0732 8.82164 9.54502 9.46011 8.8932 9.94057C8.59484 10.1577 8.26409 10.3462 7.90173 10.4997C6.29455 11.1807 4.44597 10.9525 3.05258 9.901C0.0502737 7.63552 0.626354 2.96986 4.08969 1.50272C5.3555 0.966514 6.77101 0.994119 7.99592 1.54365C7.99533 1.54418 7.99473 1.54471 7.99414 1.54525C9.65279 2.28896 10.821 3.93029 10.8806 5.85103C10.902 6.61658 10.7459 7.38355 10.4211 8.08168Z" />
</mask>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M8.99832 11.1877C9.8811 11.6997 10.9066 11.9929 12.0006 11.9929C15.3102 11.9929 17.993 9.31022 17.993 6.00105C17.993 2.69188 15.3102 0.00927734 12.0006 0.00927734C10.9071 0.00927734 9.88211 0.302156 8.99959 0.813692C5.36865 -1.31465 0.583686 0.92861 0.0457609 5.26024C-0.218391 7.38758 0.67407 9.49392 2.38612 10.784C4.45312 12.3436 7.03283 12.3191 8.99832 11.1877ZM10.4211 8.08168C10.0732 8.82164 9.54502 9.46011 8.8932 9.94057C8.59484 10.1577 8.26409 10.3462 7.90173 10.4997C6.29455 11.1807 4.44597 10.9525 3.05258 9.901C0.0502737 7.63552 0.626354 2.96986 4.08969 1.50272C5.3555 0.966514 6.77101 0.994119 7.99592 1.54365C7.99533 1.54418 7.99473 1.54471 7.99414 1.54525C9.65279 2.28896 10.821 3.93029 10.8806 5.85103C10.902 6.61658 10.7459 7.38355 10.4211 8.08168Z"
fill="url(#paint1_linear_598_58741)" fill-opacity="0.8" />
<path
d="M8.99832 11.1877L9.09866 11.0147L8.9987 10.9567L8.89854 11.0143L8.99832 11.1877ZM8.99959 0.813692L8.89845 0.986235L8.99903 1.04519L9.09989 0.986726L8.99959 0.813692ZM0.0457609 5.26024L-0.152715 5.23559L-0.152715 5.2356L0.0457609 5.26024ZM2.38612 10.784L2.50659 10.6243L2.50648 10.6243L2.38612 10.784ZM8.8932 9.94057L9.01088 10.1023L9.01186 10.1016L8.8932 9.94057ZM10.4211 8.08168L10.6021 8.16678L10.6024 8.16604L10.4211 8.08168ZM7.90173 10.4997L7.82371 10.3155L7.8237 10.3156L7.90173 10.4997ZM3.05258 9.901L3.17305 9.74135L3.17305 9.74135L3.05258 9.901ZM4.08969 1.50272L4.01168 1.31856L4.01168 1.31856L4.08969 1.50272ZM7.99592 1.54365L8.12958 1.69243L8.35825 1.487L8.07778 1.36117L7.99592 1.54365ZM7.99414 1.54525L7.86039 1.39655L7.63195 1.60203L7.91231 1.72774L7.99414 1.54525ZM10.8806 5.85103L11.0806 5.84545L11.0805 5.84483L10.8806 5.85103ZM12.0006 11.7929C10.9428 11.7929 9.95179 11.5095 9.09866 11.0147L8.89798 11.3607C9.81041 11.8899 10.8704 12.1929 12.0006 12.1929V11.7929ZM17.793 6.00105C17.793 9.19975 15.1997 11.7929 12.0006 11.7929V12.1929C15.4206 12.1929 18.193 9.4207 18.193 6.00105H17.793ZM12.0006 0.209277C15.1998 0.209277 17.793 2.80236 17.793 6.00105H18.193C18.193 2.58141 15.4206 -0.190723 12.0006 -0.190723V0.209277ZM9.09989 0.986726C9.95276 0.492375 10.9433 0.209277 12.0006 0.209277V-0.190723C10.8709 -0.190723 9.81146 0.111937 8.8993 0.640658L9.09989 0.986726ZM9.10073 0.641149C5.34805 -1.55855 0.403246 0.758733 -0.152715 5.23559L0.244236 5.28489C0.764126 1.09849 5.38925 -1.07074 8.89845 0.986235L9.10073 0.641149ZM-0.152715 5.2356C-0.425684 7.43394 0.496568 9.61059 2.26576 10.9437L2.50648 10.6243C0.851572 9.37725 -0.011098 7.34121 0.244237 5.28489L-0.152715 5.2356ZM2.26566 10.9436C4.40191 12.5556 7.06895 12.529 9.0981 11.361L8.89854 11.0143C6.99671 12.1091 4.50432 12.1317 2.50659 10.6243L2.26566 10.9436ZM9.01186 10.1016C9.69018 9.60158 10.2399 8.93712 10.6021 8.16678L10.2401 7.99657C9.90647 8.70615 9.39987 9.31865 8.77453 9.77957L9.01186 10.1016ZM8.77552 9.77885C8.48954 9.98696 8.1721 10.168 7.82371 10.3155L7.97974 10.6839C8.35607 10.5244 8.70014 10.3284 9.01088 10.1023L8.77552 9.77885ZM7.8237 10.3156C6.28232 10.9686 4.5094 10.7498 3.17305 9.74135L2.93211 10.0606C4.38253 11.1551 6.30678 11.3927 7.97975 10.6839L7.8237 10.3156ZM3.17305 9.74135C0.293644 7.56861 0.846137 3.09396 4.16771 1.68687L4.01168 1.31856C0.406571 2.84575 -0.193096 7.70243 2.93211 10.0606L3.17305 9.74135ZM4.16771 1.68688C5.38167 1.17263 6.73925 1.19908 7.91405 1.72613L8.07778 1.36117C6.80276 0.78916 5.32933 0.760396 4.01168 1.31856L4.16771 1.68688ZM8.12789 1.69394C8.12849 1.69341 8.12868 1.69324 8.12958 1.69243L7.86226 1.39487C7.86198 1.39512 7.86098 1.39602 7.86039 1.39655L8.12789 1.69394ZM11.0805 5.84483C11.0185 3.845 9.80209 2.13672 8.07597 1.36275L7.91231 1.72774C9.50349 2.4412 10.6236 4.01559 10.6807 5.85723L11.0805 5.84483ZM10.6807 5.85661C10.7012 6.59148 10.5514 7.32762 10.2398 7.99731L10.6024 8.16604C10.9405 7.43947 11.1028 6.64168 11.0806 5.84545L10.6807 5.85661Z"
fill="url(#paint2_linear_598_58741)" mask="url(#path-2-inside-1_598_58741)" />
<defs>
<linearGradient id="paint0_linear_598_58741" x1="5.96061" y1="2.08669" x2="5.96061" y2="8.29351"
gradientUnits="userSpaceOnUse">
<stop stop-color="#F02E65" />
<stop offset="1" stop-color="#F02E65" stop-opacity="0.48" />
</linearGradient>
<linearGradient id="paint1_linear_598_58741" x1="12.9936" y1="-5.98253" x2="12.9936" y2="14.4895"
gradientUnits="userSpaceOnUse">
<stop stop-color="#F02E65" />
<stop offset="1" stop-color="#F02E65" stop-opacity="0.35" />
</linearGradient>
<linearGradient id="paint2_linear_598_58741" x1="23.5" y1="4.5" x2="-13" y2="13.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#F02E65" stop-opacity="0.57" />
<stop offset="1" stop-color="white" stop-opacity="0" />
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 6.9 KiB

+14 -9
View File
@@ -1,21 +1,23 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/stores';
import { Submit, trackEvent } from '$lib/actions/analytics';
import { toggleCommandCenter } from '$lib/commandCenter/commandCenter.svelte';
import { AvatarInitials, DropList, DropListItem, DropListLink } from '$lib/components';
import { app } from '$lib/stores/app';
import { user } from '$lib/stores/user';
import { organizationList, organization, newOrgModal } from '$lib/stores/organization';
import { Feedback } from '$lib/components/feedback';
import Button from '$lib/elements/forms/button.svelte';
import AppwriteLogo from '$lib/images/appwrite-gray-light.svg';
import DarkMode from '$lib/images/mode/dark-mode.svg';
import LightMode from '$lib/images/mode/light-mode.svg';
import SystemMode from '$lib/images/mode/system-mode.svg';
import { slide } from 'svelte/transition';
import { page } from '$app/stores';
import { Submit, trackEvent } from '$lib/actions/analytics';
import { sdk } from '$lib/stores/sdk';
import { goto } from '$app/navigation';
import { app } from '$lib/stores/app';
import { feedback } from '$lib/stores/feedback';
import { Feedback } from '$lib/components/feedback';
import { newOrgModal, organization, organizationList } from '$lib/stores/organization';
import { sdk } from '$lib/stores/sdk';
import { user } from '$lib/stores/user';
import { isCloud } from '$lib/system';
import { slide } from 'svelte/transition';
let showDropdown = false;
let droplistElement: HTMLDivElement;
@@ -93,6 +95,9 @@
class="button is-small is-text">
<span class="text">Support</span>
</a>
<Button text class="is-small" on:click={toggleCommandCenter}>
<i class="icon-search" />
</Button>
</nav>
<nav class="u-flex u-height-100-percent u-sep-inline-start">
{#if $user}
+5 -8
View File
@@ -135,22 +135,19 @@
Skip optional steps
</Button>
{/if}
{#if currentStep === 1}
<Button secondary on:click={handleExit}>Cancel</Button>
<Button submit>Next</Button>
{:else if isLastStep}
<Button
secondary
on:click={() => currentStep--}
on:click={() => trackEvent('wizard_back')}>Back</Button>
<Button submit>{finalAction}</Button>
{:else}
<Button
secondary
on:click={() => currentStep--}
on:click={() => trackEvent('wizard_back')}>Back</Button>
<Button submit>Next</Button>
{/if}
<Button submit disabled={$wizard.nextDisabled}>
{isLastStep ? finalAction : 'Next'}
</Button>
</div>
</div>
</Form>
+8
View File
@@ -1,11 +1,19 @@
<script lang="ts">
import { Heading } from '$lib/components';
import { wizard } from '$lib/stores/wizard';
import { onDestroy } from 'svelte';
/**
* Pass callback that is run before the Form submit, can be canceled with throwing an exception.
*/
export let beforeSubmit: () => Promise<void> = null;
export let nextDisabled = false;
$: wizard.setNextDisabled(nextDisabled);
onDestroy(() => {
wizard.setNextDisabled(false);
});
if (beforeSubmit) {
wizard.setInterceptor(beforeSubmit);
+8 -1
View File
@@ -1,6 +1,6 @@
import { browser } from '$app/environment';
import { VARS } from '$lib/system';
import { writable } from 'svelte/store';
import { derived, writable } from 'svelte/store';
import type { Feedback } from './feedback';
export type AppStore = {
@@ -13,6 +13,12 @@ export const app = writable<AppStore>({
themeInUse: 'light'
});
export const iconPath = derived(app, ($app) => {
return (name: string, type: 'color' | 'grayscale') => {
return `/icons/${$app.themeInUse}/${type}/${name}.svg`;
};
});
function createFeedbackStore() {
const { subscribe, update } = writable<Feedback>({
elapsed: browser ? parseInt(localStorage.getItem('feedbackElapsed')) : 0,
@@ -81,6 +87,7 @@ function createFeedbackStore() {
}
};
}
export const feedback = createFeedbackStore();
if (browser) {
+219
View File
@@ -0,0 +1,219 @@
import { writable } from 'svelte/store';
import { includesAll } from '$lib/helpers/array';
const initialFormData = {
users: {
root: false,
teams: false
},
databases: {
root: false,
documents: false
},
functions: {
root: false,
env: false,
inactive: false
},
storage: {
root: false
}
};
export type MigrationFormData = typeof initialFormData;
export const createMigrationFormStore = () => {
const store = writable({ ...initialFormData });
const reset = () => {
store.set({ ...initialFormData });
};
return {
...store,
reset
};
};
const resources = [
'user',
'team',
'membership',
'file',
'bucket',
'function',
'envVar',
'deployment',
'database',
'collection',
'index',
'attribute',
'document'
] as const;
type Resource = (typeof resources)[number];
export const providerResources: Record<Provider, Resource[]> = {
appwrite: [...resources],
supabase: [
'user',
'database',
'collection',
'attribute',
'index',
'document',
'bucket',
'file'
],
nhost: ['user', 'database', 'collection', 'attribute', 'index', 'document', 'bucket', 'file'],
firebase: ['user', 'database', 'collection', 'attribute', 'document', 'bucket', 'file']
};
export const migrationFormToResources = (
formData: MigrationFormData,
provider?: Provider
): Resource[] => {
const resources: Resource[] = [];
const addResource = (resource: Resource) => {
if (providerResources[provider].includes(resource)) {
resources.push(resource);
}
};
if (formData.users.root) {
addResource('user');
}
if (formData.users.teams) {
addResource('team');
addResource('membership');
}
if (formData.databases.root) {
addResource('database');
}
if (formData.databases.documents) {
addResource('collection');
addResource('attribute');
addResource('index');
addResource('document');
}
if (formData.functions.root) {
addResource('function');
}
if (formData.functions.env) {
addResource('envVar');
}
if (formData.functions.inactive) {
addResource('deployment');
}
if (formData.storage.root) {
addResource('bucket');
addResource('file');
}
return resources;
};
const compareVersions = (a: string, b: string) => {
const aParts = a.split('.');
const bParts = b.split('.');
const maxLength = Math.max(aParts.length, bParts.length);
for (let i = 0; i < maxLength; i++) {
const aPart = aParts[i] ? parseInt(aParts[i]) : 0;
const bPart = bParts[i] ? parseInt(bParts[i]) : 0;
if (aPart > bPart) {
return 1;
}
if (aPart < bPart) {
return -1;
}
}
return 0;
};
export const isVersionAtLeast = (version: string, atLeast: string) => {
return compareVersions(version, atLeast) >= 0;
};
export const resourcesToMigrationForm = (
resources: Resource[],
version = '0.0.0'
): MigrationFormData => {
const formData = { ...initialFormData };
if (resources.includes('user')) {
formData.users.root = true;
}
if (includesAll(resources, ['team', 'membership'])) {
formData.users.teams = true;
}
if (resources.includes('database')) {
formData.databases.root = true;
}
if (includesAll(resources, ['collection', 'attribute', 'document'])) {
formData.databases.documents = true;
}
if (resources.includes('function') && isVersionAtLeast(version, '1.4.0')) {
formData.functions.root = true;
}
if (resources.includes('envVar') && isVersionAtLeast(version, '1.4.0')) {
formData.functions.env = true;
}
if (resources.includes('deployment') && isVersionAtLeast(version, '1.4.0')) {
formData.functions.inactive = true;
}
if (includesAll(resources, ['bucket', 'file'])) {
formData.storage.root = true;
}
return formData;
};
type AppwriteInput = {
provider: 'appwrite';
endpoint?: string;
projectID?: string;
apiKey?: string;
};
type FirebaseInput = {
provider: 'firebase';
serviceAccount?: string;
projectId?: string;
};
type SupabaseInput = {
provider: 'supabase';
host?: string;
username?: string;
password?: string;
endpoint?: string;
apiKey?: string;
port?: number;
};
type NhostInput = {
provider: 'nhost';
region?: string;
subdomain?: string;
database?: string;
username?: string;
password?: string;
adminSecret?: string;
};
export type ProviderInput = AppwriteInput | NhostInput | SupabaseInput | FirebaseInput;
export type Provider = ProviderInput['provider'];
const initialProvider: ProviderInput = { provider: 'appwrite' };
export const createMigrationProviderStore = () => {
const store = writable<ProviderInput>({ ...initialProvider });
const changeProvider = (provider: Provider) => {
const newProvider: ProviderInput = { provider };
store.set(newProvider);
};
return {
...store,
changeProvider
};
};
+14 -7
View File
@@ -8,6 +8,7 @@ import {
Functions,
Health,
Locale,
Migrations,
Projects,
Project,
Storage,
@@ -39,7 +40,16 @@ const sdkForProject = {
teams: new Teams(clientProject),
users: new Users(clientProject),
vcs: new Vcs(clientProject),
proxy: new Proxy(clientProject)
proxy: new Proxy(clientProject),
migrations: new Migrations(clientProject)
};
export const getSdkForProject = (projectId: string) => {
if (projectId && projectId !== clientProject.config.project) {
clientProject.setProject(projectId);
}
return sdkForProject;
};
export const sdk = {
@@ -52,14 +62,11 @@ export const sdk = {
locale: new Locale(clientConsole),
projects: new Projects(clientConsole),
teams: new Teams(clientConsole),
users: new Users(clientConsole)
users: new Users(clientConsole),
migrations: new Migrations(clientConsole)
},
get forProject() {
const projectId = getProjectId();
if (projectId && projectId !== clientProject.config.project) {
clientProject.setProject(projectId);
}
return sdkForProject;
return getSdkForProject(projectId);
}
};
+9 -2
View File
@@ -8,6 +8,7 @@ export type WizardStore = {
component?: typeof SvelteComponent;
cover?: typeof SvelteComponent;
interceptor?: () => Promise<void>;
nextDisabled: boolean;
};
function createWizardStore() {
@@ -16,7 +17,8 @@ function createWizardStore() {
component: null,
cover: null,
interceptor: null,
media: null
media: null,
nextDisabled: false
});
return {
@@ -35,7 +37,12 @@ function createWizardStore() {
setInterceptor: (callback: WizardStore['interceptor']) => {
update((n) => {
n.interceptor = callback;
return n;
});
},
setNextDisabled: (disabled: boolean) => {
update((n) => {
n.nextDisabled = disabled;
return n;
});
},
+1
View File
@@ -11,6 +11,7 @@ export const VARS = {
CONSOLE_MODE: import.meta.env?.VITE_CONSOLE_MODE?.toString() as string | undefined,
VERCEL_ENV: import.meta.env?.VITE_VERCEL_ENV?.toString() as string | undefined,
GOOGLE_ANALYTICS: import.meta.env?.VITE_GA_PROJECT?.toString() as string | undefined,
ASSISTANT_ENDPOINT: import.meta.env?.VITE_ASSISTANT_ENDPOINT?.toString() as string | undefined,
GOOGLE_TAG: import.meta.env?.VITE_GTM_PROJECT?.toString() as string | undefined
};
+1 -1
View File
@@ -10,8 +10,8 @@
import { user } from '$lib/stores/user';
import { ENV, isCloud } from '$lib/system';
import * as Sentry from '@sentry/svelte';
import LogRocket from 'logrocket';
import { BrowserTracing } from '@sentry/tracing';
import LogRocket from 'logrocket';
import { onMount } from 'svelte';
import { onCLS, onFCP, onFID, onINP, onLCP, onTTFB } from 'web-vitals';
import Loading from './loading.svelte';
+8
View File
@@ -6,11 +6,19 @@ import { sdk } from '$lib/stores/sdk';
import { redirect } from '@sveltejs/kit';
import { Dependencies } from '$lib/constants';
import type { LayoutLoad } from './$types';
import { requestedMigration } from './store';
import { parseIfString } from '$lib/helpers/object';
export const ssr = false;
export const load: LayoutLoad = async ({ depends, url }) => {
depends(Dependencies.ACCOUNT);
if (url.searchParams.has('migrate')) {
const migrateData = url.searchParams.get('migrate');
requestedMigration.set(parseIfString(migrateData));
}
try {
const account = await sdk.forConsole.account.get();
@@ -0,0 +1,22 @@
import { createMigrationFormStore, createMigrationProviderStore } from '$lib/stores/migration';
import { wizard } from '$lib/stores/wizard';
import { requestedMigration } from '$routes/store';
import { get, writable } from 'svelte/store';
import Wizard from './wizard.svelte';
export const formData = createMigrationFormStore();
export function openMigrationWizard() {
wizard.start(Wizard);
const migData = get(requestedMigration);
provider.set({
provider: 'appwrite',
apiKey: migData?.apiKey,
endpoint: migData?.endpoint,
projectID: migData?.projectId
});
}
export const selectedProject = writable(null);
export const provider = createMigrationProviderStore();
@@ -0,0 +1,395 @@
<script lang="ts">
import { EyebrowHeading } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { deepMap } from '$lib/helpers/object';
import type { DeepKeys, WritableValue } from '$lib/helpers/types';
import { sdk } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import {
createMigrationFormStore,
createMigrationProviderStore,
isVersionAtLeast,
providerResources,
resourcesToMigrationForm
} from '$lib/stores/migration';
import { addNotification } from '$lib/stores/notifications';
export let formData: ReturnType<typeof createMigrationFormStore>;
export let provider: ReturnType<typeof createMigrationProviderStore>;
type FormData = WritableValue<typeof formData>;
type FormKeys = DeepKeys<FormData>;
/**
*
* @param field {string} The field to update.
* Representes the path to the field in the formData object e.g. 'users.root'
*/
function handleInputChange(field: FormKeys) {
return (event: Event) => {
const checked = (event.target as HTMLInputElement).checked;
// For each entry in formData, if the root is changed, change all children to the same value
// otherwise, if a child is changed to true, change the root to true
const [parent, child] = field.split('.');
if (child === 'root') {
formData.update((data) => {
data[parent].root = checked;
for (const key in data[parent]) {
if (key !== 'root') {
data[parent][key] = checked;
}
}
return data;
});
} else if (checked) {
formData.update((data) => {
data[parent].root = checked;
return data;
});
}
};
}
function deselectAll() {
$formData = deepMap($formData, (v) => {
if (typeof v === 'boolean') {
return false;
}
return v;
});
}
function selectAll() {
$formData = resourcesToMigrationForm(resources, version);
}
let report: any;
$: version = report?.version || '0.0.0';
let isOpen = false;
onMount(async () => {
isOpen = true;
try {
switch ($provider.provider) {
case 'appwrite': {
const res = await sdk.forProject.migrations.getAppwriteReport(
providerResources.appwrite,
$provider.endpoint,
$provider.projectID,
$provider.apiKey
);
report = res;
break;
}
case 'supabase': {
const res = await sdk.forProject.migrations.getSupabaseReport(
providerResources.supabase,
$provider.endpoint,
$provider.apiKey,
$provider.host,
$provider.username,
$provider.password,
$provider.port
);
report = res;
break;
}
case 'firebase': {
if ($provider.projectId) {
// OAuth
} else if ($provider.serviceAccount) {
// Manual auth
const res = await sdk.forProject.migrations.getFirebaseReport(
providerResources.firebase,
$provider.serviceAccount
);
report = res;
}
break;
}
case 'nhost': {
const res = await sdk.forProject.migrations.getNHostReport(
providerResources.nhost,
$provider.subdomain,
$provider.region,
$provider.adminSecret,
$provider.database,
$provider.username,
$provider.password
);
report = res;
}
}
} catch (e) {
if (!isOpen) return;
addNotification({
message: e.message,
type: 'error'
});
}
return () => {
isOpen = false;
};
});
$: resources = providerResources[$provider.provider];
// $: wizard.setNextDisabled(!report);
</script>
<div class="box" style:border-radius="0.5rem">
<div class="u-flex u-flex-vertical u-gap-16">
<EyebrowHeading class="eyebrow" tag="h3" size={3}>Good to know</EyebrowHeading>
<div class="u-flex u-gap-16">
<div class="circled">
<i class="icon-cog" />
</div>
<div>
<p class="u-bold">Project settings are not imported</p>
<p>You will need to set service and project settings manually</p>
</div>
</div>
<div class="u-flex u-gap-16">
<div class="circled">
<i class="icon-trending-up" />
</div>
<div>
<p class="u-bold">Keep your project plan limits in mind</p>
<p>Make sure to have enough storage in your project plan when importing files</p>
</div>
</div>
{#if $provider.provider === 'firebase'}
<div class="u-flex u-gap-16">
<div class="circled">
<i class="icon-exclamation u-color-text-warning" />
</div>
<div>
<p class="u-bold">Possible charges by Firebase</p>
<p>
Appwrite does not impose charges for importing data, but please note that
Firebase may have its own pricing for this service
</p>
</div>
</div>
{:else}
<div class="u-flex u-gap-16">
<div class="circled">
<i class="icon-currency-dollar" />
</div>
<div>
<p class="u-bold">Transfer is free of charge</p>
<p>You won't be charged for Appwrite bandwidth usage for importing data</p>
</div>
</div>
{/if}
</div>
</div>
<ul class="buttons-list u-margin-block-start-32 u-main-end">
<li class="buttons-list-item">
<Button text on:click={deselectAll}>Deselect all</Button>
</li>
<li class="buttons-list-item">
<Button text on:click={selectAll}>Select all</Button>
</li>
</ul>
<ul class="u-flex u-flex-vertical u-gap-32 u-margin-block-start-16">
{#if resources.includes('user')}
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.users.root}
on:change={handleInputChange('users.root')} />
<div class="u-flex u-gap-4">
<span class="u-bold">Users</span>
{#if $provider.provider !== 'firebase'}
<span class="inline-tag">{report?.user ?? '...'}</span>
{/if}
</div>
<div />
<span>Import all users</span>
{#if resources.includes('team')}
<ul>
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.users.teams}
on:change={handleInputChange('users.teams')} />
<div class="u-flex u-gap-4">
<span class="u-bold">Include teams</span>
{#if $provider.provider === 'firebase'}
<span class="inline-tag">{report?.team ?? '...'}</span>
{/if}
</div>
<div />
<span>Import all teams and the team memberships of your users</span>
</li>
</ul>
{/if}
</li>
{/if}
{#if resources.includes('database')}
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.databases.root}
on:change={handleInputChange('databases.root')} />
<div class="u-flex u-gap-4">
<span class="u-bold">Databases</span>
{#if $provider.provider !== 'firebase'}
<span class="inline-tag">{report?.database ?? '...'}</span>
{/if}
</div>
<div />
<span>Import all databases, including collections, indexes and attributes</span>
{#if resources.includes('document')}
<ul>
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.databases.documents}
on:change={handleInputChange('databases.documents')} />
<div class="u-flex u-gap-4">
<span class="u-bold">Include documents</span>
{#if $provider.provider !== 'firebase'}
<span class="inline-tag">{report?.document ?? '...'}</span>
{/if}
</div>
<div />
<span>Import all of your documents</span>
</li>
</ul>
{/if}
</li>
{/if}
{#if resources.includes('function') && isVersionAtLeast(version, '1.4.0')}
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.functions.root}
on:change={handleInputChange('functions.root')} />
<div class="u-flex u-gap-4">
<span class="u-bold">Functions</span>
{#if $provider.provider !== 'firebase'}
<span class="inline-tag">{report?.function ?? '...'}</span>
{/if}
</div>
<div />
<span>Import all functions and their active deployment</span>
<ul>
{#if resources.includes('envVar')}
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.functions.env}
on:change={handleInputChange('functions.env')} />
<div class="u-flex u-gap-4">
<span class="u-bold">Include environment variables</span>
</div>
<div />
<span>Import all environment variables</span>
</li>
{/if}
{#if resources.includes('deployment')}
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.functions.inactive}
on:change={handleInputChange('functions.inactive')} />
<div class="u-flex u-gap-4">
<span class="u-bold">Include inactive deployments</span>
</div>
<div />
<span>Import all deployments that are not currently active</span>
</li>
{/if}
</ul>
</li>
{/if}
{#if resources.includes('bucket') && resources.includes('file')}
<li class="checkbox-field">
<input
type="checkbox"
bind:checked={$formData.storage.root}
on:change={handleInputChange('storage.root')} />
<div class="u-flex u-gap-4">
<span class="u-bold">Storage</span>
{#if $provider.provider !== 'firebase'}
<span class="inline-tag">
{report?.size ? `${report.size.toFixed(2)}MB` : '...'}
</span>
{/if}
</div>
<div />
<span>
Import all buckets
{#if $provider.provider !== 'firebase'}
<span class="inline-tag">{report?.bucket ?? '...'}</span>
{/if}
and files
{#if $provider.provider !== 'firebase'}
<span class="inline-tag">{report?.file ?? '...'}</span>
{/if}
</span>
</li>
{/if}
</ul>
<style lang="scss">
.box :global(.eyebrow) {
font-weight: 500;
color: hsl(var(--color-neutral-70));
}
.circled {
width: 1.5rem;
height: 1.5rem;
flex-shrink: 0;
border-radius: 100%;
border: 1px solid hsl(var(--color-border));
position: relative;
i {
position: absolute;
left: 50%;
top: 50%;
translate: -50% -50%;
font-size: 1rem;
}
}
.buttons-list {
border-block-end: 1px solid hsl(var(--color-border));
padding-block-end: 0.625rem;
}
.checkbox-field {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.25rem 1rem;
align-items: center;
ul {
grid-column-start: 2;
padding-block-end: 2rem;
li {
margin-block-start: 1rem;
}
}
}
</style>
@@ -0,0 +1,121 @@
<script lang="ts">
import { page } from '$app/stores';
import { InputSelect } from '$lib/elements/forms';
import InputText from '$lib/elements/forms/inputText.svelte';
import { WizardStep } from '$lib/layout';
import { Query, type Models, ID } from '@appwrite.io/console';
import { selectedProject } from '.';
import { sdk } from '$lib/stores/sdk';
const organizations = $page.data.organizations
.teams as Models.TeamList<Models.Preferences>['teams'];
let selectedOrg = organizations.length ? organizations[0].$id : null;
let projects = [] as Models.ProjectList['projects'];
$: hasProjects = projects.length > 0;
let loadingProjects = false;
let projectType = 'existing';
let newProjName = '';
async function getProjects(orgId: string | null) {
if (!orgId) {
projects = [];
} else {
loadingProjects = true;
projects = await sdk.forConsole.projects
.list([Query.equal('teamId', orgId), Query.orderDesc('$createdAt')])
.then((res) => res.projects);
projectType = projects.length ? 'existing' : 'new';
loadingProjects = false;
}
}
$: {
getProjects(selectedOrg);
}
const beforeSubmit = async () => {
if (projectType === 'existing') return;
const project = await sdk.forConsole.projects.create(ID.unique(), newProjName, selectedOrg);
selectedProject.set(project.$id);
};
</script>
<WizardStep {beforeSubmit}>
<svelte:fragment slot="title">
{hasProjects ? 'Select project' : 'Create project'}
</svelte:fragment>
<div class="u-flex u-flex-vertical u-gap-24">
{#if organizations.length > 1}
<InputSelect
id="organization"
bind:value={selectedOrg}
label="Select organization"
options={organizations.map((p) => ({
label: p.name,
value: p.$id
}))}
disabled={loadingProjects} />
{/if}
{#if selectedOrg && !loadingProjects}
{#if projects.length}
<div class="radios">
<div class="u-flex u-gap-8">
<input
type="radio"
name="project_type"
id="project_type--existing"
bind:group={projectType}
value="existing" />
<label for="project_type--existing">
<p class="u-bold">Existing project</p>
<p>Import data to an existing project</p>
</label>
</div>
<div class="u-flex u-gap-8">
<input
type="radio"
name="project_type"
id="project_type--new"
bind:group={projectType}
value="new" />
<label for="project_type--new">
<p class="u-bold">Create new project</p>
<span>Import data to a new project</span>
</label>
</div>
</div>
{/if}
<div>
{#if projectType === 'existing'}
<InputSelect
id="project"
bind:value={$selectedProject}
label="Select a project"
options={projects.map((p) => ({
label: p.name,
value: p.$id
}))} />
{:else}
<InputText
label="Project name"
placeholder="Enter project name"
id="project_name"
bind:value={newProjName}
required />
{/if}
</div>
{/if}
</div>
</WizardStep>
<style lang="scss">
.radios {
display: grid;
grid-template-columns: 1fr 1fr;
}
</style>
@@ -0,0 +1,11 @@
<script lang="ts">
import { WizardStep } from '$lib/layout';
import { formData, provider } from '.';
import ResourceForm from './resource-form.svelte';
</script>
<WizardStep>
<svelte:fragment slot="title">Select data</svelte:fragment>
<ResourceForm {provider} {formData} />
</WizardStep>
@@ -0,0 +1,49 @@
<script lang="ts">
import { goto, invalidateAll } from '$app/navigation';
import type { WizardStepsType } from '$lib/layout/wizard.svelte';
import Wizard from '$lib/layout/wizard.svelte';
import { migrationFormToResources } from '$lib/stores/migration';
import { getSdkForProject } from '$lib/stores/sdk';
import { wizard } from '$lib/stores/wizard';
import { onDestroy } from 'svelte';
import { formData, provider, selectedProject } from '.';
import Step1 from './step1.svelte';
import Step2 from './step2.svelte';
import { requestedMigration } from '$routes/store';
const onExit = () => {
formData.reset();
requestedMigration.set(null);
};
const onFinish = async () => {
const resources = migrationFormToResources($formData);
if ($provider.provider !== 'appwrite') return;
await getSdkForProject($selectedProject).migrations.migrateAppwrite(
resources,
$provider.endpoint,
$provider.projectID,
$provider.apiKey
);
await invalidateAll();
wizard.hide();
goto(`/console/project-${$selectedProject}/settings/migrations`);
};
onDestroy(onExit);
const steps: WizardStepsType = new Map();
steps.set(1, {
label: 'Select project',
component: Step1
});
steps.set(2, {
label: 'Select data',
component: Step2
});
</script>
<Wizard title="Create Project" {steps} on:exit={onExit} on:finish={onFinish} />
+122 -2
View File
@@ -6,14 +6,126 @@
import Header from '$lib/layout/header.svelte';
import SideNavigation from '$lib/layout/navigation.svelte';
import Shell from '$lib/layout/shell.svelte';
import { feedback } from '$lib/stores/feedback';
import { app, feedback } from '$lib/stores/app';
import { log } from '$lib/stores/logs';
import { newOrgModal } from '$lib/stores/organization';
import { wizard } from '$lib/stores/wizard';
import { onMount } from 'svelte';
import { loading } from '../store';
import { loading, requestedMigration } from '../store';
import Create from './createOrganization.svelte';
import { goto } from '$app/navigation';
import { CommandCenter, registerCommands, registerSearchers } from '$lib/commandCenter';
import { AIPanel, OrganizationsPanel, ProjectsPanel } from '$lib/commandCenter/panels';
import { orgSearcher, projectsSearcher } from '$lib/commandCenter/searchers';
import { addSubPanel } from '$lib/commandCenter/subPanels';
import { addNotification } from '$lib/stores/notifications';
import { openMigrationWizard } from './(migration-wizard)';
$: $registerCommands([
{
label: 'Ask AI',
callback: () => {
addSubPanel(AIPanel);
},
keys: ['a', 'i'],
icon: 'light-bulb'
},
{
label: 'Go to account',
callback: () => {
goto('/console/account');
},
keys: ['i'],
group: 'navigation'
},
{
label: 'Find an organization',
callback: () => {
addSubPanel(OrganizationsPanel);
},
group: 'organizations',
icon: 'search',
keys: ['f', 'o']
},
{
label: 'Find a project',
callback: () => {
addSubPanel(ProjectsPanel);
},
keys: ['f', 'p'],
group: 'projects',
icon: 'search'
},
{
label: 'Create new organization',
callback: () => {
newOrgModal.set(true);
},
keys: ['c', 'o'],
group: 'organizations'
},
{
label: 'Go to home',
callback: () => {
goto('/console');
},
keys: ['h'],
group: 'navigation'
},
{
label: 'Open documentation',
callback: () => {
window.open('https://appwrite.io/docs', '_blank');
},
group: 'help',
icon: 'book-open'
},
{
label: 'Contact support',
callback: () => {
window.open('https://appwrite.io/support', '_blank');
},
group: 'help',
icon: 'question-mark-circle'
},
{
label: 'Send feedback',
callback: () => {
feedback.toggleFeedback();
},
group: 'help',
icon: 'annotation'
},
{
label: 'Join Discord community',
callback: () => {
window.open('https://appwrite.io/discord', '_blank');
},
group: 'help',
icon: 'discord'
},
{
label: 'Toggle theme',
callback: () => {
if ($app.theme === 'auto') {
$app.theme = 'light';
} else if ($app.theme === 'light') {
$app.theme = 'dark';
} else {
$app.theme = 'auto';
}
addNotification({
title: 'Theme changed',
message: `Theme changed to ${$app.theme}`,
type: 'success'
});
},
group: 'misc',
icon: 'switch-horizontal'
}
]);
let isOpen = false;
onMount(() => {
@@ -41,8 +153,16 @@
$log.data = null;
$log.func = null;
}
$: if ($requestedMigration) {
openMigrationWizard();
}
$registerSearchers(orgSearcher, projectsSearcher);
</script>
<CommandCenter />
<Shell
bind:isOpen
showSideNavigation={$page.url.pathname !== '/console' &&
+1
View File
@@ -1,4 +1,5 @@
import { sdk } from '$lib/stores/sdk';
import type { LayoutLoad } from './$types';
export const load: LayoutLoad = async ({ fetch }) => {
@@ -1,7 +1,13 @@
<script lang="ts">
import { newOrgModal, newMemberModal } from '$lib/stores/organization';
import CreateMember from './createMember.svelte';
import { newMemberModal, newOrgModal } from '$lib/stores/organization';
import { requestedMigration } from '$routes/store';
import { openMigrationWizard } from '../(migration-wizard)';
import Create from '../createOrganization.svelte';
import CreateMember from './createMember.svelte';
$: if ($requestedMigration) {
openMigrationWizard();
}
</script>
<svelte:head>
@@ -1,9 +1,9 @@
import Header from './header.svelte';
import Breadcrumbs from './breadcrumbs.svelte';
import { sdk } from '$lib/stores/sdk';
import type { LayoutLoad } from './$types';
import { error } from '@sveltejs/kit';
import { Dependencies } from '$lib/constants';
import { sdk } from '$lib/stores/sdk';
import { error } from '@sveltejs/kit';
import type { LayoutLoad } from './$types';
import Breadcrumbs from './breadcrumbs.svelte';
import Header from './header.svelte';
export const load: LayoutLoad = async ({ params, depends }) => {
depends(Dependencies.ORGANIZATION);
@@ -1,15 +1,31 @@
<script lang="ts">
import { base } from '$app/paths';
import { Pill } from '$lib/elements';
import { GridItem1, Heading, Empty, CardContainer, PaginationWithLimit } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { Container } from '$lib/layout';
import CreateProject from './createProject.svelte';
import CreateOrganization from '../createOrganization.svelte';
import type { PageData } from './$types';
import { page } from '$app/stores';
import { registerCommands } from '$lib/commandCenter';
import {
CardContainer,
DropList,
DropListItem,
Empty,
GridItem1,
Heading,
PaginationWithLimit
} from '$lib/components';
import { Pill } from '$lib/elements';
import { Button } from '$lib/elements/forms';
import { goto } from '$app/navigation';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Container } from '$lib/layout';
import { services } from '$lib/stores/project-services';
import { sdk } from '$lib/stores/sdk';
import { loading } from '$routes/store';
import type { Models } from '@appwrite.io/console';
import { ID } from '@appwrite.io/console';
import CreateOrganization from '../createOrganization.svelte';
import { openImportWizard } from '../project-[project]/settings/migrations/(import)';
import type { PageData } from './$types';
import CreateProject from './createProject.svelte';
export let data: PageData;
@@ -56,14 +72,60 @@
(value, index, self) => index === self.findIndex((t) => t.name === value.name)
);
}
$: $registerCommands([
{
label: 'Create project',
callback: () => {
showCreate = true;
},
keys: ['c'],
disabled: showCreate,
group: 'projects',
icon: 'plus'
}
]);
let showDropdown = false;
const importProject = async () => {
try {
loading.set(true);
const project = await sdk.forConsole.projects.create(
ID.unique(),
`Imported project ${new Date().toISOString()}`,
$page.params.organization,
'default'
);
trackEvent(Submit.ProjectCreate, {
teamId: $page.params.organization
});
await goto(`/console/project-${project.$id}/settings/migrations`);
openImportWizard();
loading.set(false);
} catch (e) {
trackError(e, Submit.ProjectCreate);
}
};
</script>
<Container>
<div class="u-flex u-gap-12 common-section u-main-space-between">
<Heading tag="h2" size="5">Projects</Heading>
<Button on:click={() => (showCreate = true)} event="create_project">
<span class="icon-plus" aria-hidden="true" /> <span class="text">Create project</span>
</Button>
<DropList bind:show={showDropdown} placement="bottom-end">
<Button on:click={() => (showDropdown = true)} event="create_project">
<span class="icon-plus" aria-hidden="true" />
<span class="text">Create project</span>
</Button>
<svelte:fragment slot="list">
<DropListItem on:click={() => (showCreate = true)}>Empty project</DropListItem>
<DropListItem on:click={importProject}>
<div class="u-flex u-gap-8 u-cross-center">
Import project <span class="tag eyebrow-heading-3">Experimental</span>
</div>
</DropListItem>
</svelte:fragment>
</DropList>
</div>
{#if data.projects.total}
@@ -2,7 +2,26 @@
import { UploadBox } from '$lib/components';
import { sdk } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import { stats } from './store';
import { project, stats } from './store';
import { goto } from '$app/navigation';
import { addSubPanel, registerCommands, registerSearchers } from '$lib/commandCenter';
import {
BucketsPanel,
DatabasesPanel,
FunctionsPanel,
TeamsPanel,
UsersPanel
} from '$lib/commandCenter/panels';
import {
bucketSearcher,
dbSearcher,
functionsSearcher,
teamSearcher,
userSearcher
} from '$lib/commandCenter/searchers';
import { MigrationBox } from '$lib/components';
onMount(() => {
return sdk.forConsole.client.subscribe(['project', 'console'], (response) => {
@@ -13,8 +32,109 @@
}
});
});
$: $registerCommands([
{
label: 'Go to overview',
callback: () => {
goto(`/console/project-${$project.$id}`);
},
keys: ['o'],
group: 'navigation'
},
{
label: 'Go to auth',
callback: () => {
goto(`/console/project-${$project.$id}/auth`);
},
keys: ['a'],
group: 'navigation'
},
{
label: 'Go to databases',
callback: () => {
goto(`/console/project-${$project.$id}/databases`);
},
keys: ['d'],
group: 'navigation'
},
{
label: 'Go to functions',
callback: () => {
goto(`/console/project-${$project.$id}/functions`);
},
keys: ['f'],
group: 'navigation'
},
{
label: 'Go to storage',
callback: () => {
goto(`/console/project-${$project.$id}/storage`);
},
keys: ['s'],
group: 'navigation'
},
{
label: 'Go to settings',
callback: () => {
goto(`/console/project-${$project.$id}/settings`);
},
keys: ['p'],
group: 'navigation'
},
{
label: 'Find users',
callback: () => {
addSubPanel(UsersPanel);
},
group: 'users',
icon: 'search',
keys: ['f', 'u'],
rank: 10
},
{
label: 'Find teams',
callback: () => {
addSubPanel(TeamsPanel);
},
group: 'teams',
icon: 'search',
keys: ['f', 't']
},
{
label: 'Find databases',
callback: () => {
addSubPanel(DatabasesPanel);
},
group: 'databases',
icon: 'search',
keys: ['f', 'd']
},
{
label: 'Find functions',
callback: () => {
addSubPanel(FunctionsPanel);
},
group: 'functions',
icon: 'search',
keys: ['f', 'f']
},
{
label: 'Find buckets',
callback: () => {
addSubPanel(BucketsPanel);
},
group: 'buckets',
icon: 'search',
keys: ['f', 'b']
}
]);
$registerSearchers(userSearcher, teamSearcher, dbSearcher, functionsSearcher, bucketSearcher);
</script>
<slot />
<UploadBox />
<MigrationBox />
@@ -1,3 +1,131 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { registerCommands, updateCommandGroupRanks } from '$lib/commandCenter';
import { project } from '../store';
import { showCreateUser } from './+page.svelte';
import { showCreateTeam } from './teams/+page.svelte';
$: $registerCommands([
{
label: 'Create user',
callback: async () => {
if (!$page.url.pathname.endsWith('auth')) {
await goto(`/console/project-${$project.$id}/auth`);
}
showCreateUser.set(true);
},
keys: $page.url.pathname.endsWith('auth') ? ['c'] : ['c', 'u'],
group: 'users',
icon: 'plus',
rank: $page.url.pathname.endsWith('auth') ? 10 : 0
},
{
label: 'Create team',
callback: async () => {
if (!$page.url.pathname.endsWith('teams')) {
await goto(`/console/project-${$project.$id}/auth/teams`);
}
showCreateTeam.set(true);
},
keys: $page.url.pathname.endsWith('teams') ? ['c'] : ['c', 't'],
group: 'teams',
icon: 'plus',
rank: $page.url.pathname.endsWith('teams') ? 10 : 0
},
{
label: 'Go to teams',
keys: ['g', 't'],
callback() {
goto(`/console/project-${$project.$id}/auth/teams`);
},
group: 'navigation',
rank: 1,
disabled: $page.url.pathname.endsWith('teams')
},
{
label: 'Go to usage',
keys: ['g', 'u'],
callback() {
goto(`/console/project-${$project.$id}/auth/usage`);
},
group: 'navigation',
rank: 1,
disabled: $page.url.pathname.endsWith('usage')
},
{
label: 'Go to security',
keys: ['g', 'e'],
callback() {
goto(`/console/project-${$project.$id}/auth/security`);
},
group: 'navigation',
rank: 1,
disabled: $page.url.pathname.endsWith('security')
},
{
label: 'Go to settings',
keys: ['g', 's'],
callback() {
goto(`/console/project-${$project.$id}/auth/settings`);
},
group: 'navigation',
rank: 1,
disabled: $page.url.pathname.endsWith('settings')
},
{
label: 'Users limit',
async callback() {
await goto(`/console/project-${$project.$id}/auth/security#users-limit`);
scrollBy({ top: -100 });
},
group: 'security',
icon: 'pencil'
},
{
label: 'Session length',
async callback() {
await goto(`/console/project-${$project.$id}/auth/security#session-length`);
scrollBy({ top: -100 });
},
group: 'security',
icon: 'pencil'
},
{
label: 'Sessions limit',
async callback() {
await goto(`/console/project-${$project.$id}/auth/security#sessions-limit`);
scrollBy({ top: -100 });
},
group: 'security',
icon: 'pencil'
},
{
label: 'Password history',
async callback() {
await goto(`/console/project-${$project.$id}/auth/security#password-history`);
scrollBy({ top: -100 });
},
group: 'security',
icon: 'pencil'
},
{
label: 'Password dictionary',
async callback() {
await goto(`/console/project-${$project.$id}/auth/security#password-dictionary`);
scrollBy({ top: -100 });
},
group: 'security',
icon: 'pencil'
}
]);
// To prioritize the groups!
$: $updateCommandGroupRanks({ users: 300, teams: 200, security: 100, navigation: 50 });
</script>
<svelte:head>
<title>Auth - Appwrite</title>
</svelte:head>
@@ -1,31 +1,36 @@
<script lang="ts" context="module">
export let showCreateUser = writable(false);
</script>
<script lang="ts">
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/stores';
import {
AvatarInitials,
Copy,
Empty,
EmptySearch,
Copy,
SearchQuery,
AvatarInitials,
PaginationWithLimit
PaginationWithLimit,
SearchQuery
} from '$lib/components';
import { Pill } from '$lib/elements';
import { Button } from '$lib/elements/forms';
import {
Table,
TableHeader,
TableBody,
TableCellHead,
TableCell,
TableCellHead,
TableCellText,
TableHeader,
TableRowLink
} from '$lib/elements/table';
import { Pill } from '$lib/elements';
import { toLocaleDate, toLocaleDateTime } from '$lib/helpers/date';
import { Container } from '$lib/layout';
import { base } from '$app/paths';
import { goto } from '$app/navigation';
import Create from './createUser.svelte';
import type { Models } from '@appwrite.io/console';
import { writable } from 'svelte/store';
import type { PageData } from './$types';
import Create from './createUser.svelte';
export let data: PageData;
@@ -36,7 +41,6 @@
return { accessedAt, labels, ...user };
});
let showCreate = false;
const projectId = $page.params.project;
async function userCreated(event: CustomEvent<Models.User<Record<string, unknown>>>) {
await goto(`${base}/console/project-${projectId}/auth/user-${event.detail.$id}`);
@@ -45,7 +49,7 @@
<Container>
<SearchQuery search={data.search} placeholder="Search by name, email, phone, or ID">
<Button on:click={() => (showCreate = true)} event="create_user">
<Button on:click={() => ($showCreateUser = true)} event="create_user">
<span class="icon-plus" aria-hidden="true" /> <span class="text">Create user</span>
</Button>
</SearchQuery>
@@ -143,8 +147,8 @@
single
href="https://appwrite.io/docs/server/users"
target="user"
on:click={() => (showCreate = true)} />
on:click={() => showCreateUser.set(true)} />
{/if}
</Container>
<Create bind:showCreate on:created={userCreated} />
<Create bind:showCreate={$showCreateUser} on:created={userCreated} />
@@ -3,12 +3,16 @@ import { sdk } from '$lib/stores/sdk';
import { getLimit, getPage, getSearch, pageToOffset } from '$lib/helpers/load';
import { PAGE_LIMIT } from '$lib/constants';
import type { PageLoad } from './$types';
import { showCreateUser } from './+page.svelte';
export const load: PageLoad = async ({ url, route }) => {
const page = getPage(url);
const search = getSearch(url);
const limit = getLimit(url, route, PAGE_LIMIT);
const offset = pageToOffset(page, limit);
if (typeof url.searchParams.get('create') === 'string') {
showCreateUser.set(true);
}
return {
offset,
@@ -35,7 +35,7 @@
<Form onSubmit={updatePasswordDictionary}>
<CardGrid>
<Heading tag="h2" size="7">Password Dictionary</Heading>
<Heading tag="h2" size="7" id="password-dictionary">Password Dictionary</Heading>
<svelte:fragment slot="aside">
<FormList>
<InputSwitch
@@ -39,7 +39,7 @@
<Form onSubmit={updatePasswordHistoryLimit}>
<CardGrid>
<Heading tag="h2" size="7">Password History</Heading>
<Heading tag="h2" size="7" id="password-history">Password History</Heading>
<svelte:fragment slot="aside">
<FormList>
<InputSwitch
@@ -34,7 +34,7 @@
</script>
<CardGrid>
<Heading tag="h2" size="7">Session Length</Heading>
<Heading tag="h2" size="7" id="session-length">Session Length</Heading>
<p>
If you reduce the limit, users who are currently logged in will be logged out of the
@@ -34,7 +34,7 @@
<Form onSubmit={updateSessionsLimit}>
<CardGrid>
<Heading tag="h2" size="7">Sessions Limit</Heading>
<Heading tag="h2" size="7" id="sessions-limit">Sessions Limit</Heading>
<p>Maximum number of active sessions allowed per user.</p>
<svelte:fragment slot="aside">
<ul>
@@ -46,7 +46,7 @@
</script>
<CardGrid>
<Heading tag="h2" size="7">Users Limit</Heading>
<Heading tag="h2" size="7" id="users-limit">Users Limit</Heading>
<p>
Limit new users from signing up for your project, regardless of authentication method. You
can still create users and team memberships from your Appwrite console.
@@ -1,3 +1,7 @@
<script lang="ts" context="module">
export let showCreateTeam = writable(false);
</script>
<script lang="ts">
import { page } from '$app/stores';
import {
@@ -24,11 +28,10 @@
import { base } from '$app/paths';
import type { Models } from '@appwrite.io/console';
import type { PageData } from './$types';
import { writable } from 'svelte/store';
export let data: PageData;
let showCreate = false;
const project = $page.params.project;
const teamCreated = async (event: CustomEvent<Models.Team<Record<string, unknown>>>) => {
await goto(`${base}/console/project-${project}/auth/teams/team-${event.detail.$id}`);
@@ -37,7 +40,7 @@
<Container>
<SearchQuery search={data.search} placeholder="Search by name">
<Button on:click={() => (showCreate = true)} event="create_team">
<Button on:click={() => ($showCreateTeam = true)} event="create_team">
<span class="icon-plus" aria-hidden="true" /> <span class="text">Create team</span>
</Button>
</SearchQuery>
@@ -86,10 +89,10 @@
{:else}
<Empty
single
on:click={() => (showCreate = true)}
on:click={() => ($showCreateTeam = true)}
href="https://appwrite.io/docs/client/teams"
target="team" />
{/if}
</Container>
<Create bind:showCreate on:created={teamCreated} />
<Create bind:showCreate={$showCreateTeam} on:created={teamCreated} />
@@ -1,13 +1,23 @@
<script lang="ts" context="module">
import { get } from 'svelte/store';
let showDelete = writable(false);
export const promptDeleteUser = (id: string) => {
showDelete.set(true);
goto(`/console/project-${get(project).$id}/auth/user-${id}`);
};
</script>
<script lang="ts">
import { CardGrid, Box, Heading, AvatarInitials } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { toLocaleDate } from '$lib/helpers/date';
import { writable } from 'svelte/store';
import DeleteUser from './deleteUser.svelte';
import { user } from './store';
let showDelete = false;
// TODO: Remove this when the console SDK is updated
import { goto } from '$app/navigation';
import { project } from '../../store';
import { toLocaleDate } from '$lib/helpers/date';
$: accessedAt = ($user as unknown as { accessedAt: string }).accessedAt;
</script>
@@ -51,8 +61,8 @@
</svelte:fragment>
<svelte:fragment slot="actions">
<Button secondary on:click={() => (showDelete = true)} event="delete_user">Delete</Button>
<Button secondary on:click={() => ($showDelete = true)} event="delete_user">Delete</Button>
</svelte:fragment>
</CardGrid>
<DeleteUser bind:showDelete />
<DeleteUser bind:showDelete={$showDelete} />
@@ -1,3 +1,25 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { registerCommands, updateCommandGroupRanks } from '$lib/commandCenter';
import { project } from '../store';
$: $registerCommands([
{
label: 'Go to usage',
callback: () => {
goto(`/console/project-${$project.$id}/databases/usage`);
},
keys: ['g', 'u'],
disabled:
$page.url.pathname.includes('usage') || $page.url.pathname.includes('database-'),
group: 'databases'
}
]);
$: $updateCommandGroupRanks({ databases: 200, navigation: 100 });
</script>
<svelte:head>
<title>Databases - Appwrite</title>
</svelte:head>
@@ -1,16 +1,18 @@
<script lang="ts">
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { Button } from '$lib/elements/forms';
import { Empty, PaginationWithLimit } from '$lib/components';
import { Container, GridHeader } from '$lib/layout';
import { base } from '$app/paths';
import { page } from '$app/stores';
import { Empty, PaginationWithLimit } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { Container, GridHeader } from '$lib/layout';
import type { Models } from '@appwrite.io/console';
import type { PageData } from './$types';
import Create from './create.svelte';
import Grid from './grid.svelte';
import Table from './table.svelte';
import type { Models } from '@appwrite.io/console';
import type { PageData } from './$types';
import { columns } from './store';
import Table from './table.svelte';
import { registerCommands } from '$lib/commandCenter';
export let data: PageData;
@@ -21,6 +23,20 @@
showCreate = false;
await goto(`${base}/console/project-${project}/databases/database-${event.detail.$id}`);
}
$: $registerCommands([
{
label: 'Create database',
callback: () => {
showCreate = true;
},
keys: ['c'],
disabled: showCreate,
icon: 'plus',
group: 'databases',
rank: 10
}
]);
</script>
<Container>
@@ -1,10 +1,11 @@
import { CARD_LIMIT } from '$lib/constants';
import { CARD_LIMIT, Dependencies } from '$lib/constants';
import { getLimit, getPage, getView, pageToOffset, View } from '$lib/helpers/load';
import { sdk } from '$lib/stores/sdk';
import { Query } from '@appwrite.io/console';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ url, route }) => {
export const load: PageLoad = async ({ url, route, depends }) => {
depends(Dependencies.DATABASES);
const page = getPage(url);
const limit = getLimit(url, route, CARD_LIMIT);
const view = getView(url, route, View.Grid);
@@ -6,6 +6,14 @@
import { base } from '$app/paths';
import { page } from '$app/stores';
import { Dependencies } from '$lib/constants';
import {
addSubPanel,
registerCommands,
registerSearchers,
updateCommandGroupRanks
} from '$lib/commandCenter';
import { collectionsSearcher } from '$lib/commandCenter/searchers';
import { CollectionsPanel } from '$lib/commandCenter/panels';
const project = $page.params.project;
const databaseId = $page.params.database;
@@ -17,6 +25,67 @@
`${base}/console/project-${project}/databases/database-${databaseId}/collection-${event.detail.$id}`
);
}
$: $registerCommands([
{
label: 'Find collections',
callback() {
addSubPanel(CollectionsPanel);
},
keys: ['f', 'c'],
group: 'collections',
icon: 'search'
},
{
label: 'Create collection',
callback() {
$showCreate = true;
if (!$page.url.pathname.endsWith(databaseId)) {
goto(`/console/project-${project}/databases/database-${databaseId}`);
}
},
keys: $page.url.pathname.endsWith(databaseId) ? ['c'] : ['c', 'c'],
disabled: $page.url.pathname.includes('collection-'),
group: 'collections',
icon: 'plus'
},
{
label: 'Go to collections',
callback() {
goto(`/console/project-${project}/databases/database-${databaseId}`);
},
disabled:
$page.url.pathname.endsWith(databaseId) ||
$page.url.pathname.includes('collection-'),
keys: ['g', 'c'],
group: 'collections'
},
{
label: 'Go to usage',
callback() {
goto(`/console/project-${project}/databases/database-${databaseId}/usage`);
},
disabled:
$page.url.pathname.includes('/usage') || $page.url.pathname.includes('collection-'),
keys: ['g', 'u'],
group: 'collections'
},
{
label: 'Go to settings',
callback() {
goto(`/console/project-${project}/databases/database-${databaseId}/settings`);
},
disabled:
$page.url.pathname.includes('/settings') ||
$page.url.pathname.includes('collection-'),
keys: ['g', 's'],
group: 'collections'
}
]);
$registerSearchers(collectionsSearcher);
$: $updateCommandGroupRanks({ collections: 10 });
</script>
<svelte:head>
@@ -2,9 +2,10 @@ import { Query } from '@appwrite.io/console';
import { sdk } from '$lib/stores/sdk';
import { getLimit, getPage, getView, pageToOffset, View } from '$lib/helpers/load';
import type { PageLoad } from './$types';
import { CARD_LIMIT } from '$lib/constants';
import { CARD_LIMIT, Dependencies } from '$lib/constants';
export const load: PageLoad = async ({ params, url, route }) => {
export const load: PageLoad = async ({ params, url, route, depends }) => {
depends(Dependencies.COLLECTIONS);
const page = getPage(url);
const limit = getLimit(url, route, CARD_LIMIT);
const view = getView(url, route, View.Grid);

Some files were not shown because too many files have changed in this diff Show More