mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch 'main' into feat-animations
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
VITE_APPWRITE_ENDPOINT=
|
||||
VITE_APPWRITE_GROWTH_ENDPOINT=
|
||||
VITE_GA_PROJECT=
|
||||
VITE_SENTRY_DSN=
|
||||
VITE_CONSOLE_MODE=self-hosted
|
||||
+9
-1
@@ -4,7 +4,15 @@ module.exports = {
|
||||
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
|
||||
plugins: ['svelte3', '@typescript-eslint'],
|
||||
ignorePatterns: ['*.cjs'],
|
||||
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.svelte'],
|
||||
processor: 'svelte3/svelte3',
|
||||
rules: {
|
||||
'no-redeclare': 'off'
|
||||
}
|
||||
}
|
||||
],
|
||||
settings: {
|
||||
'svelte3/typescript': () => require('typescript')
|
||||
},
|
||||
|
||||
@@ -13,13 +13,13 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16
|
||||
- name: Audit dependencies
|
||||
run: npm audit --audit-level high
|
||||
node-version: 18
|
||||
# - name: Audit dependencies
|
||||
# run: npm audit --audit-level low
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Build Console
|
||||
@@ -30,7 +30,3 @@ jobs:
|
||||
run: npm run lint
|
||||
- name: Unit Tests
|
||||
run: npm test
|
||||
# - name: E2E Tests
|
||||
# run: |
|
||||
# npx playwright install
|
||||
# npm run e2e
|
||||
|
||||
+48
-29
@@ -1,10 +1,10 @@
|
||||
# Contributing
|
||||
|
||||
We would ❤️ for you to contribute to Appwrite and help make it better! We want contributing to Appwrite to be fun, enjoyable, and educational for anyone and everyone. All contributions are welcome, including issues, new docs as well as updates and tweaks, blog posts, workshops, and more.
|
||||
We would ❤️ for you to contribute to Appwrite and help make it better! We want your experience while contributing to Appwrite to be fun, enjoyable, and educational for anyone and everyone. All contributions are welcome, including issues, and new docs, as well as updates and tweaks, blog posts, workshops, and more.
|
||||
|
||||
## How to Start?
|
||||
|
||||
If you are worried or don’t know where to start, check out our next section explaining what kind of help is needed and where you can get involved. You can reach out with questions on our [Discord](https://appwrite.io/discord) server. You can also submit an issue and a maintainer can guide you!
|
||||
If you are worried about or don’t know where to start, check out the next section that explains what kind of help is needed and how you can get involved. You can reach out with any questions on our [Discord](https://appwrite.io/discord) server. You can also submit an issue and a maintainer can guide you!
|
||||
|
||||
## Repo Structure
|
||||
|
||||
@@ -33,12 +33,31 @@ If you are worried or don’t know where to start, check out our next section ex
|
||||
|
||||
## Development
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install`, start a development server:
|
||||
### 1. Clone the repository with git
|
||||
|
||||
```bash
|
||||
git clone https://github.com/appwrite/console.git appwrite-console
|
||||
```
|
||||
|
||||
### 2. Install dependencies with npm
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### 3. Setup environment variables
|
||||
|
||||
Add a `.env` file by copying the `.env.example` file as a template in the project's root directory.
|
||||
|
||||
Finally, start a development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
> **Note**
|
||||
> If http://localhost:3000 is blank and the browser console logs have a `TypeError: Failed to fetch dynamically imported module: http://localhost:3000/.svelte-kit/generated/nodes/0.js` error, try to turn off any ad blocker you have and reload the page ([reference](https://github.com/sveltejs/kit/issues/3308#issuecomment-1149942109)).
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
@@ -57,7 +76,7 @@ This will run tests in the `tests/` directory.
|
||||
|
||||
### Format
|
||||
|
||||
Code should like everywhere the same, for this reason a code-formatter is mandatory.
|
||||
Code should be consistently formatted everywhere. Before committing code, run the code-formatter.
|
||||
|
||||
```bash
|
||||
npm run format
|
||||
@@ -71,7 +90,7 @@ npm run lint
|
||||
|
||||
### Diagnostics
|
||||
|
||||
Diagnostic tool that checks for following:
|
||||
Diagnostic tool that checks for the following:
|
||||
|
||||
- Unused CSS
|
||||
- Svelte A11y hints
|
||||
@@ -83,7 +102,7 @@ npm run check
|
||||
|
||||
## Submit a Pull Request 🚀
|
||||
|
||||
Branch naming convention is as follows
|
||||
The branch naming convention is as follows
|
||||
|
||||
`TYPE-ISSUE_ID-DESCRIPTION`
|
||||
|
||||
@@ -101,11 +120,11 @@ When `TYPE` can be:
|
||||
- **fix** - a bug fix
|
||||
- **refactor** - code change that neither fixes a bug nor adds a feature
|
||||
|
||||
**All PRs must include a commit message with the description of the changes made!**
|
||||
**All PRs must include a commit message with a description of the changes made!**
|
||||
|
||||
Start by forking the project and use the `git clone` command to download the repository to your computer. A standard procedure for working on an issue would be to:
|
||||
|
||||
1. Before creating a new branch, pull the changes from upstream to make sure your default branch is be up to date.
|
||||
1. Before creating a new branch, pull the changes from upstream to make sure your default branch is up to date.
|
||||
|
||||
```
|
||||
$ git pull
|
||||
@@ -138,7 +157,7 @@ $ git push origin [name_of_your_new_branch]
|
||||
|
||||
### Consistency
|
||||
|
||||
Before commiting always make sure to run all available tools to improve the codebase:
|
||||
Before committing always make sure to run all available tools to improve the codebase:
|
||||
|
||||
- Formatter
|
||||
- `npm run format`
|
||||
@@ -153,21 +172,21 @@ Page load times are a key consideration for users of all browsers and device typ
|
||||
|
||||
There are some general things we can do in front-end development:
|
||||
|
||||
- Minimise HTTP requests
|
||||
- Minimise blocking – content should be readable before client side processing
|
||||
- Lazy load 'supplementary' content (especially images)
|
||||
- Minimize HTTP requests
|
||||
- Minimize blocking – content should be readable before client-side processing
|
||||
- Lazy load "supplementary" content, especially images
|
||||
|
||||
### Don't Repeat Yourself (DRY)
|
||||
|
||||
If you repeat anything that has already been defined in code, refactor it so that it only ever has one representation in the codebase.
|
||||
|
||||
If you stick to this principle, you will ensure that you will only ever need to change one implementation of a feature without worrying about needing to change any other part of the code.
|
||||
If you stick to this principle, you will ensure that you will only ever need to change one implementation of a feature without worrying about the need to change any other part of the code.
|
||||
|
||||
### Separation of concerns
|
||||
|
||||
Separate _structure_ from _presentation_ from _behaviour_ to aid maintainability and understanding.
|
||||
Separate _structure_ from _presentation_ from _behavior_ to aid maintainability and understanding.
|
||||
|
||||
- Keep CSS (presentation), JS (behaviour) and HTML (structure) in the same respective Svelte component
|
||||
- Keep CSS (presentation), JS (behavior) and HTML (structure) in the same respective Svelte component
|
||||
- Avoid writing inline CSS or Javascript in HTML
|
||||
- Avoid writing CSS or HTML in Javascript
|
||||
- Don't choose HTML elements to imply style
|
||||
@@ -176,29 +195,29 @@ Separate _structure_ from _presentation_ from _behaviour_ to aid maintainability
|
||||
|
||||
### Write code to be read
|
||||
|
||||
Follow the principles of ['Keep It Simple, Stupid'](http://en.wikipedia.org/wiki/KISS_principle) (KISS); hard to read or obfuscated code is difficult to maintain and debug. Don't be too clever; write code to be read.
|
||||
Follow the principles of ['Keep It Simple, Stupid'](http://en.wikipedia.org/wiki/KISS_principle) (KISS); hard-to-read or obfuscated code is difficult to maintain and debug. Don't be too clever; write code to be read.
|
||||
|
||||
### Identify technical debt
|
||||
|
||||
Use code comment annotations (`@todo`) to mark parts of your code that require further work. This will allow the measurement and management of technical debt.
|
||||
|
||||
Don't use `@fixme` (which defines things that are broken) - you shouldn't be committing broken code to the repo.
|
||||
Don't use `@fixme`, which defines things that are broken. Don't commit broken code in to the repo.
|
||||
|
||||
### Dependencies
|
||||
|
||||
Please avoid introducing new dependencies to Appwrite without consulting the team. New dependencies can be very helpful but also introduce new security and privacy issues, complexity, and impact total docker image size.
|
||||
Please avoid introducing new dependencies to Appwrite without consulting the team. New dependencies can be very helpful, but they also introduce new security and privacy issues, complexity, and impact total docker image size.
|
||||
|
||||
Adding a new dependency should have vital value on the product with minimum possible risk.
|
||||
Adding a new dependency should contribute vital value to the product with minimum possible risk.
|
||||
|
||||
## Introducing New Features
|
||||
|
||||
We would 💖 you to contribute to Appwrite, but we would also like to make sure Appwrite is as great as possible and loyal to its vision and mission statement 🙏.
|
||||
We would 💖 you to contribute to Appwrite, but we also want to ensure Appwrite is loyal to its vision and mission statement 🙏.
|
||||
|
||||
For us to find the right balance, please open an issue explaining your ideas before introducing a new pull request.
|
||||
|
||||
This will allow the Appwrite community to have sufficient discussion about the new feature value and how it fits in the product roadmap and vision.
|
||||
This will allow the Appwrite community to sufficiently discuss the new feature value and how it fits within the product roadmap and vision.
|
||||
|
||||
This is also important for Appwrite maintainers to discuss the feature's design and architecture to help the contribution fit in organically in the Appwrite codebase. Some bigger features might need to go through our [RFC process](https://github.com/appwrite/rfc).
|
||||
This is also important for the Appwrite lead developers to be able to provide technical input and potentially a different emphasis regarding the feature design and architecture. Some bigger features might need to go through our [RFC process](https://github.com/appwrite/rfc).
|
||||
|
||||
## Other Ways to Help
|
||||
|
||||
@@ -206,27 +225,27 @@ Pull requests are great, but there are many other areas where you can help Appwr
|
||||
|
||||
### Blogging & Speaking
|
||||
|
||||
Blogging, speaking about, or creating tutorials about one of Appwrite’s many features. Mention [@appwrite](https://twitter.com/appwrite) on Twitter and/or email [team@appwrite.io](mailto:team@appwrite.io) so we can give pointers and tips and help you spread the word by promoting your content on the different Appwrite communication channels. Please add your blog posts and videos of talks to our [Awesome Appwrite](https://github.com/appwrite/awesome-appwrite) repo on GitHub.
|
||||
When blogging, speaking about, or creating tutorials about one of Appwrite's many features, mention [@appwrite](https://twitter.com/appwrite) on Twitter and/or email [team@appwrite.io](mailto:team@appwrite.io) so we can give pointers and tips and help you spread the word by promoting your content on the different Appwrite communication channels. Please add your blog posts and videos of talks to our [Awesome Appwrite](https://github.com/appwrite/awesome-appwrite) repo on GitHub.
|
||||
|
||||
### Presenting at Meetups
|
||||
|
||||
Presenting at meetups and conferences about your Appwrite projects. Your unique challenges and successes in building things with Appwrite can provide great speaking material. We’d love to review your talk abstract/CFP, so get in touch with us if you’d like some help!
|
||||
Presenting at meetups and conferences about your Appwrite projects is another excellent way to get the word out about Appwrite. Your unique challenges and successes in building things with Appwrite can provide great speaking material. We’d love to review your talk abstract/CFP, so get in touch with us if you’d like some help!
|
||||
|
||||
### Sending Feedbacks & Reporting Bugs
|
||||
### Sending Feedback & Reporting Bugs
|
||||
|
||||
Sending feedback is a great way for us to understand your different use cases of Appwrite better. If you had any issues, bugs, or want to share about your experience, feel free to do so on our GitHub issues page or at our [Discord channel](https://discord.gg/GSeTUeA).
|
||||
Sending feedback is an excellent way for us to understand different use cases for Appwrite. If you have any issues or want to share your experience, feel free to do so on our GitHub issues page or our [Discord channel](https://discord.gg/GSeTUeA).
|
||||
|
||||
### Submitting New Ideas
|
||||
|
||||
If you think Appwrite could use a new feature, please open an issue on our GitHub repository, stating as much information as you can think about your new idea and it's implications. We would also use this issue to gather more information, get more feedback from the community, and have a proper discussion about the new feature.
|
||||
If you think Appwrite could use a new feature, please open an issue on our GitHub repository, stating as much information as you can think about your new idea and its implications. We would also use this issue to gather more information, get more feedback from the community, and have a proper discussion about the new feature.
|
||||
|
||||
### Improving Documentation
|
||||
|
||||
Submitting documentation updates, enhancements, designs, or bug fixes. Spelling or grammar fixes will be very much appreciated.
|
||||
Submitting documentation updates, enhancements, designs, or bug fixes help us to improve our documentation. Spelling or grammar fixes are also very much appreciated.
|
||||
|
||||
### Helping Someone
|
||||
|
||||
Searching for Appwrite on Discord, GitHub, or StackOverflow and helping someone else who needs help. You can also help by teaching others how to contribute to Appwrite's repo!
|
||||
You can also help by teaching others how to contribute to Appwrite's repo! Please consider searching for Appwrite on Discord, GitHub, or StackOverflow and helping someone else who needs help.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ The new console is more maintainable and welcoming to community contributions. C
|
||||
|
||||
## Contributing
|
||||
|
||||
All code contributions - including those of people having commit access - must go through a pull request and be approved by a core developer before being merged. This is to ensure a proper review of all the code.
|
||||
All code contributions, including those of people with commit access, must go through a pull request and be approved by a core developer before being merged. This is to ensure a proper review of all the code.
|
||||
|
||||
We truly ❤️ pull requests! If you wish to help, you can learn more about how you can contribute to this project in the [contribution guide](CONTRIBUTING.md).
|
||||
|
||||
@@ -45,7 +45,7 @@ For security issues, kindly email us at [security@appwrite.io](mailto:security@a
|
||||
|
||||
## Follow Us
|
||||
|
||||
Join our growing community around the world! See our official [Blog](https://medium.com/appwrite-io). Follow us on [Twitter](https://twitter.com/appwrite), [Facebook Page](https://www.facebook.com/appwrite.io), [Facebook Group](https://www.facebook.com/groups/appwrite.developers/), [Dev Community](https://dev.to/appwrite) or join our live [Discord server](https://appwrite.io/discord) for more help, ideas, and discussions.
|
||||
Join our growing community around the world! See our official [Blog](https://medium.com/appwrite-io). Follow us on [Twitter](https://twitter.com/appwrite), [Facebook Page](https://www.facebook.com/appwrite.io), [Facebook Group](https://www.facebook.com/groups/appwrite.developers/), and [Dev Community](https://dev.to/appwrite) or join our live [Discord server](https://appwrite.io/discord) for more help, ideas, and discussions.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
Generated
+2990
-7507
File diff suppressed because it is too large
Load Diff
+30
-29
@@ -20,47 +20,48 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@analytics/google-analytics": "^1.0.5",
|
||||
"analytics": "^0.8.1",
|
||||
"@aw-labs/appwrite-console": "^13.0.0",
|
||||
"@aw-labs/icons": "0.0.0-77",
|
||||
"@aw-labs/ui": "0.0.0-77",
|
||||
"@appwrite.io/pink": "^0.0.4",
|
||||
"@aw-labs/appwrite-console": "^13.1.0",
|
||||
"@popperjs/core": "^2.11.6",
|
||||
"@sentry/svelte": "^7.19.0",
|
||||
"@sentry/tracing": "^7.19.0",
|
||||
"echarts": "^5.4.0",
|
||||
"pretty-bytes": "^6.0.0",
|
||||
"@sentry/svelte": "^7.36.0",
|
||||
"@sentry/tracing": "^7.36.0",
|
||||
"analytics": "^0.8.1",
|
||||
"echarts": "^5.4.1",
|
||||
"logrocket": "^3.0.1",
|
||||
"pretty-bytes": "^6.1.0",
|
||||
"prismjs": "^1.29.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"web-vitals": "^3.0.3"
|
||||
"web-vitals": "^3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.27.1",
|
||||
"@sveltejs/adapter-static": "1.0.0-next.46",
|
||||
"@sveltejs/kit": "1.0.0-next.525",
|
||||
"@sveltejs/vite-plugin-svelte": "^1.1.0",
|
||||
"@testing-library/dom": "^8.19.0",
|
||||
"@playwright/test": "^1.30.0",
|
||||
"@sveltejs/adapter-static": "1.0.6",
|
||||
"@sveltejs/kit": "^1.3.10",
|
||||
"@sveltejs/vite-plugin-svelte": "^2.0.2",
|
||||
"@testing-library/dom": "^8.20.0",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/svelte": "^3.2.2",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/prismjs": "^1.26.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.41.0",
|
||||
"@typescript-eslint/parser": "^5.41.0",
|
||||
"@vitest/ui": "^0.24.3",
|
||||
"eslint": "^8.26.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.50.0",
|
||||
"@typescript-eslint/parser": "^5.50.0",
|
||||
"@vitest/ui": "^0.28.5",
|
||||
"eslint": "^8.33.0",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-plugin-svelte3": "^4.0.0",
|
||||
"jsdom": "^21.1.0",
|
||||
"pre-commit": "^1.2.2",
|
||||
"prettier": "^2.7.1",
|
||||
"prettier-plugin-svelte": "^2.8.0",
|
||||
"sass": "^1.55.0",
|
||||
"svelte": "^3.52.0",
|
||||
"svelte-check": "^2.9.2",
|
||||
"prettier": "^2.8.3",
|
||||
"prettier-plugin-svelte": "^2.9.0",
|
||||
"sass": "^1.58.0",
|
||||
"svelte": "^3.55.1",
|
||||
"svelte-check": "^3.0.3",
|
||||
"svelte-jester": "^2.3.2",
|
||||
"svelte-preprocess": "^4.10.7",
|
||||
"tslib": "^2.4.0",
|
||||
"typescript": "^4.8.4",
|
||||
"vite": "^3.2.1",
|
||||
"vitest": "^0.24.3"
|
||||
"svelte-preprocess": "^5.0.1",
|
||||
"tslib": "^2.5.0",
|
||||
"typescript": "^4.9.5",
|
||||
"vite": "^4.1.1",
|
||||
"vitest": "^0.28.5"
|
||||
},
|
||||
"type": "module",
|
||||
"pre-commit": [
|
||||
|
||||
@@ -4,6 +4,44 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="description" content="" />
|
||||
<link rel="icon" href="/favicon.png" />
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/inter/inter-v8-latin-600.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin />
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/inter/inter-v8-latin-regular.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin />
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/poppins/poppins-v19-latin-500.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin />
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/poppins/poppins-v19-latin-600.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin />
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/poppins/poppins-v19-latin-700.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin />
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/source-code-pro/source-code-pro-v20-latin-regular.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin />
|
||||
<link rel="preload" as="style" type="text/css" href="/fonts/main.css" />
|
||||
<link rel="stylesheet" href="/fonts/main.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
|
||||
+153
-15
@@ -1,15 +1,16 @@
|
||||
import Analytics from 'analytics';
|
||||
import googleAnalytics from '@analytics/google-analytics';
|
||||
import { get } from 'svelte/store';
|
||||
import { page } from '$app/stores';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { Mode } from '$lib/constants';
|
||||
import { ENV, MODE, VARS } from '$lib/system';
|
||||
import googleAnalytics from '@analytics/google-analytics';
|
||||
import { AppwriteException } from '@aw-labs/appwrite-console';
|
||||
import Analytics from 'analytics';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
const analytics = Analytics({
|
||||
app: 'appwrite',
|
||||
plugins: [
|
||||
googleAnalytics({
|
||||
measurementIds: [import.meta.env.VITE_GA_PROJECT?.toString() || 'G-R4YJ9JN8L4']
|
||||
measurementIds: [VARS.GOOGLE_ANALYTICS || 'G-R4YJ9JN8L4']
|
||||
})
|
||||
]
|
||||
});
|
||||
@@ -18,9 +19,32 @@ export function trackEvent(name: string, data: object = null): void {
|
||||
if (!isTrackingAllowed()) {
|
||||
return;
|
||||
}
|
||||
const path = get(page).routeId;
|
||||
analytics.track(name, { ...data, path });
|
||||
sendEventToGrowth(name, path, data);
|
||||
|
||||
const currentPage = get(page);
|
||||
const path = currentPage.route.id;
|
||||
|
||||
if (currentPage.params?.project) {
|
||||
data = {
|
||||
...data,
|
||||
project: currentPage.params.project
|
||||
};
|
||||
}
|
||||
|
||||
if (ENV.DEV || ENV.PREVIEW) {
|
||||
console.debug(`[Analytics] Event ${name} ${path}`, data);
|
||||
} else {
|
||||
analytics.track(name, { ...data, path });
|
||||
sendEventToGrowth(name, path, data);
|
||||
}
|
||||
}
|
||||
|
||||
export function trackError(exception: Error, event: Submit): void {
|
||||
if (exception instanceof AppwriteException && exception.type) {
|
||||
trackEvent(Submit.Error, {
|
||||
type: exception.type,
|
||||
form: event
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function trackPageView(path: string) {
|
||||
@@ -28,19 +52,24 @@ export function trackPageView(path: string) {
|
||||
return;
|
||||
}
|
||||
|
||||
analytics.page({
|
||||
path
|
||||
});
|
||||
if (ENV.DEV || ENV.PREVIEW) {
|
||||
console.debug(`[Analytics] Pageview ${path}`);
|
||||
} else {
|
||||
analytics.page({
|
||||
path
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function sendEventToGrowth(event: string, path: string, data: object = null): void {
|
||||
let email: string, name: string;
|
||||
if (!VARS.GROWTH_ENDPOINT) return;
|
||||
const userStore = get(user);
|
||||
let email: string, name: string;
|
||||
if (userStore) {
|
||||
email = userStore.email;
|
||||
name = userStore.name;
|
||||
}
|
||||
fetch('https://growth.appwrite.io/v1/analytics', {
|
||||
fetch(`${VARS.GROWTH_ENDPOINT}/analytics`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
@@ -49,7 +78,7 @@ function sendEventToGrowth(event: string, path: string, data: object = null): vo
|
||||
action: event,
|
||||
label: event,
|
||||
url: window.location.origin + path,
|
||||
account: import.meta.env.VITE_CONSOLE_MODE?.toString() || Mode.SELF_HOSTED,
|
||||
account: MODE,
|
||||
data: {
|
||||
email,
|
||||
name,
|
||||
@@ -59,7 +88,10 @@ function sendEventToGrowth(event: string, path: string, data: object = null): vo
|
||||
});
|
||||
}
|
||||
|
||||
function isTrackingAllowed() {
|
||||
export function isTrackingAllowed() {
|
||||
if (ENV.TEST) {
|
||||
return;
|
||||
}
|
||||
if (window.navigator?.doNotTrack) {
|
||||
if (navigator.doNotTrack === '1' || navigator.doNotTrack === 'yes') {
|
||||
return false;
|
||||
@@ -70,3 +102,109 @@ function isTrackingAllowed() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export enum Submit {
|
||||
Error = 'submit_error',
|
||||
AccountCreate = 'submit_account_create',
|
||||
AccountLogin = 'submit_account_login',
|
||||
AccountLogout = 'submit_account_logout',
|
||||
AccountRecover = 'submit_account_recover',
|
||||
AccountUpdateName = 'submit_account_update_name',
|
||||
AccountUpdateEmail = 'submit_account_update_email',
|
||||
AccountUpdatePassword = 'submit_account_update_password',
|
||||
AccountDelete = 'submit_account_delete',
|
||||
AccountDeleteSession = 'submit_account_delete_session',
|
||||
AccountDeleteAllSessions = 'submit_account_delete_all_sessions',
|
||||
UserCreate = 'submit_user_create',
|
||||
UserDelete = 'submit_user_delete',
|
||||
UserUpdateEmail = 'submit_user_update_email',
|
||||
UserUpdateName = 'submit_user_update_name',
|
||||
UserUpdatePassword = 'submit_user_update_password',
|
||||
UserUpdatePhone = 'submit_user_update_phone',
|
||||
UserUpdatePreferences = 'submit_user_update_preferences',
|
||||
UserUpdateStatus = 'submit_user_update_status',
|
||||
UserUpdateVerificationEmail = 'submit_user_update_verification_email',
|
||||
UserUpdateVerificationPhone = 'submit_user_update_verification_phone',
|
||||
OrganizationCreate = 'submit_organization_create',
|
||||
OrganizationDelete = 'submit_organization_delete',
|
||||
OrganizationUpdateName = 'submit_organization_update_name',
|
||||
ProjectCreate = 'submit_project_create',
|
||||
ProjectDelete = 'submit_project_delete',
|
||||
ProjectUpdateName = 'submit_project_update_name',
|
||||
ProjectService = 'submit_project_service',
|
||||
MemberCreate = 'submit_member_create',
|
||||
MemberDelete = 'submit_member_delete',
|
||||
MembershipUpdateStatus = 'submit_membership_update_status',
|
||||
ProviderUpdate = 'submit_provider_update',
|
||||
TeamCreate = 'submit_team_create',
|
||||
TeamDelete = 'submit_team_delete',
|
||||
TeamUpdateName = 'submit_team_update_name',
|
||||
AuthLimitUpdate = 'submit_auth_limit_update',
|
||||
AuthStatusUpdate = 'submit_auth_status_update',
|
||||
SessionsLengthUpdate = 'submit_sessions_length_update',
|
||||
SessionsLimitUpdate = 'submit_sessions_limit_update',
|
||||
SessionDelete = 'submit_session_delete',
|
||||
SessionDeleteAll = 'submit_session_delete_all',
|
||||
DatabaseCreate = 'submit_database_create',
|
||||
DatabaseDelete = 'submit_database_delete',
|
||||
DatabaseUpdateName = 'submit_database_update_name',
|
||||
AttributeCreate = 'submit_attribute_create',
|
||||
AttributeDelete = 'submit_attribute_delete',
|
||||
DocumentCreate = 'submit_document_create',
|
||||
DocumentDelete = 'submit_document_delete',
|
||||
DocumentUpdate = 'submit_document_update',
|
||||
DocumentUpdatePermissions = 'submit_document_update_permissions',
|
||||
IndexCreate = 'submit_index_create',
|
||||
IndexDelete = 'submit_index_delete',
|
||||
CollectionCreate = 'submit_collection_create',
|
||||
CollectionDelete = 'submit_collection_delete',
|
||||
CollectionUpdateName = 'submit_collection_update_name',
|
||||
CollectionUpdatePermissions = 'submit_collection_update_permissions',
|
||||
CollectionUpdateSecurity = 'submit_collection_update_security',
|
||||
CollectionUpdateEnabled = 'submit_collection_update_enabled',
|
||||
FunctionCreate = 'submit_function_create',
|
||||
FunctionDelete = 'submit_function_delete',
|
||||
FunctionUpdateName = 'submit_function_update_name',
|
||||
FunctionUpdatePermissions = 'submit_function_update_permissions',
|
||||
FunctionUpdateSchedule = 'submit_function_update_schedule',
|
||||
FunctionUpdateTimeout = 'submit_function_update_timeout',
|
||||
FunctionUpdateEvents = 'submit_function_update_events',
|
||||
DeploymentCreate = 'submit_deployment_create',
|
||||
DeploymentDelete = 'submit_deployment_delete',
|
||||
DeploymentUpdate = 'submit_deployment_update',
|
||||
ExecutionCreate = 'submit_execution_create',
|
||||
VariableCreate = 'submit_variable_create',
|
||||
VariableDelete = 'submit_variable_delete',
|
||||
VariableUpdate = 'submit_variable_update',
|
||||
KeyCreate = 'submit_key_create',
|
||||
KeyDelete = 'submit_key_delete',
|
||||
KeyUpdateName = 'submit_key_update_name',
|
||||
KeyUpdateScopes = 'submit_key_update_scopes',
|
||||
KeyUpdateExpire = 'submit_key_update_expire',
|
||||
PlatformCreate = 'submit_platform_create',
|
||||
PlatformDelete = 'submit_platform_delete',
|
||||
PlatformUpdate = 'submit_platform_update',
|
||||
DomainCreate = 'submit_domain_create',
|
||||
DomainDelete = 'submit_domain_delete',
|
||||
DomainUpdateVerification = 'submit_domain_update_verification',
|
||||
WebhookCreate = 'submit_webhook_create',
|
||||
WebhookDelete = 'submit_webhook_delete',
|
||||
WebhookUpdateSignature = 'submit_webhook_update_signature',
|
||||
WebhookUpdateUrl = 'submit_webhook_update_url',
|
||||
WebhookUpdateEvents = 'submit_webhook_update_events',
|
||||
WebhookUpdateName = 'submit_webhook_update_name',
|
||||
WebhookUpdateSecurity = 'submit_webhook_update_security',
|
||||
BucketCreate = 'submit_bucket_create',
|
||||
BucketDelete = 'submit_bucket_delete',
|
||||
BucketUpdateEnabled = 'submit_bucket_update_enabled',
|
||||
BucketUpdateName = 'submit_bucket_update_name',
|
||||
BucketUpdatePermissions = 'submit_bucket_update_permissions',
|
||||
BucketUpdateSecurity = 'submit_bucket_update_security',
|
||||
BucketUpdateFileSecurity = 'submit_bucket_update_file_security',
|
||||
BucketUpdateSize = 'submit_bucket_update_size',
|
||||
BucketUpdateCompression = 'submit_bucket_update_compression',
|
||||
BucketUpdateExtensions = 'submit_bucket_update_extensions',
|
||||
FileCreate = 'submit_file_create',
|
||||
FileDelete = 'submit_file_delete',
|
||||
FileUpdatePermissions = 'submit_file_update_permissions'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { Action } from 'svelte/action';
|
||||
|
||||
type Props = {
|
||||
onChange: (selectionStart: number) => void;
|
||||
};
|
||||
|
||||
export const selectionStart: Action<HTMLInputElement, Props> = (node, { onChange }) => {
|
||||
const handler = () => {
|
||||
onChange(node.selectionStart);
|
||||
};
|
||||
node.addEventListener('click', handler);
|
||||
node.addEventListener('keydown', handler);
|
||||
node.addEventListener('keyup', handler);
|
||||
|
||||
return {
|
||||
update({ onChange }) {
|
||||
const handler = () => {
|
||||
onChange(node.selectionStart);
|
||||
};
|
||||
node.addEventListener('click', handler);
|
||||
node.addEventListener('keydown', handler);
|
||||
node.addEventListener('keyup', handler);
|
||||
},
|
||||
destroy() {
|
||||
onChange(-1);
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import { calculateTime } from '$lib/helpers/timeConversion';
|
||||
import type { Action } from 'svelte/action';
|
||||
|
||||
type Props = {
|
||||
start?: string;
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
const defaultProps: Props = {
|
||||
enabled: true
|
||||
};
|
||||
|
||||
export const timer: Action<HTMLElement, Props> = (node, props) => {
|
||||
let startDate: Date;
|
||||
let frame: number;
|
||||
|
||||
function init(props: Props) {
|
||||
const { start, enabled } = { ...defaultProps, ...props };
|
||||
startDate = start ? new Date(start) : new Date();
|
||||
frame = enabled ? window.requestAnimationFrame(step) : null;
|
||||
}
|
||||
|
||||
function step() {
|
||||
const diffInSeconds = Math.floor((new Date().getTime() - startDate.getTime()) / 1000);
|
||||
node.textContent = calculateTime(diffInSeconds);
|
||||
frame = window.requestAnimationFrame(step);
|
||||
}
|
||||
|
||||
init(props);
|
||||
|
||||
return {
|
||||
update(props) {
|
||||
window.cancelAnimationFrame(frame);
|
||||
init(props);
|
||||
},
|
||||
destroy() {
|
||||
window.cancelAnimationFrame(frame);
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -1,17 +1,24 @@
|
||||
import type { Action } from 'svelte/action';
|
||||
import type { Props } from 'tippy.js';
|
||||
import type { Props as TippyProps } from 'tippy.js';
|
||||
import tippy from 'tippy.js';
|
||||
|
||||
type Props = TippyProps & {
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const tooltip: Action<HTMLElement, Partial<Props>> = (node, config) => {
|
||||
const instance = tippy(node, config);
|
||||
if (config.disabled) instance.disable();
|
||||
|
||||
return {
|
||||
update({ content }) {
|
||||
update({ content, disabled }) {
|
||||
if (content !== instance.props.content) {
|
||||
instance.setProps({
|
||||
content
|
||||
});
|
||||
}
|
||||
|
||||
disabled ? instance.disable() : instance.enable();
|
||||
},
|
||||
destroy() {
|
||||
instance.destroy();
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import type { ECharts } from 'echarts/core';
|
||||
import type { BarSeriesOption, LineSeriesOption } from 'echarts/charts';
|
||||
import type { EChartsOption } from 'echarts';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
|
||||
registerTheme('light', { ...base, ...light });
|
||||
registerTheme('dark', { ...base, ...dark });
|
||||
@@ -70,7 +71,7 @@
|
||||
|
||||
let timeoutId: unknown;
|
||||
const onResize = () => {
|
||||
if (timeoutId == undefined) {
|
||||
if (timeoutId == undefined && !$wizard.show) {
|
||||
timeoutId = setTimeout(() => {
|
||||
if (chart && !chart.isDisposed()) {
|
||||
chart.resize();
|
||||
@@ -79,6 +80,10 @@
|
||||
}, 250);
|
||||
}
|
||||
};
|
||||
|
||||
$: if (!$wizard.show) {
|
||||
onResize();
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:resize={onResize} />
|
||||
|
||||
@@ -119,6 +119,11 @@
|
||||
"crossStyle": {
|
||||
"color": "#444444"
|
||||
}
|
||||
},
|
||||
"backgroundColor": "#373B4D",
|
||||
"borderColor": "#373B4D",
|
||||
"textStyle": {
|
||||
"color": "#F2F2F8"
|
||||
}
|
||||
},
|
||||
"timeline": {
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
{#if dismissible}
|
||||
<button
|
||||
type="button"
|
||||
class="x-button"
|
||||
class="button is-text is-only-icon"
|
||||
style="--button-size:1.5rem;"
|
||||
aria-label="close alert box"
|
||||
on:click={() => dispatch('dismiss')}>
|
||||
<span class="icon-x" aria-hidden="true" />
|
||||
@@ -31,15 +32,15 @@
|
||||
class:icon-exclamation={type === 'warning'}
|
||||
class:icon-exclamation-circle={type === 'error'}
|
||||
aria-hidden="true" />
|
||||
<div class="content">
|
||||
<div class="alert-content" data-private>
|
||||
{#if $$slots.title}
|
||||
<h6 class="title">
|
||||
<h6 class="alert-title">
|
||||
<slot name="title" />
|
||||
</h6>
|
||||
{/if}
|
||||
<p class="message"><slot /></p>
|
||||
<p class="alert-message"><slot /></p>
|
||||
{#if buttons?.length}
|
||||
<div class="buttons u-flex">
|
||||
<div class="alert-buttons u-flex">
|
||||
{#each buttons as button}
|
||||
<button class="button is-text" on:click={button.method}>
|
||||
<span class="text">{button.name}</span>
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
export let danger = false;
|
||||
export let href: string = null;
|
||||
|
||||
//allows to add the href attribute without throwing an error
|
||||
let attributes = { href } as Record<string, string>;
|
||||
|
||||
function getElement(): string {
|
||||
switch (true) {
|
||||
case !!href:
|
||||
@@ -27,6 +30,6 @@
|
||||
class:is-danger={danger}
|
||||
on:click
|
||||
on:keyup={clickOnEnter}
|
||||
{href}>
|
||||
{...attributes}>
|
||||
<slot />
|
||||
</svelte:element>
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
|
||||
<ul
|
||||
class="grid-box common-section u-margin-block-start-32"
|
||||
style={`--grid-gap:1.5rem; --grid-item-size:${total > 3 ? '22rem' : '25rem'};`}>
|
||||
style={`--grid-gap:1.5rem; --grid-item-size:${total > 3 ? '22rem' : '25rem'};`}
|
||||
data-private>
|
||||
<slot />
|
||||
|
||||
{#if total > 3 ? total < limit + offset : total % 2 !== 0}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script lang="ts">
|
||||
export let withIndentation = false;
|
||||
export let open = false;
|
||||
</script>
|
||||
|
||||
<li class="collapsible-item">
|
||||
<details class="collapsible-wrapper">
|
||||
<details class="collapsible-wrapper" {open}>
|
||||
<summary class="collapsible-button">
|
||||
<slot name="beforetitle" />
|
||||
<div>
|
||||
|
||||
@@ -2,39 +2,44 @@
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
import { clickOnEnter } from '$lib/helpers/a11y';
|
||||
import { copy } from '$lib/helpers/copy';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
|
||||
export let value: string;
|
||||
export let event: string = null;
|
||||
export let eventContext = 'click_id_tag';
|
||||
export let appendTo: Parameters<typeof tooltip>['1']['appendTo'] = undefined;
|
||||
|
||||
let content = 'Click to copy';
|
||||
|
||||
const copy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
async function handleClick() {
|
||||
const success = await copy(value);
|
||||
|
||||
if (success) {
|
||||
content = 'Copied';
|
||||
} catch (error) {
|
||||
} else {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
message: 'Unable to copy to clipboard',
|
||||
type: 'error'
|
||||
});
|
||||
} finally {
|
||||
if (event) {
|
||||
trackEvent('click_id_tag', {
|
||||
name: event
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (event) {
|
||||
trackEvent(eventContext, {
|
||||
name: event
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<span
|
||||
on:click|preventDefault={copy}
|
||||
on:click|preventDefault={handleClick}
|
||||
on:keyup={clickOnEnter}
|
||||
on:mouseenter={() => setTimeout(() => (content = 'Click to copy'))}
|
||||
use:tooltip={{
|
||||
content,
|
||||
hideOnClick: false
|
||||
hideOnClick: false,
|
||||
appendTo
|
||||
}}>
|
||||
<slot />
|
||||
</span>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
import { copy } from '$lib/helpers/copy';
|
||||
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
|
||||
@@ -9,13 +10,14 @@
|
||||
|
||||
let content = 'Click to copy';
|
||||
|
||||
const copy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
const handleCopy = async () => {
|
||||
const success = await copy(value);
|
||||
|
||||
if (success) {
|
||||
content = 'Copied';
|
||||
} catch (error) {
|
||||
} else {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
message: 'Unable to copy to clipboard',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
@@ -29,9 +31,9 @@
|
||||
<div class="options-list">
|
||||
<button
|
||||
type="button"
|
||||
class="input-button"
|
||||
class="options-list-button"
|
||||
aria-label="Click to copy."
|
||||
on:click={copy}
|
||||
on:click={handleCopy}
|
||||
on:mouseenter={() => setTimeout(() => (content = 'Click to copy'))}
|
||||
use:tooltip={{
|
||||
content,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { InnerModal } from '$lib/components';
|
||||
import { InputText } from '$lib/elements/forms';
|
||||
import { InputId } from '$lib/elements/forms';
|
||||
|
||||
export let show = false;
|
||||
export let name: string;
|
||||
@@ -27,22 +27,7 @@
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content">
|
||||
<div class="form">
|
||||
<InputText
|
||||
id="id"
|
||||
label="Custom ID"
|
||||
showLabel={false}
|
||||
placeholder="Enter ID"
|
||||
autofocus={true}
|
||||
bind:value={id} />
|
||||
|
||||
<div class="u-flex u-gap-4 u-margin-block-start-8 u-small">
|
||||
<span
|
||||
class="icon-info u-cross-center u-margin-block-start-2 u-line-height-1 u-icon-small"
|
||||
aria-hidden="true" />
|
||||
<span class="text u-line-height-1-5">
|
||||
Allowed characters: alphanumeric, hyphen, non-leading underscore, period
|
||||
</span>
|
||||
</div>
|
||||
<InputId bind:value={id} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</InnerModal>
|
||||
|
||||
@@ -4,10 +4,13 @@
|
||||
export let href: string;
|
||||
export let icon: string = null;
|
||||
export let disabled = false;
|
||||
|
||||
//allows to add the disabled attribute to <a> tag without throwing an error
|
||||
let attributes = { disabled } as Record<string, boolean>;
|
||||
</script>
|
||||
|
||||
<li class="drop-list-item" on:click on:keyup={clickOnEnter}>
|
||||
<a {href} class="drop-button" {disabled}>
|
||||
<a {href} class="drop-button" {...attributes}>
|
||||
<span class="text"><slot /></span>
|
||||
{#if icon}
|
||||
<span class={`icon-${icon}`} aria-hidden="true" />
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<ul class="drop-tabs">
|
||||
<slot />
|
||||
</ul>
|
||||
@@ -1,9 +0,0 @@
|
||||
<script lang="ts">
|
||||
export let disabled = false;
|
||||
</script>
|
||||
|
||||
<li class="drop-tabs-item">
|
||||
<button class="drop-tabs-button" {disabled} on:click>
|
||||
<span class="text"><slot /></span>
|
||||
</button>
|
||||
</li>
|
||||
@@ -1,16 +0,0 @@
|
||||
<script lang="ts">
|
||||
export let href: string;
|
||||
export let disabled = false;
|
||||
</script>
|
||||
|
||||
<li class="drop-tabs-item">
|
||||
{#if disabled}
|
||||
<button class="drop-tabs-button" disabled>
|
||||
<span class="text"><slot /></span>
|
||||
</button>
|
||||
{:else}
|
||||
<a class="drop-tabs-button" {href}>
|
||||
<span class="text"><slot /></span>
|
||||
</a>
|
||||
{/if}
|
||||
</li>
|
||||
@@ -1,320 +1,376 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { Modal, Copy } from '$lib/components';
|
||||
import { Modal } from '$lib/components';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { at, empty } from '$lib/helpers/array';
|
||||
import Copy from './copy.svelte';
|
||||
import { selectionStart } from '$lib/actions/selectionStart';
|
||||
import { singular } from '$lib/helpers/string';
|
||||
import {
|
||||
eventServices as services,
|
||||
type EventAction,
|
||||
type EventResource,
|
||||
type EventService
|
||||
} from '$lib/constants';
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
|
||||
// Props
|
||||
export let show = false;
|
||||
export let initialValue = '';
|
||||
|
||||
type Service = {
|
||||
name: string;
|
||||
resources: Resource[];
|
||||
actions?: Action[];
|
||||
};
|
||||
// Constants
|
||||
const serviceNames = services.map((s) => s.name);
|
||||
const resourceNames = services.flatMap((s) => s.resources.map((r) => r.name));
|
||||
const actionNames = [
|
||||
...services.flatMap((s) => s.actions.map((a) => a.name)),
|
||||
...services.flatMap((s) => s.resources.flatMap((r) => r.actions.map((a) => a.name)))
|
||||
];
|
||||
const attributeNames = [
|
||||
...services.flatMap((s) => s.actions.flatMap((a) => a.attributes ?? [])),
|
||||
...services.flatMap((s) =>
|
||||
s.resources.flatMap((r) => r.actions.flatMap((a) => a.attributes ?? []))
|
||||
)
|
||||
];
|
||||
|
||||
type Resource = {
|
||||
name: string;
|
||||
actions?: Action[];
|
||||
attributes?: string[];
|
||||
};
|
||||
|
||||
type Action = {
|
||||
name: string;
|
||||
attributes?: string[];
|
||||
};
|
||||
|
||||
let selectedService: Service = null;
|
||||
let selectedResource: Resource = null;
|
||||
let selectedAction: Action = null;
|
||||
let selectedAttribute: string = null;
|
||||
let helper: string = null;
|
||||
let inputData: string = null;
|
||||
let showInput = false;
|
||||
// Helpers
|
||||
const isService = (s: string) => serviceNames.includes(s);
|
||||
const isResource = (s: string) => resourceNames.includes(s);
|
||||
const isAction = (s: string) => actionNames.includes(s);
|
||||
const isAttribute = (s: string) => attributeNames.includes(s);
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function create() {
|
||||
show = false;
|
||||
dispatch('created', copyValue);
|
||||
trackEvent('events_add', {
|
||||
value: inputValue
|
||||
});
|
||||
dispatch('created', inputValue);
|
||||
}
|
||||
|
||||
const actions = [{ name: 'create' }, { name: 'update' }, { name: 'delete' }];
|
||||
|
||||
const services = [
|
||||
{
|
||||
name: 'buckets',
|
||||
resources: [{ name: 'files', actions }],
|
||||
actions
|
||||
},
|
||||
{
|
||||
name: 'databases',
|
||||
resources: [
|
||||
{ name: 'collections', actions },
|
||||
{ name: 'documents', actions }
|
||||
],
|
||||
actions
|
||||
},
|
||||
{
|
||||
name: 'functions',
|
||||
resources: [
|
||||
{ name: 'deployments', actions },
|
||||
{ name: 'executions', actions }
|
||||
],
|
||||
actions
|
||||
},
|
||||
{
|
||||
name: 'teams',
|
||||
resources: [{ name: 'memberships', actions }],
|
||||
actions: [
|
||||
{ name: 'create' },
|
||||
{ name: 'update', attributes: ['status'] },
|
||||
{ name: 'delete' }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'users',
|
||||
resources: [
|
||||
{ name: 'recovery', actions: [{ name: 'create' }, { name: 'delete' }] },
|
||||
{ name: 'sessions', actions: [{ name: 'create' }, { name: 'delete' }] },
|
||||
{ name: 'verifications', actions: [{ name: 'create' }, { name: 'delete' }] }
|
||||
],
|
||||
actions: [
|
||||
{ name: 'create' },
|
||||
{ name: 'update', attributes: ['email', 'name', 'password', 'status', 'prefs'] },
|
||||
{ name: 'delete' }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
function createEventString(
|
||||
service: Service,
|
||||
resource: Resource,
|
||||
action: Action,
|
||||
attribute: string
|
||||
) {
|
||||
const data = new Map();
|
||||
if (service) {
|
||||
data.set('service', { value: service.name, description: 'service' });
|
||||
data.set('serviceId', {
|
||||
value: '*',
|
||||
description: `ID of ${service?.name.slice(0, -1)}`
|
||||
});
|
||||
function select(field: 'service', value: EventService): void;
|
||||
function select(field: 'resource', value: EventResource): void;
|
||||
function select(field: 'action', value: EventAction): void;
|
||||
function select(field: 'attribute', value: string): void;
|
||||
function select(
|
||||
field: string,
|
||||
value: string | EventService | EventResource | EventAction
|
||||
): void {
|
||||
if (typeof value === 'string') {
|
||||
selected[field as 'attribute'] = selected[field] === value ? null : value;
|
||||
} else {
|
||||
selected[field] = selected[field]?.name === value.name ? null : value;
|
||||
}
|
||||
|
||||
if (resource) {
|
||||
if (resource.name === 'documents') {
|
||||
data.set('resource', {
|
||||
value: 'collections',
|
||||
description: 'resource'
|
||||
});
|
||||
data.set('resourceId', {
|
||||
value: '*',
|
||||
description: `ID of collection`
|
||||
});
|
||||
data.set('secondaryResource', {
|
||||
value: 'documents',
|
||||
description: 'secondary resource'
|
||||
});
|
||||
data.set('secondaryResourceId', {
|
||||
value: '*',
|
||||
description: `ID of document`
|
||||
});
|
||||
} else {
|
||||
data.set('resource', {
|
||||
value: resource.name,
|
||||
description: `resource`
|
||||
});
|
||||
data.set('resourceId', {
|
||||
value: '*',
|
||||
description: `ID of ${resource?.name.slice(0, -1)}`
|
||||
});
|
||||
customInput = null;
|
||||
|
||||
switch (field) {
|
||||
case 'service': {
|
||||
selected.resource = null;
|
||||
selected.action = null;
|
||||
selected.attribute = null;
|
||||
break;
|
||||
}
|
||||
case 'resource': {
|
||||
const availableActions = selected.resource
|
||||
? selected.resource?.actions
|
||||
: selected.service?.actions;
|
||||
|
||||
// We need to check if the selected action name is still
|
||||
// on the available list, and if so, change tje action to the
|
||||
// appropriate one, as it may contain different attributes
|
||||
const availableAction = availableActions.find(
|
||||
(a) => a.name === selected.action?.name
|
||||
);
|
||||
|
||||
if (!availableAction) {
|
||||
selected.action = null;
|
||||
selected.attribute = null;
|
||||
} else {
|
||||
selected.action = availableAction;
|
||||
|
||||
if (!selected?.action?.attributes?.includes(selected.attribute)) {
|
||||
selected.attribute = null;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'action': {
|
||||
selected.attribute = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resetSelected() {
|
||||
Object.keys(selected).forEach((key) => (selected[key] = null));
|
||||
}
|
||||
|
||||
function inferSelectedFromCustomInput() {
|
||||
resetSelected();
|
||||
for (const field of customInput.split('.')) {
|
||||
if (isService(field)) {
|
||||
selected.service = services.find((s) => s.name === field);
|
||||
} else if (isResource(field)) {
|
||||
const resource = selected.service?.resources.find((r) => r.name === field);
|
||||
selected.resource = resource;
|
||||
} else if (isAction(field)) {
|
||||
const action =
|
||||
selected.resource?.actions.find((a) => a.name === field) ||
|
||||
selected.service?.actions.find((a) => a.name === field);
|
||||
selected.action = action;
|
||||
} else if (isAttribute(field)) {
|
||||
const attribute = selected.action?.attributes?.find((a) => a === field);
|
||||
selected.attribute = attribute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleShowInput(e: Event) {
|
||||
e.preventDefault();
|
||||
|
||||
showInput = !showInput;
|
||||
if (showInput) return;
|
||||
|
||||
helper = null;
|
||||
inferSelectedFromCustomInput();
|
||||
}
|
||||
|
||||
function getHelperStr(fields: string[], index: number) {
|
||||
const currField = at(fields, index);
|
||||
const prevField = at(fields, index - 1);
|
||||
const secondToLastField = at(fields, index - 2);
|
||||
|
||||
if (index === 0 || isService(currField)) return 'service';
|
||||
if (isAttribute(currField) || isAction(prevField)) return 'attribute';
|
||||
if (isAction(currField)) return 'action';
|
||||
if (isResource(currField)) return 'resource';
|
||||
if (isService(prevField) || isResource(prevField) || index === 1) {
|
||||
return `ID of ${singular(prevField)}`;
|
||||
}
|
||||
// TODO: Find better name for 'resource or action'
|
||||
if (isService(secondToLastField) || index === 2) return 'resource or action';
|
||||
if (isResource(secondToLastField)) return 'action';
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function getCustomInputHelperStr(input: string, selectionStart?: number): string {
|
||||
const fields = input.split('.');
|
||||
let fieldIndex = -1;
|
||||
|
||||
if (selectionStart > -1) {
|
||||
let currentIndex = 0;
|
||||
let arrayIndex = 0;
|
||||
|
||||
for (const item of fields) {
|
||||
currentIndex += item.length + 1;
|
||||
if (currentIndex > selectionStart) {
|
||||
fieldIndex = arrayIndex;
|
||||
break;
|
||||
}
|
||||
arrayIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
if (action) {
|
||||
data.set('action', { value: action.name, description: 'action' });
|
||||
return getHelperStr(fields, fieldIndex);
|
||||
}
|
||||
|
||||
// State & Reactive Declarations
|
||||
let selected = {
|
||||
service: null as EventService | null,
|
||||
resource: null as EventResource | null,
|
||||
action: null as EventAction | null,
|
||||
attribute: null as string | null
|
||||
};
|
||||
let helper: string = null;
|
||||
let customInput: string = null;
|
||||
let customInputCursor = -1;
|
||||
let showInput = false;
|
||||
let copyParent: HTMLElement;
|
||||
|
||||
$: if (show && initialValue) {
|
||||
customInput = initialValue;
|
||||
inferSelectedFromCustomInput();
|
||||
}
|
||||
|
||||
$: available = {
|
||||
services: services,
|
||||
resources: selected.service?.resources || [],
|
||||
actions: (selected.resource ? selected.resource?.actions : selected.service?.actions) || [],
|
||||
attributes: selected.action?.attributes || []
|
||||
};
|
||||
|
||||
$: eventString = (function createEventString(): Array<{ value: string; description: string }> {
|
||||
let fields: string[] = [];
|
||||
|
||||
// Avoid calculating the event string if the user is typing, as it is not shown
|
||||
if (showInput) return [];
|
||||
if (customInput) {
|
||||
fields = customInput.split('.');
|
||||
} else {
|
||||
if (selected.service) {
|
||||
fields.push(selected.service.name, '*');
|
||||
}
|
||||
if (selected.resource?.name === 'documents') {
|
||||
fields.push('collections', '*');
|
||||
}
|
||||
if (selected.resource) {
|
||||
fields.push(selected.resource.name, '*');
|
||||
}
|
||||
if (selected.action) {
|
||||
fields.push(selected.action.name);
|
||||
}
|
||||
if (selected.attribute) {
|
||||
fields.push(selected.attribute);
|
||||
}
|
||||
}
|
||||
|
||||
if (attribute && !resource) {
|
||||
data.set('attribute', { value: attribute, description: 'attribute' });
|
||||
} else if (action?.attributes && !resource) {
|
||||
data.set('attribute', { value: '*', description: `attribute` });
|
||||
}
|
||||
return data;
|
||||
}
|
||||
return fields.map((value, i, arr) => ({ value, description: getHelperStr(arr, i) }));
|
||||
})();
|
||||
|
||||
$: eventString = createEventString(
|
||||
selectedService,
|
||||
selectedResource,
|
||||
selectedAction,
|
||||
selectedAttribute
|
||||
);
|
||||
$: copyValue =
|
||||
inputData ??
|
||||
Array.from(eventString.values())
|
||||
.map((d) => d.value)
|
||||
.join('.');
|
||||
|
||||
$: if (selectedService) {
|
||||
selectedResource = null;
|
||||
selectedAction = null;
|
||||
selectedAttribute = null;
|
||||
helper = null;
|
||||
}
|
||||
|
||||
$: if (!selectedAction?.attributes) {
|
||||
selectedAttribute = null;
|
||||
}
|
||||
|
||||
$: if (!showInput) {
|
||||
helper = null;
|
||||
}
|
||||
$: inputValue = eventString.map((d) => d.value).join('.');
|
||||
|
||||
$: if (!show) {
|
||||
selectedService = null;
|
||||
selectedResource = null;
|
||||
selectedAction = null;
|
||||
selectedAttribute = null;
|
||||
resetSelected();
|
||||
helper = null;
|
||||
inputData = null;
|
||||
customInput = null;
|
||||
showInput = false;
|
||||
}
|
||||
|
||||
$: if (showInput) {
|
||||
helper = getCustomInputHelperStr(customInput, customInputCursor);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:show on:submit={create} size="big">
|
||||
<svelte:fragment slot="header">Create Event</svelte:fragment>
|
||||
|
||||
<slot />
|
||||
<div>
|
||||
<p class="u-text">Choose a service</p>
|
||||
<div class="u-flex u-gap-8 u-margin-block-start-8">
|
||||
{#each services as service}
|
||||
{#each available.services as service}
|
||||
<Pill
|
||||
disabled={showInput}
|
||||
selected={service.name === selectedService?.name}
|
||||
selected={selected.service?.name === service.name}
|
||||
button
|
||||
on:click={() => {
|
||||
selectedService = service;
|
||||
inputData = null;
|
||||
}}>{service.name}</Pill>
|
||||
on:click={() => select('service', service)}>
|
||||
{service.name}
|
||||
</Pill>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{#if selectedService}
|
||||
|
||||
{#if !empty(available.resources)}
|
||||
<div>
|
||||
<p class="u-text">Choose a resource (optional)</p>
|
||||
<div class="u-flex u-gap-8 u-margin-block-start-8">
|
||||
{#each selectedService.resources as resource}
|
||||
{#each available.resources as resource}
|
||||
<Pill
|
||||
disabled={showInput}
|
||||
selected={resource.name === selectedResource?.name}
|
||||
selected={selected.resource?.name === resource.name}
|
||||
button
|
||||
on:click={() => {
|
||||
selectedResource === resource
|
||||
? (selectedResource = null)
|
||||
: (selectedResource = resource);
|
||||
inputData = null;
|
||||
}}>{resource.name}</Pill>
|
||||
on:click={() => select('resource', resource)}>
|
||||
{resource.name}
|
||||
</Pill>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !empty(available.actions)}
|
||||
<div>
|
||||
<p class="u-text">Choose an action (optional)</p>
|
||||
<div class="u-flex u-gap-8 u-margin-block-start-8">
|
||||
{#each selectedResource?.actions ?? selectedService?.actions as action}
|
||||
{#each available.actions as action}
|
||||
<Pill
|
||||
disabled={showInput}
|
||||
selected={selectedAction === action}
|
||||
selected={selected.action?.name === action.name}
|
||||
button
|
||||
on:click={() => {
|
||||
selectedAction === action
|
||||
? (selectedAction = null)
|
||||
: (selectedAction = action);
|
||||
inputData = null;
|
||||
}}>{action.name}</Pill>
|
||||
on:click={() => select('action', action)}>
|
||||
{action.name}
|
||||
</Pill>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{#if selectedAction?.attributes}
|
||||
<div>
|
||||
<p class="u-text">Choose an attribute (optional)</p>
|
||||
<div class="u-flex u-gap-8 u-margin-block-start-8">
|
||||
{#each selectedAction.attributes as attribute}
|
||||
<Pill
|
||||
disabled={showInput}
|
||||
selected={selectedAttribute === attribute}
|
||||
button
|
||||
on:click={() => {
|
||||
selectedAttribute === attribute
|
||||
? (selectedAttribute = null)
|
||||
: (selectedAttribute = attribute);
|
||||
inputData = null;
|
||||
}}>{attribute}</Pill>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if !empty(available.attributes)}
|
||||
<div>
|
||||
<p class="u-text">Choose an attribute (optional)</p>
|
||||
<div class="u-flex u-gap-8 u-margin-block-start-8">
|
||||
{#each available.attributes as attribute}
|
||||
<Pill
|
||||
disabled={showInput}
|
||||
selected={selected.attribute === attribute}
|
||||
button
|
||||
on:click={() => select('attribute', attribute)}>
|
||||
{attribute}
|
||||
</Pill>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showInput}
|
||||
<div class="input-text-wrapper" style="--amount-of-buttons:2">
|
||||
<input type="text" placeholder="Enter custom event" bind:value={inputData} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter custom event"
|
||||
bind:value={customInput}
|
||||
use:selectionStart={{ onChange: (newValue) => (customInputCursor = newValue) }} />
|
||||
<div class="options-list">
|
||||
<button
|
||||
on:click|preventDefault={() => {
|
||||
showInput = false;
|
||||
}}
|
||||
class="options-list-button"
|
||||
aria-label="confirm">
|
||||
<button on:click={toggleShowInput} class="options-list-button" aria-label="confirm">
|
||||
<span class="icon-check" aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
on:click|preventDefault={() => {
|
||||
inputData = null;
|
||||
showInput = false;
|
||||
}}
|
||||
class="options-list-button"
|
||||
aria-label="cancel">
|
||||
<button on:click={toggleShowInput} class="options-list-button" aria-label="cancel">
|
||||
<span class="icon-x" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<p style="height: 2rem;">{helper ?? ''}</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="input-text-wrapper" style="--amount-of-buttons:2">
|
||||
<div type="text" readonly style="min-height: 2.5rem;">
|
||||
{#if inputData}
|
||||
<span>{inputData}</span>
|
||||
{:else}
|
||||
{#each Array.from(eventString.values()) as route, i}
|
||||
<button
|
||||
class:u-opacity-0-5={helper !== route.description}
|
||||
on:mouseenter={() => {
|
||||
helper = route.description;
|
||||
}}
|
||||
on:mouseleave={() => {
|
||||
helper = null;
|
||||
}}>
|
||||
{route.value}
|
||||
</button>
|
||||
<span class="u-opacity-0-5">
|
||||
{i + 1 < eventString?.size ? '.' : ''}
|
||||
</span>
|
||||
{/each}
|
||||
{/if}
|
||||
<div class="input-text-wrapper" style="--amount-of-buttons:2" bind:this={copyParent}>
|
||||
<!--
|
||||
This object syntax avoids TS erroring because 'type' isn't a valid HTMLDivElement attribute
|
||||
(we need to set it to 'text' to add styling)
|
||||
-->
|
||||
<div {...{ type: 'text' }} style="min-height: 2.5rem;">
|
||||
{#each eventString as route, i}
|
||||
<span
|
||||
class:u-opacity-0-5={helper !== route.description}
|
||||
on:mouseenter={() => (helper = route.description)}
|
||||
on:mouseleave={() => (helper = null)}>
|
||||
{route.value}
|
||||
</span>
|
||||
<span class="u-opacity-0-5">
|
||||
{i + 1 < eventString?.length ? '.' : ''}
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="options-list">
|
||||
<button
|
||||
on:click|preventDefault={() => {
|
||||
inputData = copyValue;
|
||||
showInput = true;
|
||||
}}
|
||||
class="options-list-button"
|
||||
aria-label="edit event">
|
||||
<span class="icon-pencil" aria-hidden="true" />
|
||||
</button>
|
||||
<button disabled={!copyValue} class="options-list-button" aria-label="copy text">
|
||||
<Copy value={copyValue}>
|
||||
<span class="icon-duplicate" aria-hidden="true" />
|
||||
</Copy>
|
||||
</button>
|
||||
{#key copyParent}
|
||||
<button
|
||||
on:click={(e) => {
|
||||
customInput = inputValue;
|
||||
toggleShowInput(e);
|
||||
}}
|
||||
use:tooltip={{ content: 'Edit event', appendTo: copyParent }}
|
||||
class="options-list-button"
|
||||
aria-label="edit event">
|
||||
<span class="icon-pencil" aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
disabled={!inputValue}
|
||||
class="options-list-button"
|
||||
aria-label="copy text">
|
||||
<Copy value={inputValue} appendTo={copyParent}>
|
||||
<span class="icon-duplicate" aria-hidden="true" />
|
||||
</Copy>
|
||||
</button>
|
||||
{/key}
|
||||
</div>
|
||||
<p style="height: 2rem;">{helper ?? ''}</p>
|
||||
</div>
|
||||
@@ -322,6 +378,7 @@
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (show = false)}>Cancel</Button>
|
||||
<Button disabled={showInput || !copyValue} submit>Create</Button>
|
||||
<Button disabled={showInput || !inputValue} submit
|
||||
>{initialValue ? 'Update' : 'Create'}</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
@@ -18,13 +18,18 @@
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await feedback.submitFeedback('feedback-general', message, name, email);
|
||||
show = false;
|
||||
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Feedback submitted successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
show = false;
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
} finally {
|
||||
show = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -34,7 +39,8 @@
|
||||
<h4 class="body-text-1">How can we improve?</h4>
|
||||
<button
|
||||
type="button"
|
||||
class="x-button u-margin-inline-start-auto"
|
||||
class="button is-text is-only-icon u-margin-inline-start-auto"
|
||||
style="--button-size:1.5rem;"
|
||||
aria-label="Close Modal"
|
||||
title="Close Modal"
|
||||
on:click={() => (show = false)}>
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await feedback.submitFeedback('feedback-nps', message, name, email, value);
|
||||
console.log(value, message);
|
||||
feedback.switchType('general');
|
||||
} catch (error) {
|
||||
feedback.switchType('general');
|
||||
@@ -37,7 +36,8 @@
|
||||
<h4 class="body-text-1">How are we doing?</h4>
|
||||
<button
|
||||
type="button"
|
||||
class="x-button u-margin-inline-start-auto"
|
||||
class="button is-text is-only-icon u-margin-inline-start-auto"
|
||||
style="--button-size:1.5rem;"
|
||||
aria-label="Close Modal"
|
||||
title="Close Modal"
|
||||
on:click={() => (show = false)}>
|
||||
|
||||
@@ -20,9 +20,8 @@ export { default as DropListItem } from './dropListItem.svelte';
|
||||
export { default as DropListLink } from './dropListLink.svelte';
|
||||
export { default as Collapsible } from './collapsible.svelte';
|
||||
export { default as CollapsibleItem } from './collapsibleItem.svelte';
|
||||
export { default as DropTabs } from './dropTabs.svelte';
|
||||
export { default as DropTabsItem } from './dropTabsItem.svelte';
|
||||
export { default as DropTabsLink } from './dropTabsLink.svelte';
|
||||
export { default as SecondaryTabs } from './secondaryTabs.svelte';
|
||||
export { default as SecondaryTabsItem } from './secondaryTabsItem.svelte';
|
||||
export { default as Avatar } from './avatar.svelte';
|
||||
export { default as AvatarInitials } from './avatarInitials.svelte';
|
||||
export { default as AvatarGroup } from './avatarGroup.svelte';
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
</h4>
|
||||
<button
|
||||
type="button"
|
||||
class="x-button"
|
||||
class="button is-text is-only-icon"
|
||||
style="--button-size:1.5rem;"
|
||||
aria-label="Close"
|
||||
title="Close"
|
||||
on:click={() => (show = false)}>
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
<form class="modal-form" role="form" on:submit|preventDefault>
|
||||
<header class="modal-header">
|
||||
{#if warning}
|
||||
<div class="avatar is-color-orange is-medium">
|
||||
<div class="avatar is-warning is-medium">
|
||||
<span class="icon-exclamation" aria-hidden="true" />
|
||||
</div>
|
||||
{/if}
|
||||
@@ -91,7 +91,8 @@
|
||||
{#if closable}
|
||||
<button
|
||||
type="button"
|
||||
class="x-button"
|
||||
class="button is-text is-only-icon"
|
||||
style="--button-size:1.5rem;"
|
||||
aria-label="Close Modal"
|
||||
title="Close Modal"
|
||||
on:click={() =>
|
||||
|
||||
@@ -142,9 +142,9 @@
|
||||
</script>
|
||||
|
||||
{#if [...$groups]?.length}
|
||||
<Table noMargin noStyles noMobile>
|
||||
<Table noMargin noStyles>
|
||||
<TableHeader>
|
||||
<TableCellHead>Role</TableCellHead>
|
||||
<TableCellHead width={70}>Role</TableCellHead>
|
||||
{#if withCreate}
|
||||
<TableCellHead width={70}>Create</TableCellHead>
|
||||
{/if}
|
||||
@@ -154,7 +154,7 @@
|
||||
<TableCellHead width={40} />
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each [...$groups].sort(sortRoles) as [role, permission]}
|
||||
{#each [...$groups].sort(sortRoles) as [role, permission] (role)}
|
||||
<TableRow>
|
||||
<TableCell title="Role">
|
||||
<Row {role} />
|
||||
|
||||
@@ -92,9 +92,9 @@
|
||||
</script>
|
||||
|
||||
{#if [...$groups.keys()]?.length}
|
||||
<Table noMargin noStyles noMobile>
|
||||
<Table noMargin noStyles>
|
||||
<TableHeader>
|
||||
<TableCellHead>Role</TableCellHead>
|
||||
<TableCellHead width={70}>Role</TableCellHead>
|
||||
<TableCellHead width={40} />
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
|
||||
@@ -53,59 +53,61 @@
|
||||
})
|
||||
.finally(() => {
|
||||
tick().then(() => {
|
||||
instance.setContent(content.innerHTML);
|
||||
instance.setContent(content);
|
||||
});
|
||||
});
|
||||
}
|
||||
}}>
|
||||
{role}
|
||||
</div>
|
||||
<div class="u-hide" bind:this={content}>
|
||||
{#if data}
|
||||
{@const isUser = role.startsWith('user')}
|
||||
{@const isTeam = role.startsWith('team')}
|
||||
{@const isAnonymous = !data.email && !data.phone}
|
||||
<div class="user-profile">
|
||||
{#if isAnonymous}
|
||||
<div class="avatar is-size-small ">
|
||||
<span class="icon-anonymous" aria-hidden="true" />
|
||||
</div>
|
||||
{:else if data.name}
|
||||
<AvatarInitials name={data.name} size={40} />
|
||||
{:else}
|
||||
<div class="avatar is-size-small ">
|
||||
<span class="icon-minus-sm" aria-hidden="true" />
|
||||
</div>
|
||||
{/if}
|
||||
<span class="user-profile-info is-only-desktop">
|
||||
<span class="name">
|
||||
{data.name
|
||||
? data.name
|
||||
: data?.email
|
||||
? data?.email
|
||||
: data?.phone
|
||||
? data?.phone
|
||||
: '-'}
|
||||
</span>
|
||||
<Output value={data.$id}>{role}</Output>
|
||||
</span>
|
||||
{#if (isUser && (data?.email || data?.phone)) || isTeam}
|
||||
<span class="user-profile-sep" />
|
||||
|
||||
<span class="user-profile-empty-column" />
|
||||
<div class="u-hide">
|
||||
<div bind:this={content}>
|
||||
{#if data}
|
||||
{@const isUser = role.startsWith('user')}
|
||||
{@const isTeam = role.startsWith('team')}
|
||||
{@const isAnonymous = !data.email && !data.phone && isUser}
|
||||
<div class="user-profile">
|
||||
{#if isAnonymous}
|
||||
<div class="avatar is-size-small ">
|
||||
<span class="icon-anonymous" aria-hidden="true" />
|
||||
</div>
|
||||
{:else if data.name}
|
||||
<AvatarInitials name={data.name} size={40} />
|
||||
{:else}
|
||||
<div class="avatar is-size-small ">
|
||||
<span class="icon-minus-sm" aria-hidden="true" />
|
||||
</div>
|
||||
{/if}
|
||||
<span class="user-profile-info is-only-desktop">
|
||||
{#if isUser}
|
||||
<p class="text u-x-small">{data?.email}</p>
|
||||
<p class="text u-x-small">{data?.phone}</p>
|
||||
{:else if isTeam}
|
||||
<p class="text u-x-small">Members: {data?.total}</p>
|
||||
{/if}
|
||||
<span class="name">
|
||||
{data.name ?? data?.email ?? data?.phone ?? '-'}
|
||||
</span>
|
||||
<Output value={data.$id}>{role}</Output>
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
Not found.
|
||||
{/if}
|
||||
{#if (isUser && (data?.email || data?.phone)) || isTeam}
|
||||
<span class="user-profile-sep" />
|
||||
|
||||
<span class="user-profile-empty-column" />
|
||||
<span class="user-profile-info is-only-desktop">
|
||||
{#if isUser}
|
||||
<div class="u-grid u-gap-4">
|
||||
{#if data?.email}
|
||||
<p class="text u-x-small">Email: {data?.email}</p>
|
||||
{/if}
|
||||
{#if data?.phone}
|
||||
<p class="text u-x-small">Phone: {data?.phone}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if isTeam}
|
||||
<p class="text u-x-small">Members: {data?.total}</p>
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
Not found.
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { onMount } from 'svelte';
|
||||
import { onDestroy } from 'svelte';
|
||||
|
||||
@@ -30,7 +31,7 @@
|
||||
|
||||
$: valueChange(search);
|
||||
|
||||
const valueChange = (value: string) => {
|
||||
function valueChange(value: string) {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(async () => {
|
||||
const url = new URL($page.url);
|
||||
@@ -45,10 +46,10 @@
|
||||
}
|
||||
|
||||
url.search = value;
|
||||
|
||||
goto(url, { keepfocus: true });
|
||||
trackEvent('search');
|
||||
goto(url, { keepFocus: true });
|
||||
}, debounce);
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="u-flex u-gap-12 common-section u-main-space-between">
|
||||
@@ -63,7 +64,11 @@
|
||||
bind:value={search} />
|
||||
<span class="icon-search" aria-hidden="true" />
|
||||
{#if isWithEndButton && search}
|
||||
<button class="x-button" aria-label="Clear search" on:click={() => (search = '')}>
|
||||
<button
|
||||
class="button is-text is-only-icon"
|
||||
style="--button-size:1.5rem;"
|
||||
aria-label="Clear search"
|
||||
on:click={() => (search = '')}>
|
||||
<span class="icon-x" aria-hidden="true" />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<ul class="secondary-tabs">
|
||||
<slot />
|
||||
</ul>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
export let href: string;
|
||||
export let disabled = false;
|
||||
</script>
|
||||
|
||||
<li class="secondary-tabs-item">
|
||||
{#if href}
|
||||
{#if disabled}
|
||||
<button class="secondary-tabs-button" disabled>
|
||||
<span class="text"><slot /></span>
|
||||
</button>
|
||||
{:else}
|
||||
<a class="secondary-tabs-button" {href}>
|
||||
<span class="text"><slot /></span>
|
||||
</a>
|
||||
{/if}
|
||||
{:else}
|
||||
<button class="secondary-tabs-button" {disabled} on:click>
|
||||
<span class="text"><slot /></span>
|
||||
</button>
|
||||
{/if}
|
||||
</li>
|
||||
@@ -1,11 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
import { Copy } from '.';
|
||||
|
||||
export let show = false;
|
||||
export let value: string;
|
||||
export let copyEvent: string = null;
|
||||
</script>
|
||||
|
||||
<div class="interactive-text-output" class:is-textarea={show}>
|
||||
<div class="interactive-text-output" class:is-textarea={show} data-private>
|
||||
{#if show}
|
||||
<span class="text u-line-height-1-5 u-break-all">{value}</span>
|
||||
{:else}
|
||||
@@ -16,10 +19,14 @@
|
||||
class="interactive-text-output-button"
|
||||
aria-label="show hidden text"
|
||||
type="button"
|
||||
on:click={() => (show = !show)}>
|
||||
on:click={() => {
|
||||
show = !show;
|
||||
trackEvent(`click_secret_${show ? 'show' : 'hide'}`);
|
||||
}}
|
||||
use:tooltip={{ content: show ? 'Hide secret' : 'Show secret', hideOnClick: false }}>
|
||||
<span class:icon-eye-off={show} class:icon-eye={!show} aria-hidden="true" />
|
||||
</button>
|
||||
<Copy {value}>
|
||||
<Copy {value} event={copyEvent} eventContext="click_secret_copy">
|
||||
<button class="interactive-text-output-button" aria-label="copy text" type="button">
|
||||
<span class="icon-duplicate" aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<ul class="grid-box common-section" style="--grid-item-size:25rem;">
|
||||
<ul class="grid-box common-section" style="--grid-item-size:25rem;" data-private>
|
||||
<slot />
|
||||
</ul>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<span class="amount">{$uploader.files.length}</span>
|
||||
</h4>
|
||||
<button
|
||||
class="icon-button"
|
||||
class="upload-box-button"
|
||||
class:is-open={!$uploader.isCollapsed}
|
||||
aria-label="toggle upload box"
|
||||
on:click={() => uploader.toggle()}>
|
||||
@@ -28,7 +28,7 @@
|
||||
</button>
|
||||
<button
|
||||
on:click={() => uploader.reset()}
|
||||
class="icon-button"
|
||||
class="upload-box-button"
|
||||
aria-label="close upload box">
|
||||
<span class="icon-x" aria-hidden="true" />
|
||||
</button>
|
||||
@@ -52,7 +52,7 @@
|
||||
<label for={file.name} class="file-name u-trim">{file.name}</label>
|
||||
|
||||
<button
|
||||
class="icon-button"
|
||||
class="upload-box-button"
|
||||
aria-label="Failed"
|
||||
on:mouseenter={() => (hovering = true)}
|
||||
on:mouseleave={() => (hovering = false)}
|
||||
@@ -74,7 +74,7 @@
|
||||
<label for={file.name} class="file-name u-trim">{file.name}</label>
|
||||
<Pill danger>Failed</Pill>
|
||||
<button
|
||||
class="icon-button"
|
||||
class="upload-box-button"
|
||||
aria-label="Failed"
|
||||
on:click|preventDefault={() => uploader.removeFromQueue(file.$id)}>
|
||||
<span class="icon-x" />
|
||||
|
||||
+84
-5
@@ -2,11 +2,6 @@ export const PAGE_LIMIT = 12; // default page limit
|
||||
export const CARD_LIMIT = 6; // default card limit
|
||||
export const INTERVAL = 5 * 60000; // default interval to check for feedback
|
||||
|
||||
export enum Mode {
|
||||
CLOUD = 'mode:cloud',
|
||||
SELF_HOSTED = 'mode:self-hosted'
|
||||
}
|
||||
|
||||
export enum Dependencies {
|
||||
ORGANIZATION = 'dependency:organization',
|
||||
PROJECT = 'dependency:project',
|
||||
@@ -174,3 +169,87 @@ export const scopes: {
|
||||
category: 'Other'
|
||||
}
|
||||
];
|
||||
|
||||
export type EventService = {
|
||||
name: string;
|
||||
resources: EventResource[];
|
||||
actions?: EventAction[];
|
||||
};
|
||||
|
||||
export type EventResource = {
|
||||
name: string;
|
||||
actions?: EventAction[];
|
||||
};
|
||||
|
||||
export type EventAction = {
|
||||
name: string;
|
||||
attributes?: string[];
|
||||
};
|
||||
|
||||
export const eventServices: Array<EventService> = [
|
||||
{
|
||||
name: 'buckets',
|
||||
resources: [
|
||||
{
|
||||
name: 'files',
|
||||
actions: [{ name: 'create' }, { name: 'update' }, { name: 'delete' }]
|
||||
}
|
||||
],
|
||||
actions: [{ name: 'create' }, { name: 'update' }, { name: 'delete' }]
|
||||
},
|
||||
{
|
||||
name: 'databases',
|
||||
resources: [
|
||||
{
|
||||
name: 'collections',
|
||||
actions: [{ name: 'create' }, { name: 'update' }, { name: 'delete' }]
|
||||
},
|
||||
{
|
||||
name: 'documents',
|
||||
actions: [{ name: 'create' }, { name: 'update' }, { name: 'delete' }]
|
||||
}
|
||||
],
|
||||
actions: [{ name: 'create' }, { name: 'update' }, { name: 'delete' }]
|
||||
},
|
||||
{
|
||||
name: 'functions',
|
||||
resources: [
|
||||
{
|
||||
name: 'deployments',
|
||||
actions: [{ name: 'create' }, { name: 'update' }, { name: 'delete' }]
|
||||
},
|
||||
{
|
||||
name: 'executions',
|
||||
actions: [{ name: 'create' }, { name: 'update' }, { name: 'delete' }]
|
||||
}
|
||||
],
|
||||
actions: [{ name: 'create' }, { name: 'update' }, { name: 'delete' }]
|
||||
},
|
||||
{
|
||||
name: 'teams',
|
||||
resources: [
|
||||
{
|
||||
name: 'memberships',
|
||||
actions: [
|
||||
{ name: 'create' },
|
||||
{ name: 'update', attributes: ['status'] },
|
||||
{ name: 'delete' }
|
||||
]
|
||||
}
|
||||
],
|
||||
actions: [{ name: 'create' }, { name: 'update' }, { name: 'delete' }]
|
||||
},
|
||||
{
|
||||
name: 'users',
|
||||
resources: [
|
||||
{ name: 'recovery', actions: [{ name: 'create' }, { name: 'delete' }] },
|
||||
{ name: 'sessions', actions: [{ name: 'create' }, { name: 'delete' }] },
|
||||
{ name: 'verification', actions: [{ name: 'create' }, { name: 'delete' }] }
|
||||
],
|
||||
actions: [
|
||||
{ name: 'create' },
|
||||
{ name: 'update', attributes: ['email', 'name', 'password', 'status', 'prefs'] },
|
||||
{ name: 'delete' }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
export let noMargin = false;
|
||||
export let event: string = null;
|
||||
|
||||
//allows to add the disabled attribute to <a> tag without throwing an error
|
||||
let attributes = { disabled } as Record<string, boolean>;
|
||||
|
||||
function track() {
|
||||
if (!event) {
|
||||
return;
|
||||
@@ -28,7 +31,7 @@
|
||||
{#if href}
|
||||
<a
|
||||
on:click={track}
|
||||
{disabled}
|
||||
{...attributes}
|
||||
{href}
|
||||
target={external ? '_blank' : ''}
|
||||
rel={external ? 'noopener noreferrer' : ''}
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
</script>
|
||||
|
||||
<p
|
||||
class="helper u-margin-block-start-12"
|
||||
class:u-info={type === 'info'}
|
||||
class:u-error={type === 'error'}
|
||||
class:u-success={type === 'success'}
|
||||
class:u-warning={type === 'warning'}>
|
||||
class="helper u-margin-block-start-8"
|
||||
class:u-color-text-info={type === 'info'}
|
||||
class:u-color-text-danger={type === 'error'}
|
||||
class:u-color-text-success={type === 'success'}
|
||||
class:u-color-text-warning={type === 'warning'}>
|
||||
{#if type}
|
||||
<span
|
||||
class:icon-info={type === 'info' || type === 'neutral'}
|
||||
|
||||
@@ -20,4 +20,7 @@ export { default as InputChoice } from './inputChoice.svelte';
|
||||
export { default as InputPhone } from './inputPhone.svelte';
|
||||
export { default as InputCron } from './inputCron.svelte';
|
||||
export { default as InputURL } from './inputURL.svelte';
|
||||
export { default as InputId } from './inputId.svelte';
|
||||
export { default as InputSecret } from './inputSecret.svelte';
|
||||
export { default as Helper } from './helper.svelte';
|
||||
export { default as Label } from './label.svelte';
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { FormItem, Helper } from '.';
|
||||
import { FormItem, Helper, Label } from '.';
|
||||
|
||||
export let label: string;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
export let showLabel = true;
|
||||
export let id: string;
|
||||
export let value = false;
|
||||
@@ -26,7 +27,10 @@
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
<label class:u-hide={!showLabel} class="label" for={id}>{label}</label>
|
||||
<Label {required} {optionalText} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
</Label>
|
||||
|
||||
<div class="input-text-wrapper">
|
||||
<input
|
||||
{id}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { FormItem, Helper } from '.';
|
||||
import { FormItem, Helper, Label } from '.';
|
||||
|
||||
export let label: string;
|
||||
export let showLabel = true;
|
||||
@@ -18,7 +18,7 @@
|
||||
let element: HTMLInputElement;
|
||||
let error: string;
|
||||
|
||||
const pattern = String.raw`^[^]+\s+[^]+\s+[^]+\s+[^]+\s+[^]`;
|
||||
const pattern = String.raw`^[^]+\s+[^]+\s+[^]+\s+[^]+\s+[^]+`;
|
||||
|
||||
onMount(() => {
|
||||
if (element && autofocus) {
|
||||
@@ -35,7 +35,7 @@
|
||||
}
|
||||
|
||||
if (element.validity.patternMismatch) {
|
||||
error = 'Please enter a valid CRON expression';
|
||||
error = 'Please enter a valid Cron expression';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -48,7 +48,10 @@
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
<label class:u-hide={!showLabel} class="label" for={id}>{label}</label>
|
||||
<Label {required} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
</Label>
|
||||
|
||||
<div class="input-text-wrapper">
|
||||
<input
|
||||
{id}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { FormItem, Helper } from '.';
|
||||
import { FormItem, Helper, Label } from '.';
|
||||
|
||||
export let label: string;
|
||||
export let showLabel = true;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
export let id: string;
|
||||
export let value = '';
|
||||
export let required = false;
|
||||
@@ -38,8 +39,11 @@
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
<label class:u-hide={!showLabel} class="label" for={id}>{label}</label>
|
||||
<div class="input-text-wrapper">
|
||||
<Label {required} {optionalText} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
</Label>
|
||||
|
||||
<div class="input-text-wrapper" style="--amount-of-buttons:1; --button-size: 1rem">
|
||||
<input
|
||||
{id}
|
||||
{disabled}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { FormItem, Helper } from '.';
|
||||
import { FormItem, Helper, Label } from '.';
|
||||
|
||||
export let label: string;
|
||||
export let showLabel = true;
|
||||
@@ -47,7 +47,10 @@
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
<label class:u-hide={!showLabel} class="label" for={id}>{label}</label>
|
||||
<Label {required} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
</Label>
|
||||
|
||||
<div class="input-text-wrapper">
|
||||
<input
|
||||
{id}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { FormItem, Helper } from '.';
|
||||
import { FormItem, Helper, Label } from '.';
|
||||
|
||||
export let label: string;
|
||||
export let showLabel = true;
|
||||
@@ -41,7 +41,10 @@
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
<label class:u-hide={!showLabel} class="label" for={id}>{label}</label>
|
||||
<Label {required} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
</Label>
|
||||
|
||||
<div class="input-text-wrapper">
|
||||
<input
|
||||
{id}
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { Trim } from '$lib/components';
|
||||
import { Button } from '.';
|
||||
import { humanFileSize } from '$lib/helpers/sizeConvertion';
|
||||
import { Helper } from '.';
|
||||
|
||||
export let label: string = null;
|
||||
export let files: FileList;
|
||||
export let list = new DataTransfer();
|
||||
export let allowedFileExtensions: string[] = [];
|
||||
export let maxSize: number = null;
|
||||
export let required = false;
|
||||
export let error: string = null;
|
||||
|
||||
let input: HTMLInputElement;
|
||||
let hovering = false;
|
||||
|
||||
function dropHandler(ev: DragEvent) {
|
||||
ev.dataTransfer.dropEffect = 'move';
|
||||
hovering = false;
|
||||
if (ev.dataTransfer.items) {
|
||||
// Use DataTransferItemList interface to access the file(s)
|
||||
for (let i = 0; i < ev.dataTransfer.items.length; i++) {
|
||||
// If dropped items aren't files, reject them
|
||||
if (ev.dataTransfer.items[i].kind === 'file') {
|
||||
list.items.clear();
|
||||
list.items.add(ev.dataTransfer.items[i].getAsFile());
|
||||
@@ -28,6 +31,22 @@
|
||||
function dragOverHandler() {
|
||||
hovering = true;
|
||||
}
|
||||
|
||||
const handleInvalid = (event: Event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (input.validity.valueMissing) {
|
||||
error = 'This field is required';
|
||||
return;
|
||||
}
|
||||
error = input.validationMessage;
|
||||
};
|
||||
|
||||
$: if (files) {
|
||||
error = null;
|
||||
}
|
||||
|
||||
$: fileArray = files?.length ? Array.from(files) : [];
|
||||
</script>
|
||||
|
||||
<input
|
||||
@@ -35,48 +54,89 @@
|
||||
bind:this={input}
|
||||
accept={allowedFileExtensions.map((n) => `.${n}`).join(',')}
|
||||
type="file"
|
||||
style="display: none" />
|
||||
style="display: none"
|
||||
{required}
|
||||
on:invalid={handleInvalid} />
|
||||
|
||||
<div>
|
||||
{#if label}
|
||||
<p class="text">{label}</p>
|
||||
{/if}
|
||||
<div
|
||||
class="card is-border-dashed is-no-shadow"
|
||||
class="box is-no-shadow u-padding-24"
|
||||
style="--box-border-radius:var(--border-radius-xsmall); z-index: 1"
|
||||
class:is-border-dashed={!hovering}
|
||||
class:is-hover-with-file={hovering}
|
||||
on:drop|preventDefault={dropHandler}
|
||||
on:dragover|preventDefault={dragOverHandler}
|
||||
on:dragenter|preventDefault
|
||||
on:dragleave|preventDefault={() => (hovering = false)}>
|
||||
<div class="u-flex u-main-center u-cross-center u-gap-32">
|
||||
<div class="avatar is-size-large u-min-width-0">
|
||||
<span class="icon-folder" aria-hidden="true" />
|
||||
<div class="upload-file-box">
|
||||
<div class="upload-file-box-image">
|
||||
<span class="icon-upload" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="u-min-width-0 u-text-center">
|
||||
<h5 class="upload-file-box-title heading-level-7 u-inline">
|
||||
<span class="is-only-desktop">Drag and drop a file here to upload</span>
|
||||
<span class="is-not-desktop">Upload a File</span>
|
||||
</h5>
|
||||
{#if allowedFileExtensions?.length}
|
||||
<button
|
||||
class="tooltip u-inline u-margin-inline-start-4"
|
||||
aria-label="variables info">
|
||||
<span class="icon-info" aria-hidden="true" />
|
||||
<span class="tooltip-popup" role="tooltip"
|
||||
>Only {allowedFileExtensions.join(', ')} accepted.</span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="u-flex u-main-center u-cross-center u-gap-16 u-flex-vertical-mobile">
|
||||
{#if maxSize}
|
||||
{@const readableMaxSize = humanFileSize(maxSize)}
|
||||
<p class="upload-file-box-info body-text-2 u-normal">
|
||||
Max file size: {readableMaxSize.value + readableMaxSize.unit}
|
||||
</p>
|
||||
{/if}
|
||||
<button
|
||||
class="button is-secondary is-full-width-mobile"
|
||||
type="button"
|
||||
on:click={() => input.click()}>
|
||||
<span class="text">Choose a file</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="u-grid u-gap-16 u-min-width-0">
|
||||
<p>Drag and drop files here to upload</p>
|
||||
<div class="u-flex u-gap-8 u-min-width-0">
|
||||
<Button secondary on:click={() => input.click()}>
|
||||
<span class="icon-upload" aria-hidden="true" />
|
||||
<span class="text">Choose File</span>
|
||||
</Button>
|
||||
|
||||
{#if files?.length}
|
||||
{@const fileName = files.item(0).name.split('.')}
|
||||
<div class="u-flex u-cross-center u-min-width-0">
|
||||
<Trim>{fileName[0]}</Trim>
|
||||
<span class="u-min-width-0 u-flex-shrink-0 u-margin-inline-end-16"
|
||||
>.{fileName[1]}</span>
|
||||
{#if files?.length}
|
||||
<ul class="upload-file-box-list u-min-width-0">
|
||||
{#each fileArray as file}
|
||||
{@const fileName = file.name.split('.')}
|
||||
{@const fileSize = humanFileSize(file.size)}
|
||||
<li class="u-flex u-cross-center u-min-width-0">
|
||||
<span class="icon-document" aria-hidden="true" />
|
||||
<span class="upload-file-box-name u-trim u-min-width-0">
|
||||
<Trim>{fileName[0]}</Trim>
|
||||
</span>
|
||||
<span class="upload-file-box-name u-min-width-0 u-flex-shrink-0">
|
||||
.{fileName[1]}
|
||||
</span>
|
||||
<span
|
||||
class="upload-file-box-size u-margin-inline-start-4 u-margin-inline-end-16">
|
||||
{fileSize.value + fileSize.unit}
|
||||
</span>
|
||||
<button
|
||||
on:click={() => (files = null)}
|
||||
type="button"
|
||||
class="x-button"
|
||||
class="button is-text is-only-icon u-margin-inline-start-auto"
|
||||
aria-label="remove file"
|
||||
title="Remove file">
|
||||
style="--button-size:1.5rem;">
|
||||
<span class="icon-x" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if error}
|
||||
<Helper type="warning">{error}</Helper>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { FormItem } from '.';
|
||||
|
||||
export let value = '';
|
||||
|
||||
let element: HTMLInputElement;
|
||||
let icon = 'info';
|
||||
const pattern = String.raw`^[a-zA-Z0-9][a-zA-Z0-9._-]*$`;
|
||||
const autofocus = true;
|
||||
|
||||
onMount(() => {
|
||||
if (element && autofocus) {
|
||||
element.focus();
|
||||
}
|
||||
});
|
||||
|
||||
const handleInvalid = (event: Event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (element.validity.patternMismatch) {
|
||||
icon = 'exclamation';
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
$: if (value) {
|
||||
icon = 'info';
|
||||
}
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
<div class="input-text-wrapper">
|
||||
<input
|
||||
id="id"
|
||||
placeholder="Enter ID"
|
||||
maxlength={36}
|
||||
{pattern}
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
class="input-text"
|
||||
bind:value
|
||||
bind:this={element}
|
||||
on:invalid={handleInvalid} />
|
||||
<span class="text-counter">
|
||||
<span class="text-counter-count">{value?.length ?? 0}</span>
|
||||
<span class="text-counter-separator" />
|
||||
<span class="text-counter-max">36</span>
|
||||
</span>
|
||||
</div>
|
||||
</FormItem>
|
||||
<div
|
||||
class="u-flex u-gap-4 u-margin-block-start-8 u-small"
|
||||
class:u-color-text-warning={icon === 'exclamation'}>
|
||||
<span
|
||||
class:icon-info={icon === 'info'}
|
||||
class:icon-exclamation={icon === 'exclamation'}
|
||||
class="u-cross-center u-margin-block-start-2 u-line-height-1 u-icon-small"
|
||||
aria-hidden="true" />
|
||||
<span class="text u-line-height-1-5">
|
||||
Allowed characters: alphanumeric, non-leading hyphen, underscore, period
|
||||
</span>
|
||||
</div>
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { FormItem, Helper } from '.';
|
||||
import { FormItem, Helper, Label } from '.';
|
||||
|
||||
export let label: string;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
export let showLabel = true;
|
||||
export let id: string;
|
||||
export let value: number = null;
|
||||
@@ -51,7 +52,10 @@
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
<label class:u-hide={!showLabel} class="label" for={id}>{label}</label>
|
||||
<Label {required} {optionalText} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
</Label>
|
||||
|
||||
<div class="input-text-wrapper">
|
||||
<input
|
||||
{id}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { FormItem, Helper } from '.';
|
||||
import { FormItem, Helper, Label } from '.';
|
||||
|
||||
export let id: string;
|
||||
export let label: string;
|
||||
@@ -47,7 +47,10 @@
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
<label class:u-hide={!showLabel} class="label" for={id}>{label}</label>
|
||||
<Label {required} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
</Label>
|
||||
|
||||
<div class="input-text-wrapper">
|
||||
{#if showInPlainText}
|
||||
<input
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { FormItem, Helper } from '.';
|
||||
import { FormItem, Helper, Label } from '.';
|
||||
|
||||
export let label: string;
|
||||
export let showLabel = true;
|
||||
@@ -46,7 +46,9 @@
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
<label class:u-hide={!showLabel} class="label" for={id}>{label}</label>
|
||||
<Label {required} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
</Label>
|
||||
<div class="input-text-wrapper">
|
||||
<input
|
||||
{id}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { FormItem, Helper } from '.';
|
||||
import { FormItem, Helper, Label } from '.';
|
||||
|
||||
export let label: string;
|
||||
export let showLabel = true;
|
||||
@@ -40,7 +40,9 @@
|
||||
bind:group
|
||||
bind:this={element}
|
||||
on:invalid={handleInvalid} />
|
||||
<label class:u-hide={!showLabel} class="label" for={id}>{label}</label>
|
||||
<Label {required} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
</Label>
|
||||
</div>
|
||||
{#if error}
|
||||
<Helper type="warning">{error}</Helper>
|
||||
|
||||
@@ -52,7 +52,11 @@
|
||||
on:input={valueChange} />
|
||||
<span class="icon-search" aria-hidden="true" />
|
||||
{#if isWithEndButton && value}
|
||||
<button class="x-button" aria-label="Clear search" on:click={() => (value = '')}>
|
||||
<button
|
||||
class="button is-text is-only-icon"
|
||||
style="--button-size:1.5rem;"
|
||||
aria-label="Clear search"
|
||||
on:click={() => (value = '')}>
|
||||
<span class="icon-x" aria-hidden="true" />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<script lang="ts">
|
||||
import { FormItem, Label } from '.';
|
||||
|
||||
export let id: string;
|
||||
export let label: string;
|
||||
export let showLabel = true;
|
||||
export let value = '';
|
||||
export let placeholder = '';
|
||||
|
||||
let showInPlainText = false;
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
<Label hide={!showLabel} for={id}>
|
||||
{label}
|
||||
</Label>
|
||||
|
||||
<div class="input-text-wrapper" style=" --amount-of-buttons: 1;">
|
||||
{#if showInPlainText}
|
||||
<div
|
||||
contenteditable="true"
|
||||
role="textbox"
|
||||
{id}
|
||||
aria-placeholder={placeholder}
|
||||
class="input-text is-resizable"
|
||||
bind:textContent={value} />
|
||||
{:else}
|
||||
<input
|
||||
{id}
|
||||
{placeholder}
|
||||
name={id}
|
||||
type="password"
|
||||
class="input-text"
|
||||
autocomplete="off"
|
||||
bind:value />
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => (showInPlainText = !showInPlainText)}
|
||||
class="show-password-button"
|
||||
aria-label="show / hide password">
|
||||
<span
|
||||
class:icon-eye={!showInPlainText}
|
||||
class:icon-eye-off={showInPlainText}
|
||||
aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</FormItem>
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { FormItem, Helper } from '.';
|
||||
import { FormItem, Helper, Label } from '.';
|
||||
|
||||
export let id: string;
|
||||
export let label: string;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
export let showLabel = true;
|
||||
export let value: string | number | boolean;
|
||||
export let placeholder = '';
|
||||
@@ -26,13 +27,24 @@
|
||||
error = element.validationMessage;
|
||||
};
|
||||
|
||||
$: if (element && required && !value) {
|
||||
element.setCustomValidity('This field is required');
|
||||
}
|
||||
|
||||
$: if (element && required && value) {
|
||||
element.setCustomValidity('');
|
||||
}
|
||||
|
||||
$: if (value) {
|
||||
error = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
<label class:u-hide={!showLabel} class="label" for={id}>{label}</label>
|
||||
<Label {required} {optionalText} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
</Label>
|
||||
|
||||
<div class="select">
|
||||
<select
|
||||
{id}
|
||||
@@ -54,5 +66,7 @@
|
||||
</div>
|
||||
{#if error}
|
||||
<Helper type="warning">{error}</Helper>
|
||||
{:else}
|
||||
<slot name="helper" />
|
||||
{/if}
|
||||
</FormItem>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { last } from '$lib/helpers/array';
|
||||
import { onMount } from 'svelte';
|
||||
import { FormItem, Helper } from '.';
|
||||
import { FormItem, Helper, Label } from '.';
|
||||
|
||||
export let id: string;
|
||||
export let label: string;
|
||||
@@ -10,9 +11,11 @@
|
||||
export let autofocus = false;
|
||||
export let disabled = false;
|
||||
export let readonly = false;
|
||||
export let required = false;
|
||||
|
||||
let value = '';
|
||||
let element: HTMLInputElement;
|
||||
let hiddenEl: HTMLInputElement;
|
||||
let error: string;
|
||||
|
||||
onMount(() => {
|
||||
@@ -34,7 +37,7 @@
|
||||
}
|
||||
if (['Backspace', 'Delete'].includes(e.key)) {
|
||||
if (value.length === 0) {
|
||||
removeValue(tags[tags.length - 1]);
|
||||
removeValue(last(tags));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -48,17 +51,18 @@
|
||||
};
|
||||
|
||||
const removeValue = (value: string) => {
|
||||
if (readonly) return;
|
||||
tags = tags.filter((tag) => tag !== value);
|
||||
};
|
||||
|
||||
const handleInvalid = (event: Event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (element.validity.valueMissing) {
|
||||
if (hiddenEl.validity.valueMissing) {
|
||||
error = 'This field is required';
|
||||
return;
|
||||
}
|
||||
error = element.validationMessage;
|
||||
error = hiddenEl.validationMessage;
|
||||
};
|
||||
|
||||
$: if (value) {
|
||||
@@ -67,7 +71,16 @@
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
<label class:u-hide={!showLabel} class="label" for={id}>{label}</label>
|
||||
<input
|
||||
class="u-hide"
|
||||
bind:this={hiddenEl}
|
||||
value={tags.join(',')}
|
||||
{required}
|
||||
on:invalid={handleInvalid} />
|
||||
<Label {required} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
</Label>
|
||||
|
||||
<div class="input-text-wrapper">
|
||||
<div class="tags-input">
|
||||
<div class="tags">
|
||||
@@ -98,8 +111,7 @@
|
||||
bind:value
|
||||
bind:this={element}
|
||||
on:keydown={handleInput}
|
||||
on:blur={addValue}
|
||||
on:invalid={handleInvalid} />
|
||||
on:blur={addValue} />
|
||||
</div>
|
||||
</div>
|
||||
{#if error}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { tooltip as tooltipAction } from '$lib/actions/tooltip';
|
||||
import { onMount } from 'svelte';
|
||||
import { FormItem, Helper } from '.';
|
||||
import { FormItem, Helper, Label } from '.';
|
||||
|
||||
export let label: string;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
export let showLabel = true;
|
||||
export let id: string;
|
||||
export let value = '';
|
||||
@@ -42,17 +42,10 @@
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
<label class:u-hide={!showLabel} class="label" for={id}
|
||||
>{label}
|
||||
{#if tooltip}
|
||||
<span
|
||||
class="icon-info"
|
||||
aria-hidden="true"
|
||||
use:tooltipAction={{
|
||||
content: tooltip
|
||||
}} />
|
||||
{/if}
|
||||
</label>
|
||||
<Label {required} {tooltip} {optionalText} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
</Label>
|
||||
|
||||
<div class="input-text-wrapper">
|
||||
<input
|
||||
{id}
|
||||
@@ -67,6 +60,13 @@
|
||||
bind:value
|
||||
bind:this={element}
|
||||
on:invalid={handleInvalid} />
|
||||
{#if maxlength}
|
||||
<span class="text-counter">
|
||||
<span class="text-counter-count">{value?.length ?? 0}</span>
|
||||
<span class="text-counter-separator" />
|
||||
<span class="text-counter-max">{maxlength}</span>
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if error}
|
||||
<Helper type="warning">{error}</Helper>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { FormItem, Helper } from '.';
|
||||
import { FormItem, Helper, Label } from '.';
|
||||
|
||||
export let label: string;
|
||||
export let showLabel = true;
|
||||
@@ -37,7 +37,10 @@
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
<label class:u-hide={!showLabel} class="label" for={id}>{label}</label>
|
||||
<Label {required} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
</Label>
|
||||
|
||||
<div class="input-text-wrapper">
|
||||
<textarea
|
||||
{id}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { FormItem, Helper } from '.';
|
||||
import { FormItem, Helper, Label } from '.';
|
||||
|
||||
export let label: string;
|
||||
export let showLabel = true;
|
||||
@@ -38,13 +38,22 @@
|
||||
error = element.validationMessage;
|
||||
};
|
||||
|
||||
const handleInput = () => {
|
||||
if (value === '') {
|
||||
value = null;
|
||||
}
|
||||
};
|
||||
|
||||
$: if (value) {
|
||||
error = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
<label class:u-hide={!showLabel} class="label" for={id}>{label}</label>
|
||||
<Label {required} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
</Label>
|
||||
|
||||
<div class="input-text-wrapper">
|
||||
<input
|
||||
{id}
|
||||
@@ -56,6 +65,7 @@
|
||||
type="url"
|
||||
autocomplete={autocomplete ? 'on' : 'off'}
|
||||
bind:value
|
||||
on:input={handleInput}
|
||||
bind:this={element}
|
||||
on:invalid={handleInvalid} />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
interface $$Props extends Partial<HTMLLabelElement> {
|
||||
required?: boolean;
|
||||
optionalText?: string | undefined;
|
||||
hide?: boolean;
|
||||
tooltip?: string;
|
||||
for?: string;
|
||||
}
|
||||
|
||||
export let required: $$Props['required'] = false;
|
||||
export let optionalText: $$Props['optionalText'] = undefined;
|
||||
export let hide: $$Props['hide'] = false;
|
||||
export let tooltip: $$Props['tooltip'] = null;
|
||||
</script>
|
||||
|
||||
<label class:is-required={required} class:u-hide={hide} class="label" {...$$restProps}>
|
||||
<slot />
|
||||
</label>
|
||||
|
||||
{#if optionalText}
|
||||
<span class="optional">{optionalText}</span>
|
||||
{/if}
|
||||
|
||||
{#if tooltip}
|
||||
<button class="tooltip" aria-label="variables info">
|
||||
<span class="icon-info" aria-hidden="true" />
|
||||
<span class="tooltip-popup" role="tooltip">
|
||||
<p class="text">
|
||||
{tooltip}
|
||||
</p>
|
||||
</span>
|
||||
</button>
|
||||
{/if}
|
||||
@@ -11,6 +11,9 @@
|
||||
export let href: string = null;
|
||||
export let trim = false;
|
||||
|
||||
//allows to add the disabled attribute to <a> tag without throwing an error
|
||||
let attributes = { disabled } as Record<string, boolean>;
|
||||
|
||||
let element: HTMLButtonElement;
|
||||
|
||||
const isOverflowing = (elem: HTMLButtonElement, iterator = 1) => {
|
||||
@@ -41,7 +44,7 @@
|
||||
|
||||
{#if href}
|
||||
<a
|
||||
{disabled}
|
||||
{...attributes}
|
||||
{href}
|
||||
target={external ? '_blank' : '_self'}
|
||||
rel={external ? 'noopener noreferrer' : ''}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
<script lang="ts">
|
||||
export let width: number = null;
|
||||
export let onlyDesktop = false;
|
||||
export let eyebrow = true;
|
||||
</script>
|
||||
|
||||
<div
|
||||
style={width ? `--p-col-width:${width?.toString()}` : ''}
|
||||
class:is-only-desktop={onlyDesktop}
|
||||
class="table-thead-col"
|
||||
role="columnheader">
|
||||
<span class="eyebrow-heading-3">
|
||||
<span class={eyebrow ? 'eyebrow-heading-3' : 'body-text-2'}>
|
||||
<slot />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
export let title: string;
|
||||
</script>
|
||||
|
||||
<div class="table-col" data-title={title} role="cell">
|
||||
<div class="table-col" data-title={title} role="cell" data-private>
|
||||
<a role="button" tabindex="0" class="link" {href}><slot /></a>
|
||||
</div>
|
||||
|
||||
@@ -2,8 +2,15 @@
|
||||
import { Trim } from '$lib/components';
|
||||
export let title: string;
|
||||
export let showOverflow = false;
|
||||
export let onlyDesktop = false;
|
||||
</script>
|
||||
|
||||
<div class="table-col " class:u-overflow-visible={showOverflow} data-title={title} role="cell">
|
||||
<div
|
||||
class="table-col"
|
||||
class:is-only-desktop={onlyDesktop}
|
||||
class:u-overflow-visible={showOverflow}
|
||||
data-title={title}
|
||||
role="cell"
|
||||
data-private>
|
||||
<Trim><slot /></Trim>
|
||||
</div>
|
||||
|
||||
@@ -2,12 +2,6 @@
|
||||
import { clickOnEnter } from '$lib/helpers/a11y';
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-unknown-role -->
|
||||
<tr
|
||||
class="table-row"
|
||||
role="row button"
|
||||
tabindex="0"
|
||||
on:keyup={clickOnEnter}
|
||||
on:click|preventDefault>
|
||||
<tr class="table-row" role="button" tabindex="0" on:keyup={clickOnEnter} on:click|preventDefault>
|
||||
<slot />
|
||||
</tr>
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
<script lang="ts">
|
||||
export let noMargin = false;
|
||||
export let noStyles = false;
|
||||
export let noMobile = false;
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="table is-vertical"
|
||||
class:is-vertical={!noMobile}
|
||||
class="table is-selected-columns-mobile"
|
||||
class:u-margin-block-start-32={!noMargin}
|
||||
class:is-remove-outer-styles={noStyles}
|
||||
role="table">
|
||||
role="table"
|
||||
data-private>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- TODO: is 100% height needed? -->
|
||||
<div class="table-with-scroll">
|
||||
<div class="table-with-scroll" data-private>
|
||||
<div class="table-wrapper">
|
||||
<div class="table is-remove-outer-styles">
|
||||
<ul class="table-thead">
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<div class="table-with-scroll u-margin-block-start-32">
|
||||
<script lang="ts">
|
||||
export let isSticky = false;
|
||||
</script>
|
||||
|
||||
<div class="table-with-scroll u-margin-block-start-32" data-private>
|
||||
<div class="table-wrapper">
|
||||
<div class="table is-sticky-scroll" role="table">
|
||||
<table class="table" class:is-sticky-scroll={isSticky}>
|
||||
<slot />
|
||||
</div>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -27,3 +27,49 @@ export function remove<T>(arr: T[], index: number): T[] {
|
||||
// Remove the element at the given index, return a new array
|
||||
return [...arr.slice(0, index), ...arr.slice(index + 1)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the array is empty, false otherwise.
|
||||
*
|
||||
* @export
|
||||
* @param {unknown[]} arr
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function empty(arr: unknown[]): boolean {
|
||||
if (!arr) return true;
|
||||
return arr.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get nth item of Array. Negative for backward
|
||||
*
|
||||
* @export
|
||||
* @template T
|
||||
* @param {(readonly T[] | [])} array
|
||||
* @param {number} index
|
||||
* @returns {(T | undefined)}
|
||||
*/
|
||||
export function at(array: readonly [], index: number): undefined;
|
||||
export function at<T>(array: readonly T[], index: number): T;
|
||||
export function at<T>(array: readonly T[] | [], index: number): T | undefined {
|
||||
const len = array.length;
|
||||
if (!len) return undefined;
|
||||
|
||||
if (index < 0) index += len;
|
||||
|
||||
return array[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last item
|
||||
*
|
||||
* @export
|
||||
* @template T
|
||||
* @param {readonly T[]} array
|
||||
* @returns {(T | undefined)}
|
||||
*/
|
||||
export function last(array: readonly []): undefined;
|
||||
export function last<T>(array: readonly T[]): T;
|
||||
export function last<T>(array: readonly T[]): T | undefined {
|
||||
return at(array, -1);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
async function securedCopy(value: string) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function unsecuredCopy(value: string) {
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = value;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
|
||||
let success = true;
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
} catch {
|
||||
success = false;
|
||||
} finally {
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
export async function copy(value: string) {
|
||||
// securedCopy works only in HTTPS environment.
|
||||
// unsecuredCopy works in HTTP and only runs if securedCopy fails.
|
||||
const success = (await securedCopy(value)) || unsecuredCopy(value);
|
||||
|
||||
return success;
|
||||
}
|
||||
@@ -22,3 +22,20 @@ export const toLocaleDateTime = (datetime: string | number) => {
|
||||
|
||||
return date.toLocaleDateString('en', options);
|
||||
};
|
||||
|
||||
export const isSameDay = (date1: Date, date2: Date) => {
|
||||
return (
|
||||
date1.getFullYear() === date2.getFullYear() &&
|
||||
date1.getMonth() === date2.getMonth() &&
|
||||
date1.getDate() === date2.getDate()
|
||||
);
|
||||
};
|
||||
|
||||
export const isValidDate = (date: string) => {
|
||||
return !isNaN(new Date(date).getTime());
|
||||
};
|
||||
|
||||
export const diffDays = (date1: Date, date2: Date) => {
|
||||
const diffTime = Math.abs(date2.getTime() - date1.getTime());
|
||||
return Math.floor(diffTime / (1000 * 60 * 60 * 24));
|
||||
};
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Strict typed `Object.entries`
|
||||
* Extracted from https://github.com/antfu/utils
|
||||
*
|
||||
* @category Object
|
||||
*/
|
||||
export function objectEntries<T extends object>(obj: T) {
|
||||
return Object.entries(obj) as Array<[keyof T, T[keyof T]]>;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] as const;
|
||||
export type Size = (typeof sizes)[number];
|
||||
|
||||
export function calculateSize(bytes: number, decimals = 1) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
@@ -13,18 +14,19 @@ export function calculateSize(bytes: number, decimals = 1) {
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
export function sizeToBytes(value: number, unit: string) {
|
||||
export function sizeToBytes(value: number, unit: Size) {
|
||||
const index = sizes.indexOf(unit);
|
||||
return value * Math.pow(1024, index);
|
||||
}
|
||||
export function bytesToSize(value: number, unit: string) {
|
||||
|
||||
export function bytesToSize(value: number, unit: Size) {
|
||||
const index = sizes.indexOf(unit);
|
||||
return value / Math.pow(1024, index);
|
||||
}
|
||||
|
||||
export function humanFileSize(bytes: number): {
|
||||
value: string;
|
||||
unit: string;
|
||||
unit: Size;
|
||||
} {
|
||||
const value = prettyBytes(bytes, {
|
||||
locale: 'en'
|
||||
@@ -32,6 +34,6 @@ export function humanFileSize(bytes: number): {
|
||||
|
||||
return {
|
||||
value: value[0],
|
||||
unit: value[1]
|
||||
unit: value[1] as Size
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Capitalizes the first letter of a string
|
||||
*
|
||||
* @export
|
||||
* @param {string} str
|
||||
* @returns {string}
|
||||
*/
|
||||
export function capitalize(str: string): string {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a string, returns the singular version of it,
|
||||
* by removing the trailing 's'.
|
||||
*
|
||||
* @export
|
||||
* @param {string} str
|
||||
* @returns {string}
|
||||
*/
|
||||
export function singular(str: string): string {
|
||||
return str.replace(/s$/, '');
|
||||
}
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* Formats the time in a human readable format.
|
||||
*
|
||||
* @export
|
||||
* @param {number} time - The time in seconds.
|
||||
* @returns {string} The formatted time string.
|
||||
*/
|
||||
export function calculateTime(time: number) {
|
||||
const milliseconds = Math.floor(time * 1000);
|
||||
const seconds = Math.floor(time);
|
||||
@@ -8,23 +15,25 @@ export function calculateTime(time: number) {
|
||||
const years = Math.floor(months / 12);
|
||||
|
||||
if (milliseconds < 1000) {
|
||||
return milliseconds + 'ms';
|
||||
return `${milliseconds}ms`;
|
||||
} else if (seconds < 60) {
|
||||
return `${seconds} s`;
|
||||
return `${seconds}s`;
|
||||
} else if (minutes < 60) {
|
||||
return `${minutes} m`;
|
||||
return `${minutes}m`;
|
||||
} else if (hours < 24) {
|
||||
return `${hours} h`;
|
||||
return `${hours}h`;
|
||||
} else if (days < 30) {
|
||||
return `${days} d`;
|
||||
return `${days}d`;
|
||||
} else if (months < 12) {
|
||||
return `${months} M`;
|
||||
return `${months}M`;
|
||||
} else {
|
||||
return `${years} y`;
|
||||
return `${years}y`;
|
||||
}
|
||||
}
|
||||
|
||||
export function timeToSeconds(time: number, unit: string) {
|
||||
type Unit = 'ms' | 's' | 'm' | 'h' | 'd' | 'M' | 'y';
|
||||
|
||||
export function timeToSeconds(time: number, unit: Unit) {
|
||||
switch (unit) {
|
||||
case 'ms':
|
||||
return time / 1000;
|
||||
@@ -44,7 +53,7 @@ export function timeToSeconds(time: number, unit: string) {
|
||||
return time;
|
||||
}
|
||||
}
|
||||
export function timeToMinutes(time: number, unit: string) {
|
||||
export function timeToMinutes(time: number, unit: Unit) {
|
||||
switch (unit) {
|
||||
case 'ms':
|
||||
return time / 1000 / 60;
|
||||
@@ -64,3 +73,24 @@ export function timeToMinutes(time: number, unit: string) {
|
||||
return time;
|
||||
}
|
||||
}
|
||||
|
||||
export function secsToUnit(time: number, unit: Unit) {
|
||||
switch (unit) {
|
||||
case 'ms':
|
||||
return time * 1000;
|
||||
case 's':
|
||||
return time;
|
||||
case 'm':
|
||||
return time / 60;
|
||||
case 'h':
|
||||
return time / 60 / 60;
|
||||
case 'd':
|
||||
return time / 60 / 60 / 24;
|
||||
case 'M':
|
||||
return time / 60 / 60 / 24 / 30;
|
||||
case 'y':
|
||||
return time / 60 / 60 / 24 / 30 / 12;
|
||||
default:
|
||||
return time;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { derived, writable } from 'svelte/store';
|
||||
import { last } from './array';
|
||||
import { withPrevious } from './withPrevious';
|
||||
|
||||
export type Unit<T = string> = {
|
||||
name: T;
|
||||
value: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a value-unit pair. When the unit is changed, the value is updated
|
||||
* to reflect the new unit.
|
||||
*
|
||||
* @export
|
||||
* @template T = string
|
||||
* @param {number} [initialValue=0]
|
||||
* @param {Unit<T>[]} units
|
||||
*/
|
||||
export function createValueUnitPair<T = string>(initialValue = 0, units: Unit<T>[]) {
|
||||
if (!units.some((u) => u.value === 1)) {
|
||||
throw new Error('Units must have a value of 1');
|
||||
}
|
||||
|
||||
if (units.some((u) => u.value < 1)) {
|
||||
throw new Error('Units must have a value greater than 1');
|
||||
}
|
||||
|
||||
// Highest to lowest
|
||||
const sortedUnits = units.sort((a, b) => b.value - a.value);
|
||||
|
||||
const unit = withPrevious(last(sortedUnits).name);
|
||||
const value = writable(initialValue);
|
||||
const baseValue = derived([value, unit], ([$value, $unit]) => {
|
||||
return $value * units.find((u) => u.name === $unit).value;
|
||||
});
|
||||
|
||||
for (const u of sortedUnits) {
|
||||
if (initialValue >= u.value && initialValue % u.value === 0) {
|
||||
unit.set(u.name, { updateAll: true });
|
||||
value.set(initialValue / u.value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
unit.subscribe((newValue, prev) => {
|
||||
value.update((v) => {
|
||||
const unitInBase = v * units.find((u) => u.name === prev).value;
|
||||
return unitInBase / units.find((u) => u.name === newValue).value;
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
value,
|
||||
unit,
|
||||
baseValue
|
||||
};
|
||||
}
|
||||
|
||||
export function createTimeUnitPair(initialValue = 0) {
|
||||
const units: Unit[] = [
|
||||
{ name: 'Days', value: 86400 },
|
||||
{ name: 'Hours', value: 3600 },
|
||||
{ name: 'Minutes', value: 60 },
|
||||
{ name: 'Seconds', value: 1 }
|
||||
];
|
||||
return { ...createValueUnitPair(initialValue, units), units };
|
||||
}
|
||||
|
||||
export function createByteUnitPair(initialValue = 0) {
|
||||
const units: Unit[] = [
|
||||
{ name: 'Bytes', value: 1 },
|
||||
{ name: 'Kilobytes', value: 1024 },
|
||||
{ name: 'Megabytes', value: 1024 ** 2 },
|
||||
{ name: 'Gigabytes', value: 1024 ** 3 }
|
||||
];
|
||||
return { ...createValueUnitPair(initialValue, units), units };
|
||||
}
|
||||
@@ -18,7 +18,7 @@ const vitalsUrl = 'https://vitals.vercel-analytics.com/v1/vitals';
|
||||
function getConnectionSpeed() {
|
||||
return 'connection' in navigator &&
|
||||
navigator['connection'] &&
|
||||
'effectiveType' in navigator['connection']
|
||||
'effectiveType' in (navigator['connection'] as Record<string, unknown>)
|
||||
? navigator['connection']['effectiveType']
|
||||
: '';
|
||||
}
|
||||
@@ -40,7 +40,7 @@ function sendToAnalytics(metric: Metric, options: Options) {
|
||||
};
|
||||
|
||||
if (options.debug) {
|
||||
console.log('[Analytics]', metric.name, JSON.stringify(body, null, 2));
|
||||
console.debug('[Analytics]', metric.name, JSON.stringify(body, null, 2));
|
||||
}
|
||||
|
||||
const blob = new Blob([new URLSearchParams(body).toString()], {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
type SetterOptions = {
|
||||
updateAll?: boolean;
|
||||
};
|
||||
|
||||
type Listener<T> = (value: T, previous: T | null) => void;
|
||||
|
||||
export function withPrevious<T>(value: T) {
|
||||
let current = value;
|
||||
let previous: T | null = null;
|
||||
|
||||
let subscribers: Array<Listener<T>> = [];
|
||||
|
||||
const subscribe = (listener: Listener<T>) => {
|
||||
subscribers.push(listener);
|
||||
listener(current, previous);
|
||||
|
||||
return () => {
|
||||
subscribers = subscribers.filter((s) => s !== listener);
|
||||
};
|
||||
};
|
||||
|
||||
const set = (value: T, options?: SetterOptions) => {
|
||||
previous = options?.updateAll ? value : current;
|
||||
current = value;
|
||||
|
||||
subscribers.forEach((s) => s(current, previous));
|
||||
};
|
||||
|
||||
const getPrevious = () => previous;
|
||||
|
||||
const currentStore = {
|
||||
subscribe,
|
||||
set,
|
||||
getPrevious
|
||||
};
|
||||
|
||||
return currentStore;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
@@ -1,65 +1,54 @@
|
||||
<script lang="ts">
|
||||
import { EmptySearch, Pagination, Trim } from '$lib/components';
|
||||
import { AvatarInitials, EmptySearch, Pagination, Trim } from '$lib/components';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
TableCellHead,
|
||||
TableCell,
|
||||
TableCellText
|
||||
TableCellText,
|
||||
TableScroll
|
||||
} from '$lib/elements/table';
|
||||
import { Container } from '$lib/layout';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { sdkForConsole } from '$lib/stores/sdk';
|
||||
import { PAGE_LIMIT } from '$lib/constants';
|
||||
import type { Models } from '@aw-labs/appwrite-console';
|
||||
|
||||
export let logs: Models.LogList;
|
||||
export let path: string;
|
||||
export let offset = 0;
|
||||
|
||||
const getBrowser = (clientCode: string) => {
|
||||
return sdkForConsole.avatars.getBrowser(clientCode, 40, 40);
|
||||
};
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
{#if logs.total}
|
||||
<Table>
|
||||
<TableScroll>
|
||||
<TableHeader>
|
||||
<TableCellHead>Client</TableCellHead>
|
||||
<TableCellHead>Event</TableCellHead>
|
||||
<TableCellHead>Location</TableCellHead>
|
||||
<TableCellHead>IP</TableCellHead>
|
||||
<TableCellHead>Date</TableCellHead>
|
||||
<TableCellHead width={100}>User</TableCellHead>
|
||||
<TableCellHead width={100}>Event</TableCellHead>
|
||||
<TableCellHead width={80}>Location</TableCellHead>
|
||||
<TableCellHead width={90}>IP</TableCellHead>
|
||||
<TableCellHead width={140}>Date</TableCellHead>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each logs.logs as log}
|
||||
<TableRow>
|
||||
<TableCell title="Client">
|
||||
{#if log.clientName}
|
||||
<div class="u-flex u-cross-center u-gap-12">
|
||||
<div class="avatar is-small">
|
||||
<img
|
||||
height="20"
|
||||
width="20"
|
||||
src={getBrowser(log.clientCode).toString()}
|
||||
alt={log.clientName} />
|
||||
<div class="u-flex u-cross-center u-gap-12">
|
||||
{#if log.userEmail}
|
||||
{#if log.userName}
|
||||
<AvatarInitials size={32} name={log.userName} />
|
||||
<Trim>{log.userName}</Trim>
|
||||
{:else}
|
||||
<AvatarInitials size={32} name={log.userEmail} />
|
||||
<Trim>{log.userEmail}</Trim>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="avatar is-size-small ">
|
||||
<span class="icon-anonymous" aria-hidden="true" />
|
||||
</div>
|
||||
<Trim>
|
||||
{log.clientName}
|
||||
{log.clientVersion}
|
||||
on {log.osName}
|
||||
{log.osVersion}
|
||||
</Trim>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="u-flex u-cross-center u-gap-12">
|
||||
<span class="avatar is-color-empty" />
|
||||
<p class="text u-trim">Unknown</p>
|
||||
</div>
|
||||
{/if}
|
||||
<span class="text u-trim">{log.userName ?? 'Anonymous'}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCellText title="Event">{log.event}</TableCellText>
|
||||
|
||||
@@ -75,7 +64,7 @@
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableScroll>
|
||||
<div class="u-flex u-margin-block-start-32 u-main-space-between">
|
||||
<p class="text">Total results: {logs.total}</p>
|
||||
<Pagination limit={PAGE_LIMIT} {path} {offset} sum={logs.total} />
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<nav class="breadcrumbs is-only-desktop" aria-label="breadcrumb">
|
||||
<ol class="breadcrumbs-list">
|
||||
{#each breadcrumbs as breadcrumb}
|
||||
<li class="breadcrumbs-item">
|
||||
<li class="breadcrumbs-item" data-private>
|
||||
{#if breadcrumb.href}
|
||||
<a href={breadcrumb.href} title={breadcrumb.title} on:click={track}>
|
||||
{breadcrumb.title}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<span class="icon-cheveron-left" aria-hidden="true" />
|
||||
</a>
|
||||
{/if}
|
||||
<span class="text u-trim-1">
|
||||
<span class="text u-trim-1" data-private>
|
||||
<slot />
|
||||
</span>
|
||||
</Heading>
|
||||
|
||||
@@ -1,47 +1,68 @@
|
||||
<footer class="main-footer">
|
||||
<div class="container">
|
||||
<ul class="inline-links is-center">
|
||||
<script>
|
||||
import { Mode, MODE } from '$lib/system';
|
||||
|
||||
import { version } from '$routes/console/store';
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
</script>
|
||||
|
||||
<footer class="main-footer u-cross-center">
|
||||
<div class="main-footer-start">
|
||||
<ul class="inline-links is-no-padding-first-and-last u-x-small">
|
||||
<li class="inline-links-item">
|
||||
<a
|
||||
class="link"
|
||||
href="https://github.com/appwrite/appwrite"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
<span class="icon-github-circled" aria-hidden="true" />
|
||||
<span class="text">GitHub</span>
|
||||
<div class="u-flex u-cross-center u-gap-8">
|
||||
{#if MODE === Mode.CLOUD}
|
||||
<span class="icon-cloud" />
|
||||
{/if}
|
||||
{#if $version}
|
||||
<a
|
||||
class="text"
|
||||
href="https://github.com/appwrite/appwrite/blob/master/CHANGES.md"
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
Version {$version}
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
</li>
|
||||
<li class="inline-links-item">
|
||||
<a href="https://appwrite.io/policy/terms" target="_blank" rel="noreferrer">
|
||||
<span class="text">Terms</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="inline-links-item">
|
||||
<a
|
||||
class="link"
|
||||
href="https://appwrite.io/discord"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
<span class="icon-discord" aria-hidden="true" />
|
||||
<span class="text">Discord</span>
|
||||
<a href="https://appwrite.io/policy/privacy" target="_blank" rel="noreferrer">
|
||||
<span class="text">Privacy</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="main-footer-end">
|
||||
<ul class="inline-links is-no-padding-first-and-last u-x-small">
|
||||
<li class="inline-links-item">
|
||||
<a
|
||||
class="link"
|
||||
href="https://github.com/appwrite/appwrite/issues/new"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
<span class="text">Open an Issue</span>
|
||||
</a>
|
||||
<span class="text">ⓒ {currentYear} Appwrite. All rights reserved.</span>
|
||||
</li>
|
||||
<li class="inline-links-item">
|
||||
<a
|
||||
class="link"
|
||||
href="https://appwrite.io/docs"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
<span class="text">Docs</span>
|
||||
<li class="inline-links-item u-flex u-gap-8">
|
||||
<a href="https://github.com/appwrite/appwrite" target="_blank" rel="noreferrer">
|
||||
<span class="icon-github" aria-hidden="true" aria-label="Appwrite on Github" />
|
||||
</a>
|
||||
<a href="https://appwrite.io/discord" target="_blank" rel="noreferrer">
|
||||
<span
|
||||
class="icon-discord"
|
||||
aria-hidden="true"
|
||||
aria-label="Appwrite on Discord" />
|
||||
</a>
|
||||
</li>
|
||||
<li class="inline-links-item">
|
||||
<span class="text">API:0.13.2</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<style lang="scss">
|
||||
.main-footer {
|
||||
margin-block-start: auto;
|
||||
}
|
||||
|
||||
[class^='icon-']:not(:hover) {
|
||||
color: hsl(var(--color-neutral-50));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -19,7 +19,9 @@
|
||||
let showFeedback = false;
|
||||
import { slide } from 'svelte/transition';
|
||||
import { page } from '$app/stores';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { Submit, trackEvent } from '$lib/actions/analytics';
|
||||
import { sdkForConsole } from '$lib/stores/sdk';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
let showDropdown = false;
|
||||
let droplistElement: HTMLDivElement;
|
||||
@@ -32,6 +34,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
await sdkForConsole.account.deleteSession('current');
|
||||
trackEvent(Submit.AccountLogout);
|
||||
await goto(`${base}/login`);
|
||||
}
|
||||
|
||||
function onBlur(event: MouseEvent) {
|
||||
if (
|
||||
showDropdown &&
|
||||
@@ -85,7 +93,7 @@
|
||||
<span class="text">Support</span>
|
||||
</a>
|
||||
</nav>
|
||||
<nav class="u-flex u-height-100-percents u-sep-inline-start">
|
||||
<nav class="u-flex u-height-100-percent u-sep-inline-start">
|
||||
{#if $user}
|
||||
<div class="drop-wrapper" class:is-open={showDropdown} bind:this={droplistElement}>
|
||||
<button class="user-profile-button" on:click={() => (showDropdown = !showDropdown)}>
|
||||
@@ -106,9 +114,9 @@
|
||||
<div
|
||||
class="drop is-no-arrow is-block-end is-inline-end"
|
||||
transition:slide={{ duration: 100 }}>
|
||||
<section class="drop-section u-overflow-y-auto u-max-height-200">
|
||||
<ul class="drop-list">
|
||||
{#if $organizationList?.total}
|
||||
{#if $organizationList?.total}
|
||||
<section class="drop-section u-overflow-y-auto u-max-height-200">
|
||||
<ul class="drop-list">
|
||||
{#each $organizationList.teams as org}
|
||||
<DropListLink
|
||||
href={`${base}/console/organization-${org.$id}`}
|
||||
@@ -116,9 +124,9 @@
|
||||
showDropdown = false;
|
||||
}}>{org.name}</DropListLink>
|
||||
{/each}
|
||||
{/if}
|
||||
</ul>
|
||||
</section>
|
||||
</ul>
|
||||
</section>
|
||||
{/if}
|
||||
<section class="drop-section">
|
||||
<ul class="drop-list">
|
||||
<DropListItem
|
||||
@@ -134,6 +142,14 @@
|
||||
on:click={() => (showDropdown = false)}>
|
||||
Your Account
|
||||
</DropListLink>
|
||||
<DropListItem
|
||||
icon="logout-right"
|
||||
on:click={() => {
|
||||
showDropdown = false;
|
||||
logout();
|
||||
}}>
|
||||
Sign Out
|
||||
</DropListItem>
|
||||
</ul>
|
||||
</section>
|
||||
<section class="drop-section">
|
||||
|
||||
+18
-13
@@ -47,10 +47,14 @@
|
||||
<svelte:window on:keydown={handleKeydown} />
|
||||
|
||||
{#if $log.data}
|
||||
<section class="cover-frame">
|
||||
<section class="cover-frame" data-private>
|
||||
<header class="cover-frame-header u-flex u-gap-16 u-main-space-between u-cross-center">
|
||||
<h1 class="body-text-1" name="title">Function ID: {$log.func.$id}</h1>
|
||||
<button on:click={() => ($log.show = false)} class="x-button" aria-label="close popup">
|
||||
<h1 class="body-text-1">Function ID: {$log.func.$id}</h1>
|
||||
<button
|
||||
on:click={() => ($log.show = false)}
|
||||
class="button is-text is-only-icon"
|
||||
style="--button-size:1.5rem;"
|
||||
aria-label="close popup">
|
||||
<span class="icon-x" aria-hidden="true" />
|
||||
</button>
|
||||
</header>
|
||||
@@ -79,11 +83,10 @@
|
||||
<div>Size: {size.value} {size.unit}</div>
|
||||
</div>
|
||||
<div class="status u-margin-inline-start-auto">
|
||||
<div class="u-flex u-flex-vertical u-cross-end">
|
||||
<div class="status u-margin-inline-start-auto">
|
||||
<Status status={$log.data.status}>{$log.data.status}</Status>
|
||||
<p class="text">
|
||||
{calculateTime($log.data.buildTime)}
|
||||
</p>
|
||||
|
||||
<time>{calculateTime($log.data.buildTime)}</time>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -116,11 +119,13 @@
|
||||
</header>
|
||||
{#if selectedTab === 'logs'}
|
||||
<code class="code-panel-content">
|
||||
{$log.data.buildStdout ?? 'No logs recorded'}
|
||||
{$log.data.buildStdout ? $log.data.buildStdout : 'No logs recorded'}
|
||||
</code>
|
||||
{:else}
|
||||
<code class="code-panel-content">
|
||||
{$log.data.buildStderr ?? 'No errors recorded'}
|
||||
{$log.data.buildStderr
|
||||
? $log.data.buildStderr
|
||||
: 'No errors recorded'}
|
||||
</code>
|
||||
{/if}
|
||||
</section>
|
||||
@@ -149,7 +154,7 @@
|
||||
Created at: {toLocaleDateTime($log.data.$createdAt)}</time>
|
||||
</div>
|
||||
<div>
|
||||
<p>Triggered by: {$log.data.trigger}</p>
|
||||
<p>Triggered by: <b>{$log.data.trigger}</b></p>
|
||||
</div>
|
||||
<div class="status u-margin-inline-start-auto">
|
||||
<Status status={$log.data.status}>{$log.data.status}</Status>
|
||||
@@ -191,15 +196,15 @@
|
||||
</header>
|
||||
{#if selectedTab === 'logs'}
|
||||
<code class="code-panel-content">
|
||||
{$log.data.stdout ?? 'No logs recorded'}
|
||||
{$log.data.stdout ? $log.data.stdout : 'No logs recorded'}
|
||||
</code>
|
||||
{:else if selectedTab === 'errors'}
|
||||
<code class="code-panel-content">
|
||||
{$log.data.stderr ?? 'No errors recorded'}
|
||||
{$log.data.stderr ? $log.data.stderr : 'No errors recorded'}
|
||||
</code>
|
||||
{:else}
|
||||
<code class="code-panel-content">
|
||||
{$log.data.response ?? 'No response recorded'}
|
||||
{$log.data.response ? $log.data.response : 'No response recorded'}
|
||||
</code>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { flip } from 'svelte/animate';
|
||||
import type { Notification } from '../stores/notifications';
|
||||
|
||||
export let type: Notification['type'] = 'info';
|
||||
@@ -18,7 +17,11 @@
|
||||
class:is-danger={type === 'error'}
|
||||
class:is-info={type === 'info'}
|
||||
transition:fly={{ x: 50 }}>
|
||||
<button class="x-button" aria-label="close alert" on:click={() => dispatch('dismiss')}>
|
||||
<button
|
||||
class="button is-text is-only-icon"
|
||||
style="--button-size:1.5rem;"
|
||||
aria-label="close alert"
|
||||
on:click={() => dispatch('dismiss')}>
|
||||
<span class="icon-x" aria-hidden="true" />
|
||||
</button>
|
||||
<div class="alert-sticky-image">
|
||||
@@ -36,7 +39,7 @@
|
||||
<p><slot /></p>
|
||||
</div>
|
||||
{#if buttons}
|
||||
<div class="buttons u-flex">
|
||||
<div class="alert-sticky-buttons u-flex">
|
||||
{#each buttons as button}
|
||||
<button class="button is-text is-small" on:click={button.method}>
|
||||
<span class="text">{button.name}</span>
|
||||
|
||||
@@ -41,22 +41,23 @@
|
||||
|
||||
<main
|
||||
class:grid-with-side={showSideNavigation}
|
||||
class:grid={!showSideNavigation}
|
||||
class:is-open={isOpen}
|
||||
class:u-hide={$wizard.show || $log.show}>
|
||||
<header class="main-header u-padding-inline-end-0">
|
||||
<button
|
||||
class:u-hide={!showSideNavigation}
|
||||
class="icon-button is-no-desktop"
|
||||
class="icon-button is-not-desktop"
|
||||
aria-label="Open Menu"
|
||||
on:click={toggleMenu}>
|
||||
<span class:icon-x={isOpen} class:icon-menu={!isOpen} aria-hidden="true" />
|
||||
</button>
|
||||
<slot name="header" />
|
||||
</header>
|
||||
<nav class="main-side">
|
||||
<slot name="side" />
|
||||
</nav>
|
||||
{#if showSideNavigation}
|
||||
<nav class="main-side">
|
||||
<slot name="side" />
|
||||
</nav>
|
||||
{/if}
|
||||
<section class="main-content">
|
||||
{#if $page.data?.header}
|
||||
<svelte:component this={$page.data.header} />
|
||||
@@ -64,11 +65,17 @@
|
||||
|
||||
<slot />
|
||||
</section>
|
||||
<slot name="footer" />
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.grid-with-side {
|
||||
<style lang="scss">
|
||||
main {
|
||||
min-height: 100vh;
|
||||
|
||||
&:not(.grid-with-side) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.main-side {
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
import LoginDark from '$lib/images/login/login-dark-mode.svg';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { base } from '$app/paths';
|
||||
import { user } from '$lib/stores/user';
|
||||
|
||||
export let imgLight = LoginLight;
|
||||
export let imgDark = LoginDark;
|
||||
|
||||
const technologies = [
|
||||
'js',
|
||||
@@ -25,33 +29,33 @@
|
||||
<div
|
||||
class="container u-margin-block-start-20"
|
||||
style="--p-container-max-size: var(--container-size-medium);">
|
||||
<a href="/">
|
||||
<a href={user ? '/console' : '/'}>
|
||||
<img src={AppwriteLogo} width="196" height="47" class="u-block" alt="Appwrite" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="u-margin-block-start-auto" />
|
||||
|
||||
<div class="u-margin-block-start-auto is-no-mobile" />
|
||||
<div class="u-margin-block-start-auto is-not-mobile" />
|
||||
<div
|
||||
class="container u-margin-block-start-20 is-no-mobile"
|
||||
class="container u-margin-block-start-20 is-not-mobile"
|
||||
style="--p-container-max-size: var(--container-size-large);">
|
||||
{#if $app.themeInUse === 'dark'}
|
||||
<img src={LoginDark} alt="" class="u-only-dark" />
|
||||
<img src={imgDark} alt="" class="u-only-dark" />
|
||||
{:else}
|
||||
<img src={LoginLight} alt="" class="u-only-light" />
|
||||
<img src={imgLight} alt="" class="u-only-light" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="u-margin-block-start-auto" />
|
||||
|
||||
<div class="u-margin-block-start-auto is-no-mobile" />
|
||||
<div class="u-margin-block-start-auto is-not-mobile" />
|
||||
<div
|
||||
class="container u-text-color-light-gray is-no-mobile"
|
||||
class="container u-color-text-gray is-not-mobile"
|
||||
style="--p-container-max-size:var(--container-size-small); --p-container-padding-inline:1rem;">
|
||||
<p>Integrate with your favourite technologies</p>
|
||||
<ul
|
||||
class="u-flex u-main-center u-flex-wrap u-gap-16 u-margin-block-start-32 u-line-height-1 u-opacity-0-5">
|
||||
class="u-flex u-main-center u-flex-wrap u-gap-16 u-margin-block-start-32 u-line-height-1 u-opacity-50">
|
||||
{#each technologies as tech}
|
||||
<li>
|
||||
<img
|
||||
@@ -66,7 +70,7 @@
|
||||
<div class="u-margin-block-start-40" />
|
||||
</section>
|
||||
<section class="grid-1-1-col-2 u-flex u-main-center u-cross-center">
|
||||
<div class="container u-flex u-flex-vertical u-cross-center u-height-100-percents">
|
||||
<div class="container u-flex u-flex-vertical u-cross-center u-height-100-percent">
|
||||
<div class="u-margin-block-start-auto" />
|
||||
|
||||
<div class="u-max-width-500 u-width-full-line">
|
||||
|
||||
+11
-10
@@ -2,6 +2,7 @@
|
||||
export type UsagePeriods = '24h' | '30d' | '90d';
|
||||
|
||||
export function last(set: Models.Metric[]): Models.Metric | null {
|
||||
if (!set) return null;
|
||||
return set.slice(-1)[0] ?? null;
|
||||
}
|
||||
|
||||
@@ -13,7 +14,7 @@
|
||||
<script lang="ts">
|
||||
import { Container } from '$lib/layout';
|
||||
import { BarChart, LineChart } from '$lib/charts';
|
||||
import { Card, DropTabs, DropTabsLink, Heading, Tiles } from '$lib/components';
|
||||
import { Card, SecondaryTabs, SecondaryTabsItem, Heading, Tiles } from '$lib/components';
|
||||
import { Colors } from '$lib/charts/config';
|
||||
import type { Models } from '@aw-labs/appwrite-console';
|
||||
import { page } from '$app/stores';
|
||||
@@ -41,23 +42,23 @@
|
||||
<Container>
|
||||
<div class="u-flex u-main-space-between common-section">
|
||||
<Heading tag="h2" size="5">{title}</Heading>
|
||||
<DropTabs>
|
||||
<DropTabsLink href={`${path}/24h`} disabled={$page.params.period === '24h'}>
|
||||
<SecondaryTabs>
|
||||
<SecondaryTabsItem href={`${path}/24h`} disabled={$page.params.period === '24h'}>
|
||||
24h
|
||||
</DropTabsLink>
|
||||
<DropTabsLink
|
||||
</SecondaryTabsItem>
|
||||
<SecondaryTabsItem
|
||||
href={`${path}/30d`}
|
||||
disabled={!$page.params.period || $page.params.period === '30d'}>
|
||||
30d
|
||||
</DropTabsLink>
|
||||
<DropTabsLink href={`${path}/90d`} disabled={$page.params.period === '90d'}>
|
||||
</SecondaryTabsItem>
|
||||
<SecondaryTabsItem href={`${path}/90d`} disabled={$page.params.period === '90d'}>
|
||||
90d
|
||||
</DropTabsLink>
|
||||
</DropTabs>
|
||||
</SecondaryTabsItem>
|
||||
</SecondaryTabs>
|
||||
</div>
|
||||
<Card>
|
||||
{#if count}
|
||||
<Heading tag="h6" size="6">{last(count).value}</Heading>
|
||||
<Heading tag="h6" size="6">{total(count)}</Heading>
|
||||
<p>{countMetadata.title}</p>
|
||||
<div class="u-margin-block-start-16" />
|
||||
<div class="chart-container">
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
|
||||
import { Steps } from '$lib/components';
|
||||
import { Button, Form } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
@@ -31,6 +33,9 @@
|
||||
const openDialog = document.querySelectorAll('dialog[open]');
|
||||
if (event.key === 'Escape' && !showExitModal && !openDialog?.length) {
|
||||
event.preventDefault();
|
||||
trackEvent('wizard_exit', {
|
||||
from: 'escape'
|
||||
});
|
||||
dispatch('exit');
|
||||
wizard.hide();
|
||||
}
|
||||
@@ -40,6 +45,9 @@
|
||||
if (confirmExit) {
|
||||
showExitModal = true;
|
||||
} else {
|
||||
trackEvent('wizard_exit', {
|
||||
from: 'button'
|
||||
});
|
||||
dispatch('exit');
|
||||
wizard.hide();
|
||||
}
|
||||
@@ -67,8 +75,10 @@
|
||||
|
||||
wizard.setInterceptor(null);
|
||||
if (isLastStep) {
|
||||
trackEvent('wizard_finish');
|
||||
dispatch('finish');
|
||||
} else {
|
||||
trackEvent('wizard_next');
|
||||
currentStep++;
|
||||
}
|
||||
}
|
||||
@@ -88,7 +98,11 @@
|
||||
<div class="body-text-1">{title}</div>
|
||||
|
||||
<slot name="header" />
|
||||
<button class="x-button" aria-label="close wizard" on:click={handleExit}>
|
||||
<button
|
||||
class="button is-text is-only-icon"
|
||||
style="--button-size:1.5rem;"
|
||||
aria-label="close wizard"
|
||||
on:click={handleExit}>
|
||||
<span class="icon-x" aria-hidden="true" />
|
||||
</button>
|
||||
</header>
|
||||
@@ -125,10 +139,16 @@
|
||||
<Button secondary on:click={handleExit}>Cancel</Button>
|
||||
<Button submit>Next</Button>
|
||||
{:else if isLastStep}
|
||||
<Button secondary on:click={() => currentStep--}>Back</Button>
|
||||
<Button
|
||||
secondary
|
||||
on:click={() => currentStep--}
|
||||
on:click={() => trackEvent('wizard_back')}>Back</Button>
|
||||
<Button submit>{finalAction}</Button>
|
||||
{:else}
|
||||
<Button secondary on:click={() => currentStep--}>Back</Button>
|
||||
<Button
|
||||
secondary
|
||||
on:click={() => currentStep--}
|
||||
on:click={() => trackEvent('wizard_back')}>Back</Button>
|
||||
<Button submit>Next</Button>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -138,7 +158,14 @@
|
||||
</section>
|
||||
|
||||
{#if showExitModal}
|
||||
<WizardExitModal bind:show={showExitModal} on:exit={() => wizard.hide()}>
|
||||
<WizardExitModal
|
||||
bind:show={showExitModal}
|
||||
on:exit={() => {
|
||||
trackEvent('wizard_exit', {
|
||||
from: 'prompt'
|
||||
});
|
||||
wizard.hide();
|
||||
}}>
|
||||
<slot name="exit">this process</slot>
|
||||
</WizardExitModal>
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { Models, Payload } from '@aw-labs/appwrite-console';
|
||||
import { Service } from './service';
|
||||
|
||||
export class Project extends Service {
|
||||
/**
|
||||
* Get usage stats for a project
|
||||
*
|
||||
*
|
||||
* @param {string} range
|
||||
* @throws {AppwriteException}
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async getUsage(range?: string): Promise<Models.UsageProject> {
|
||||
const path = '/project/usage';
|
||||
const payload: Payload = {};
|
||||
|
||||
if (typeof range !== 'undefined') {
|
||||
payload['range'] = range;
|
||||
}
|
||||
|
||||
const uri = new URL(this.client.config.endpoint + path);
|
||||
return await this.client.call(
|
||||
'get',
|
||||
uri,
|
||||
{
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
payload
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { Client, Payload } from '@aw-labs/appwrite-console';
|
||||
|
||||
export class Service {
|
||||
static CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
|
||||
|
||||
client: Client;
|
||||
|
||||
constructor(client: Client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
static flatten(data: Payload, prefix = ''): Payload {
|
||||
let output: Payload = {};
|
||||
|
||||
for (const key in data) {
|
||||
const value = data[key];
|
||||
const finalKey = prefix ? `${prefix}[${key}]` : key;
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
output = Object.assign(output, this.flatten(value, finalKey));
|
||||
} else {
|
||||
output[finalKey] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { VARS } from '$lib/system';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export type AppStore = {
|
||||
@@ -59,7 +60,8 @@ function createFeedbackStore() {
|
||||
email?: string,
|
||||
value?: number
|
||||
) => {
|
||||
const response = await fetch('https://growth.appwrite.io/v1/feedback', {
|
||||
if (!VARS.GROWTH_ENDPOINT) return;
|
||||
const response = await fetch(`${VARS.GROWTH_ENDPOINT}/feedback`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
|
||||
@@ -37,7 +37,7 @@ export function createPersistentPagination(limit: number) {
|
||||
}
|
||||
|
||||
goto(target, {
|
||||
noscroll: true
|
||||
noScroll: true
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -54,6 +54,11 @@ function createServices() {
|
||||
label: 'Users',
|
||||
method: 'users',
|
||||
value: null
|
||||
},
|
||||
{
|
||||
label: 'GraphQL',
|
||||
method: 'graphql',
|
||||
value: null
|
||||
}
|
||||
]
|
||||
});
|
||||
@@ -107,6 +112,11 @@ function createServices() {
|
||||
label: 'Users',
|
||||
method: 'users',
|
||||
value: project.serviceStatusForUsers
|
||||
},
|
||||
{
|
||||
label: 'GraphQL',
|
||||
method: 'graphql',
|
||||
value: project.serviceStatusForGraphql
|
||||
}
|
||||
];
|
||||
set({ list });
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Project } from '$lib/sdk/project';
|
||||
import { VARS } from '$lib/system';
|
||||
import {
|
||||
Account,
|
||||
Avatars,
|
||||
@@ -12,8 +14,8 @@ import {
|
||||
Users
|
||||
} from '@aw-labs/appwrite-console';
|
||||
|
||||
const endpoint =
|
||||
import.meta.env.VITE_APPWRITE_ENDPOINT?.toString() ?? `${window?.location?.origin}/v1`;
|
||||
const endpoint = VARS.APPWRITE_ENDPOINT ?? `${globalThis?.location?.origin}/v1`;
|
||||
|
||||
const clientConsole = new Client();
|
||||
clientConsole.setEndpoint(endpoint).setProject('console');
|
||||
|
||||
@@ -42,7 +44,7 @@ const sdkForProject = {
|
||||
functions: new Functions(clientProject),
|
||||
health: new Health(clientProject),
|
||||
locale: new Locale(clientProject),
|
||||
projects: new Projects(clientProject),
|
||||
project: new Project(clientProject),
|
||||
storage: new Storage(clientProject),
|
||||
teams: new Teams(clientProject),
|
||||
users: new Users(clientProject)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user