mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch 'main' into feat/in-progress-execution-timer
This commit is contained in:
+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')
|
||||
},
|
||||
|
||||
+28
-28
@@ -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
|
||||
|
||||
@@ -62,7 +62,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
|
||||
@@ -76,7 +76,7 @@ npm run lint
|
||||
|
||||
### Diagnostics
|
||||
|
||||
Diagnostic tool that checks for following:
|
||||
Diagnostic tool that checks for the following:
|
||||
|
||||
- Unused CSS
|
||||
- Svelte A11y hints
|
||||
@@ -88,7 +88,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`
|
||||
|
||||
@@ -106,11 +106,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
|
||||
@@ -143,7 +143,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`
|
||||
@@ -158,21 +158,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
|
||||
@@ -181,29 +181,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
|
||||
|
||||
@@ -211,27 +211,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
+2964
-7486
File diff suppressed because it is too large
Load Diff
+27
-26
@@ -20,46 +20,47 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@analytics/google-analytics": "^1.0.5",
|
||||
"@appwrite.io/pink": "^0.0.3",
|
||||
"@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",
|
||||
"@sentry/svelte": "^7.36.0",
|
||||
"@sentry/tracing": "^7.36.0",
|
||||
"analytics": "^0.8.1",
|
||||
"echarts": "^5.4.0",
|
||||
"pretty-bytes": "^6.0.0",
|
||||
"echarts": "^5.4.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.4",
|
||||
"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.4"
|
||||
},
|
||||
"type": "module",
|
||||
"pre-commit": [
|
||||
|
||||
@@ -18,7 +18,7 @@ export function trackEvent(name: string, data: object = null): void {
|
||||
if (!isTrackingAllowed()) {
|
||||
return;
|
||||
}
|
||||
const path = get(page).routeId;
|
||||
const path = get(page).route.id;
|
||||
analytics.track(name, { ...data, path });
|
||||
sendEventToGrowth(name, path, data);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
export let value: string;
|
||||
export let event: string = null;
|
||||
export let appendTo: Parameters<typeof tooltip>['1']['appendTo'] = undefined;
|
||||
|
||||
let content = 'Click to copy';
|
||||
|
||||
@@ -36,7 +37,8 @@
|
||||
on:mouseenter={() => setTimeout(() => (content = 'Click to copy'))}
|
||||
use:tooltip={{
|
||||
content,
|
||||
hideOnClick: false
|
||||
hideOnClick: false,
|
||||
appendTo
|
||||
}}>
|
||||
<slot />
|
||||
</span>
|
||||
|
||||
@@ -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,184 +1,241 @@
|
||||
<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';
|
||||
|
||||
// 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);
|
||||
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);
|
||||
function select(field: 'resource', value: EventResource);
|
||||
function select(field: 'action', value: EventAction);
|
||||
function select(field: 'attribute', value: string);
|
||||
function select(field: string, value: string | EventService | EventResource | EventAction) {
|
||||
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">
|
||||
@@ -187,134 +244,126 @@
|
||||
<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-50={helper !== route.description}
|
||||
on:mouseenter={() => {
|
||||
helper = route.description;
|
||||
}}
|
||||
on:mouseleave={() => {
|
||||
helper = null;
|
||||
}}>
|
||||
{route.value}
|
||||
</button>
|
||||
<span class="u-opacity-50">
|
||||
{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 +371,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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
url.search = value;
|
||||
|
||||
goto(url, { keepfocus: true });
|
||||
goto(url, { keepFocus: true });
|
||||
}, debounce);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -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,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
import { Copy } from '.';
|
||||
|
||||
export let show = false;
|
||||
@@ -16,7 +17,8 @@
|
||||
class="interactive-text-output-button"
|
||||
aria-label="show hidden text"
|
||||
type="button"
|
||||
on:click={() => (show = !show)}>
|
||||
on:click={() => (show = !show)}
|
||||
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}>
|
||||
|
||||
@@ -176,3 +176,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: 'verifications', 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' : ''}
|
||||
|
||||
@@ -20,5 +20,6 @@ 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';
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
}
|
||||
|
||||
if (element.validity.patternMismatch) {
|
||||
error = 'Please enter a valid CRON expression';
|
||||
error = 'Please enter a valid Cron expression';
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { Trim } from '$lib/components';
|
||||
import { Button } from '.';
|
||||
import { humanFileSize } from '$lib/helpers/sizeConvertion';
|
||||
|
||||
export let label: string = null;
|
||||
export let files: FileList;
|
||||
export let list = new DataTransfer();
|
||||
export let allowedFileExtensions: string[] = [];
|
||||
export let maxSize: number = 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 +28,8 @@
|
||||
function dragOverHandler() {
|
||||
hovering = true;
|
||||
}
|
||||
|
||||
$: fileArray = files?.length ? Array.from(files) : [];
|
||||
</script>
|
||||
|
||||
<input
|
||||
@@ -42,42 +44,77 @@
|
||||
<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 files 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="button is-text is-only-icon"
|
||||
style="--button-size:1.5rem;"
|
||||
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>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<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._-]*$/`;
|
||||
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-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>
|
||||
@@ -19,8 +19,6 @@
|
||||
role="textbox"
|
||||
{id}
|
||||
aria-placeholder={placeholder}
|
||||
name={id}
|
||||
type="text"
|
||||
class="input-text is-resizable"
|
||||
bind:textContent={value} />
|
||||
{:else}
|
||||
|
||||
@@ -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' : ''}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,11 @@
|
||||
/**
|
||||
* 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$/, '');
|
||||
}
|
||||
@@ -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']
|
||||
: '';
|
||||
}
|
||||
|
||||
@@ -93,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)}>
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
{#if $log.data}
|
||||
<section class="cover-frame">
|
||||
<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>
|
||||
<h1 class="body-text-1">Function ID: {$log.func.$id}</h1>
|
||||
<button
|
||||
on:click={() => ($log.show = false)}
|
||||
class="button is-text is-only-icon"
|
||||
|
||||
@@ -67,7 +67,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">
|
||||
|
||||
@@ -14,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';
|
||||
@@ -42,19 +42,19 @@
|
||||
<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}
|
||||
|
||||
@@ -37,7 +37,7 @@ export function createPersistentPagination(limit: number) {
|
||||
}
|
||||
|
||||
goto(target, {
|
||||
noscroll: true
|
||||
noScroll: true
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -53,7 +53,6 @@
|
||||
!$page.url.pathname.startsWith('/console') &&
|
||||
!$page.url.pathname.startsWith('/invite')
|
||||
) {
|
||||
console.log('test');
|
||||
await goto(`${base}/console`, {
|
||||
replaceState: true
|
||||
});
|
||||
@@ -73,7 +72,7 @@
|
||||
});
|
||||
|
||||
afterNavigate((navigation) => {
|
||||
trackPageView(navigation.to.routeId);
|
||||
trackPageView(navigation.to.route.id);
|
||||
});
|
||||
|
||||
$: {
|
||||
|
||||
+6
-1
@@ -1,7 +1,12 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { base } from '$app/paths';
|
||||
import type { PageLoad } from './$types';
|
||||
import { user } from '$lib/stores/user';
|
||||
|
||||
export const load: PageLoad = async () => {
|
||||
throw redirect(302, `${base}/login`);
|
||||
if (user) {
|
||||
return;
|
||||
} else {
|
||||
throw redirect(302, `${base}/login`);
|
||||
}
|
||||
};
|
||||
|
||||
+1
-1
@@ -84,7 +84,7 @@
|
||||
</Table>
|
||||
{:else}
|
||||
<Empty single>
|
||||
<p>No session available</p>
|
||||
<p>No sessions available</p>
|
||||
<Button
|
||||
external
|
||||
secondary
|
||||
|
||||
+1
@@ -39,6 +39,7 @@
|
||||
? [{ value: externalAttribute.key, order: 'ASC' }]
|
||||
: [{ value: '', order: 'ASC' }];
|
||||
selectedType = 'key';
|
||||
key = `index_${$indexes.length + 1}`;
|
||||
}
|
||||
|
||||
$: if (showCreateIndex) {
|
||||
|
||||
+3
-1
@@ -62,7 +62,9 @@
|
||||
href="https://appwrite.io/docs/permissions"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link">Permissions Guide</a> in our documentation.
|
||||
class="link">
|
||||
Permissions Guide
|
||||
</a>.
|
||||
</p>
|
||||
<svelte:fragment slot="aside">
|
||||
{#if collectionPermissions}
|
||||
|
||||
+3
-1
@@ -13,7 +13,9 @@
|
||||
href="https://appwrite.io/docs/permissions"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link">Permissions Guide</a> in our documentation.
|
||||
class="link">
|
||||
Permissions Guide
|
||||
</a>.
|
||||
</svelte:fragment>
|
||||
{#if $collection.documentSecurity}
|
||||
<div class="common-section">
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
|
||||
const stepsComponents: WizardStepsType = new Map();
|
||||
stepsComponents.set(1, {
|
||||
label: 'Function Details',
|
||||
label: 'Details',
|
||||
component: Step1
|
||||
});
|
||||
stepsComponents.set(2, {
|
||||
@@ -82,7 +82,7 @@
|
||||
optional: true
|
||||
});
|
||||
stepsComponents.set(4, {
|
||||
label: 'Scheduling',
|
||||
label: 'Schedule',
|
||||
component: Step4,
|
||||
optional: true
|
||||
});
|
||||
|
||||
+24
-118
@@ -1,46 +1,35 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
CardGrid,
|
||||
Box,
|
||||
DropList,
|
||||
DropListItem,
|
||||
Empty,
|
||||
EventModal,
|
||||
Output,
|
||||
Secret
|
||||
} from '$lib/components';
|
||||
import { Container } from '$lib/layout';
|
||||
import { Button, InputNumber, InputText, InputCron, Form, FormList } from '$lib/elements/forms';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { Box, CardGrid, DropList, DropListItem, Empty, Output, Secret } from '$lib/components';
|
||||
import Heading from '$lib/components/heading.svelte';
|
||||
import { Roles } from '$lib/components/permissions';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button, Form, FormList, InputCron, InputNumber, InputText } from '$lib/elements/forms';
|
||||
import { symmetricDifference } from '$lib/helpers/array';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { Container } from '$lib/layout';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { execute, func } from '../store';
|
||||
import Delete from './delete.svelte';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdkForProject } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import type { Models } from '@aw-labs/appwrite-console';
|
||||
import { onMount } from 'svelte';
|
||||
import Variable from '../../createVariable.svelte';
|
||||
import { execute, func } from '../store';
|
||||
// import Upload from './uploadVariables.svelte';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { Roles } from '$lib/components/permissions';
|
||||
import { symmetricDifference } from '$lib/helpers/array';
|
||||
import TableList from '$lib/elements/table/tableList.svelte';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellHead,
|
||||
TableCellText,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '$lib/elements/table';
|
||||
import Heading from '$lib/components/heading.svelte';
|
||||
import { writable, type Writable } from 'svelte/store';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import type { PageData } from './$types';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import Delete from './delete.svelte';
|
||||
import UpdateEvents from './updateEvents.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
@@ -56,16 +45,11 @@
|
||||
let permissions: string[] = [];
|
||||
let arePermsDisabled = true;
|
||||
|
||||
const eventSet: Writable<Set<string>> = writable(new Set());
|
||||
let showEvents = false;
|
||||
let areEventsDisabled = true;
|
||||
|
||||
onMount(async () => {
|
||||
timeout ??= $func.timeout;
|
||||
functionName ??= $func.name;
|
||||
functionSchedule ??= $func.schedule;
|
||||
permissions = $func.execute;
|
||||
$eventSet = new Set($func.events);
|
||||
});
|
||||
|
||||
async function updateName() {
|
||||
@@ -118,31 +102,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function updateEvents() {
|
||||
try {
|
||||
await sdkForProject.functions.update(
|
||||
functionId,
|
||||
$func.name,
|
||||
$func.execute,
|
||||
Array.from($eventSet),
|
||||
$func.schedule,
|
||||
$func.timeout,
|
||||
$func.enabled
|
||||
);
|
||||
invalidate(Dependencies.FUNCTION);
|
||||
addNotification({
|
||||
message: 'Permissions have been updated',
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent('submit_function_update_events');
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSchedule() {
|
||||
try {
|
||||
await sdkForProject.functions.update(
|
||||
@@ -158,7 +117,7 @@
|
||||
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'CRON Schedule has been updated'
|
||||
message: 'Cron Schedule has been updated'
|
||||
});
|
||||
trackEvent('submit_function_update_schedule');
|
||||
} catch (error) {
|
||||
@@ -278,21 +237,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleEvent(event: CustomEvent) {
|
||||
eventSet.set($eventSet.add(event.detail));
|
||||
}
|
||||
|
||||
$: if (permissions) {
|
||||
if (symmetricDifference(permissions, $func.execute).length) {
|
||||
arePermsDisabled = false;
|
||||
} else arePermsDisabled = true;
|
||||
}
|
||||
|
||||
$: if ($eventSet) {
|
||||
if (symmetricDifference(Array.from($eventSet), $func.events).length) {
|
||||
areEventsDisabled = false;
|
||||
} else areEventsDisabled = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
@@ -359,7 +308,8 @@
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link">
|
||||
Permissions Guide</a> in our documentation.
|
||||
Permissions Guide
|
||||
</a>.
|
||||
</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<Roles bind:roles={permissions} />
|
||||
@@ -371,66 +321,24 @@
|
||||
</CardGrid>
|
||||
</Form>
|
||||
|
||||
<Form on:submit={updateEvents}>
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Update Events</Heading>
|
||||
<p>Set the events that will trigger your function. Maximum 100 events allowed.</p>
|
||||
<svelte:fragment slot="aside">
|
||||
{#if $eventSet.size}
|
||||
<div class="u-flex u-flex-vertical u-gap-16">
|
||||
<TableList>
|
||||
{#each Array.from($eventSet) as event}
|
||||
<li class="table-row">
|
||||
<TableCellText title="id">
|
||||
{event}
|
||||
</TableCellText>
|
||||
<TableCell showOverflow title="options" width={40}>
|
||||
<button
|
||||
class="button is-text is-only-icon"
|
||||
aria-label="delete id"
|
||||
on:click|preventDefault={() => {
|
||||
$eventSet.delete(event);
|
||||
eventSet.set($eventSet);
|
||||
}}>
|
||||
<span class="icon-x" aria-hidden="true" />
|
||||
</button>
|
||||
</TableCell>
|
||||
</li>
|
||||
{/each}
|
||||
</TableList>
|
||||
|
||||
<Button text noMargin on:click={() => (showEvents = true)}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="u-text">Add event</span>
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<Empty on:click={() => (showEvents = true)}>Add an event to get started</Empty>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={areEventsDisabled} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
<UpdateEvents />
|
||||
|
||||
<Form on:submit={updateSchedule}>
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Update CRON Schedule</Heading>
|
||||
<Heading tag="h6" size="7">Update Schedule</Heading>
|
||||
<p>
|
||||
Set a CRON schedule to trigger your function. Leave blank for no schedule. <a
|
||||
Set a Cron schedule to trigger your function. Leave blank for no schedule. <a
|
||||
href="https://en.wikipedia.org/wiki/Cron"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link">
|
||||
More details on CRON syntax here.</a>
|
||||
More details on Cron syntax here.</a>
|
||||
</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<FormList>
|
||||
<InputCron
|
||||
bind:value={functionSchedule}
|
||||
label="Schedule (CRON Syntax)"
|
||||
label="Schedule (Cron Syntax)"
|
||||
id="schedule" />
|
||||
</FormList>
|
||||
</svelte:fragment>
|
||||
@@ -442,7 +350,7 @@
|
||||
</Form>
|
||||
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Update Function Variables</Heading>
|
||||
<Heading tag="h6" size="7">Update Variables</Heading>
|
||||
<p>Set the variables (or secret keys) that will be passed to your function at runtime.</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<div class="u-flex u-margin-inline-start-auto u-gap-16">
|
||||
@@ -586,5 +494,3 @@
|
||||
on:created={handleVariableCreated}
|
||||
on:updated={handleVariableUpdated} />
|
||||
{/if}
|
||||
<!-- <Upload bind:show={showVariablesUpload} on:uploaded={handleVariableCreated} /> -->
|
||||
<EventModal bind:show={showEvents} on:created={handleEvent} />
|
||||
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
<script lang="ts">
|
||||
import CardGrid from '$lib/components/cardGrid.svelte';
|
||||
import Empty from '$lib/components/empty.svelte';
|
||||
import Heading from '$lib/components/heading.svelte';
|
||||
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import Form from '$lib/elements/forms/form.svelte';
|
||||
import { TableCell, TableCellText } from '$lib/elements/table';
|
||||
import TableList from '$lib/elements/table/tableList.svelte';
|
||||
import { symmetricDifference } from '$lib/helpers/array';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdkForProject } from '$lib/stores/sdk';
|
||||
import { writable, type Writable } from 'svelte/store';
|
||||
import { func } from '../store';
|
||||
import { EventModal } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import DropList from '$lib/components/dropList.svelte';
|
||||
import DropListItem from '$lib/components/dropListItem.svelte';
|
||||
|
||||
const functionId = $page.params.function;
|
||||
const eventSet: Writable<Set<string>> = writable(new Set($func.events));
|
||||
let showEvents = false;
|
||||
let eventValue: string;
|
||||
let isDisabled = true;
|
||||
let showDropdown: boolean[] = [];
|
||||
|
||||
async function updateEvents() {
|
||||
try {
|
||||
await sdkForProject.functions.update(
|
||||
functionId,
|
||||
$func.name,
|
||||
$func.execute,
|
||||
Array.from($eventSet),
|
||||
$func.schedule,
|
||||
$func.timeout,
|
||||
$func.enabled
|
||||
);
|
||||
invalidate(Dependencies.FUNCTION);
|
||||
addNotification({
|
||||
message: 'Permissions have been updated',
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent('submit_function_update_events');
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleEvent(event: CustomEvent) {
|
||||
if (eventValue) {
|
||||
const newSet = new Set(
|
||||
[...$eventSet].map((e) => (e === eventValue ? event.detail : e))
|
||||
);
|
||||
eventSet.set(newSet);
|
||||
} else {
|
||||
eventSet.set($eventSet.add(event.detail));
|
||||
}
|
||||
eventValue = null;
|
||||
}
|
||||
|
||||
$: if ($eventSet) {
|
||||
if (symmetricDifference(Array.from($eventSet), $func.events).length) {
|
||||
isDisabled = false;
|
||||
} else isDisabled = true;
|
||||
}
|
||||
|
||||
$: if (!showEvents) {
|
||||
eventValue = undefined;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Form on:submit={updateEvents}>
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Update Events</Heading>
|
||||
<p>Set the events that will trigger your function. Maximum 100 events allowed.</p>
|
||||
<svelte:fragment slot="aside">
|
||||
{#if $eventSet.size}
|
||||
<TableList>
|
||||
{#each Array.from($eventSet) as event, i}
|
||||
<li class="table-row">
|
||||
<TableCellText title="id">
|
||||
{event}
|
||||
</TableCellText>
|
||||
<TableCell showOverflow title="options" width={40}>
|
||||
<DropList
|
||||
bind:show={showDropdown[i]}
|
||||
placement="bottom-start"
|
||||
noArrow>
|
||||
<button
|
||||
class="button is-text is-only-icon"
|
||||
aria-label="more options"
|
||||
on:click|preventDefault={() =>
|
||||
(showDropdown[i] = !showDropdown[i])}>
|
||||
<span class="icon-dots-horizontal" aria-hidden="true" />
|
||||
</button>
|
||||
<svelte:fragment slot="list">
|
||||
<DropListItem
|
||||
icon="pencil"
|
||||
on:click={() => {
|
||||
showDropdown[i] = false;
|
||||
showEvents = true;
|
||||
eventValue = event;
|
||||
}}>
|
||||
Edit
|
||||
</DropListItem>
|
||||
<DropListItem
|
||||
icon="trash"
|
||||
on:click={async () => {
|
||||
$eventSet.delete(event);
|
||||
eventSet.set($eventSet);
|
||||
}}>
|
||||
Delete
|
||||
</DropListItem>
|
||||
</svelte:fragment>
|
||||
</DropList>
|
||||
</TableCell>
|
||||
</li>
|
||||
{/each}
|
||||
</TableList>
|
||||
|
||||
<div class="u-flex u-margin-block-start-16">
|
||||
<Button text noMargin on:click={() => (showEvents = true)}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="u-text">Add event</span>
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<Empty on:click={() => (showEvents = true)}>Add an event to get started</Empty>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={isDisabled} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
|
||||
<EventModal bind:show={showEvents} initialValue={eventValue} on:created={handleEvent} />
|
||||
+9
-9
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Container } from '$lib/layout';
|
||||
import { Card, DropListLink, DropTabs, Heading } from '$lib/components';
|
||||
import { Card, SecondaryTabsItem, SecondaryTabs, Heading } from '$lib/components';
|
||||
import { last } from '$lib/layout/usage.svelte';
|
||||
import { BarChart } from '$lib/charts';
|
||||
import { page } from '$app/stores';
|
||||
@@ -15,23 +15,23 @@
|
||||
<Container>
|
||||
<div class="u-flex u-main-space-between common-section">
|
||||
<Heading tag="h2" size="5">Functions</Heading>
|
||||
<DropTabs>
|
||||
<DropListLink
|
||||
<SecondaryTabs>
|
||||
<SecondaryTabsItem
|
||||
href={`/console/project-${data.project.$id}/functions/function-${data.function.$id}/usage/24h`}
|
||||
disabled={($page.params.period ?? '24h') === '24h'}>
|
||||
24h
|
||||
</DropListLink>
|
||||
<DropListLink
|
||||
</SecondaryTabsItem>
|
||||
<SecondaryTabsItem
|
||||
href={`/console/project-${data.project.$id}/functions/function-${data.function.$id}/usage/30d`}
|
||||
disabled={$page.params.period === '30d'}>
|
||||
30d
|
||||
</DropListLink>
|
||||
<DropListLink
|
||||
</SecondaryTabsItem>
|
||||
<SecondaryTabsItem
|
||||
href={`/console/project-${data.project.$id}/functions/function-${data.function.$id}/usage/90d`}
|
||||
disabled={$page.params.period === '90d'}>
|
||||
90d
|
||||
</DropListLink>
|
||||
</DropTabs>
|
||||
</SecondaryTabsItem>
|
||||
</SecondaryTabs>
|
||||
</div>
|
||||
{#if count}
|
||||
<Card>
|
||||
|
||||
@@ -32,8 +32,8 @@
|
||||
required />
|
||||
|
||||
<InputSelect
|
||||
label="Runtimes"
|
||||
id="runtimes"
|
||||
label="Runtime"
|
||||
id="runtime"
|
||||
placeholder="Select runtime"
|
||||
bind:value={$createFunction.runtime}
|
||||
{options}
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
<WizardStep>
|
||||
<svelte:fragment slot="title">Execute access</svelte:fragment>
|
||||
<svelte:fragment slot="subtitle">
|
||||
Choose who can execute this function using the client API. Check out our documentation for
|
||||
more on <a
|
||||
Choose who can execute this function using the client API. For more information, check out
|
||||
the <a
|
||||
href="https://appwrite.io/docs/permissions"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link">
|
||||
Permissions</a
|
||||
>.
|
||||
Permissions Guide
|
||||
</a>.
|
||||
</svelte:fragment>
|
||||
|
||||
<Roles bind:roles={$createFunction.execute} />
|
||||
|
||||
@@ -5,19 +5,19 @@
|
||||
</script>
|
||||
|
||||
<WizardStep>
|
||||
<svelte:fragment slot="title">Scheduling</svelte:fragment>
|
||||
<svelte:fragment slot="title">Schedule</svelte:fragment>
|
||||
<svelte:fragment slot="subtitle">
|
||||
Set a CRON schedule to trigger your function. Leave blank for no schedule. <a
|
||||
Set a Cron schedule to trigger your function. Leave blank for no schedule. <a
|
||||
href="https://appwrite.io/docs/functions#createFunction"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link">More details on CRON syntax here</a
|
||||
class="link">More details on Cron syntax here</a
|
||||
>.
|
||||
</svelte:fragment>
|
||||
<FormList>
|
||||
<InputCron
|
||||
bind:value={$createFunction.schedule}
|
||||
label="Schedule (CRON Syntax)"
|
||||
label="Schedule (Cron Syntax)"
|
||||
id="schedule" />
|
||||
</FormList>
|
||||
</WizardStep>
|
||||
|
||||
+6
-42
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { CardGrid, Heading, Secret } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button, Form, FormList, InputText, InputDateTime } from '$lib/elements/forms';
|
||||
import { Button, Form, FormList, InputText } from '$lib/elements/forms';
|
||||
import { symmetricDifference } from '$lib/helpers/array';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { Container } from '$lib/layout';
|
||||
@@ -10,21 +11,19 @@
|
||||
import { sdkForConsole } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { project } from '../../../store';
|
||||
import { key } from './store';
|
||||
import Scopes from '../scopes.svelte';
|
||||
import Delete from './delete.svelte';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { key } from './store';
|
||||
import UpdateExpirationDate from './updateExpirationDate.svelte';
|
||||
|
||||
let showDelete = false;
|
||||
let name: string = null;
|
||||
let secret: string = null;
|
||||
let expire: string = null;
|
||||
let scopes: string[] = null;
|
||||
|
||||
onMount(() => {
|
||||
name ??= $key.name;
|
||||
secret ??= $key.secret;
|
||||
expire ??= $key.expire;
|
||||
scopes ??= $key.scopes;
|
||||
});
|
||||
|
||||
@@ -51,29 +50,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function updateExpire() {
|
||||
try {
|
||||
await sdkForConsole.projects.updateKey(
|
||||
$project.$id,
|
||||
$key.$id,
|
||||
$key.name,
|
||||
$key.scopes,
|
||||
expire
|
||||
);
|
||||
invalidate(Dependencies.KEY);
|
||||
trackEvent('submit_key_update_expire');
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'API Key expiration has been updated'
|
||||
});
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function updateScopes() {
|
||||
try {
|
||||
await sdkForConsole.projects.updateKey(
|
||||
@@ -165,21 +141,9 @@
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
<Form on:submit={updateExpire}>
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Update Expiration Date</Heading>
|
||||
<p class="text">Choose any name that will help you distinguish between API keys.</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<FormList>
|
||||
<InputDateTime id="expire" label="Expiration Date" bind:value={expire} />
|
||||
</FormList>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={expire === $key.expire} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
<UpdateExpirationDate />
|
||||
|
||||
<CardGrid danger>
|
||||
<div>
|
||||
<Heading tag="h6" size="7">Delete API Key</Heading>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import CardGrid from '$lib/components/cardGrid.svelte';
|
||||
import Heading from '$lib/components/heading.svelte';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Form } from '$lib/elements/forms';
|
||||
import Button from '$lib/elements/forms/button.svelte';
|
||||
import FormList from '$lib/elements/forms/formList.svelte';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdkForConsole } from '$lib/stores/sdk';
|
||||
import { project } from '../../../store';
|
||||
import ExpirationInput from '../expirationInput.svelte';
|
||||
import { key } from './store';
|
||||
|
||||
let expiration = $key.expire;
|
||||
|
||||
async function updateExpire() {
|
||||
try {
|
||||
await sdkForConsole.projects.updateKey(
|
||||
$project.$id,
|
||||
$key.$id,
|
||||
$key.name,
|
||||
$key.scopes,
|
||||
expiration
|
||||
);
|
||||
invalidate(Dependencies.KEY);
|
||||
trackEvent('submit_key_update_expire');
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'API Key expiration has been updated'
|
||||
});
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Form on:submit={updateExpire}>
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Update Expiration Date</Heading>
|
||||
<p class="text">Set a date after which your API Key will expire.</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<FormList>
|
||||
<ExpirationInput bind:value={expiration} />
|
||||
</FormList>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={expiration === $key.expire} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
@@ -0,0 +1,63 @@
|
||||
<script lang="ts">
|
||||
import InputDateTime from '$lib/elements/forms/inputDateTime.svelte';
|
||||
import InputSelect from '$lib/elements/forms/inputSelect.svelte';
|
||||
|
||||
function incrementToday(value: number, type: 'day' | 'month' | 'year'): string {
|
||||
const date = new Date();
|
||||
switch (type) {
|
||||
case 'day':
|
||||
date.setDate(date.getDate() + value);
|
||||
break;
|
||||
case 'month':
|
||||
date.setMonth(date.getMonth() + value);
|
||||
break;
|
||||
case 'year':
|
||||
date.setMonth(date.getMonth() + value * 12);
|
||||
break;
|
||||
}
|
||||
|
||||
return date.toISOString();
|
||||
}
|
||||
|
||||
export let value: string | null = null;
|
||||
let expirationSelect = value === null ? null : 'custom';
|
||||
let expirationCustom: string | null = value ?? null;
|
||||
|
||||
$: {
|
||||
value = expirationSelect === 'custom' ? expirationCustom : expirationSelect;
|
||||
}
|
||||
</script>
|
||||
|
||||
<InputSelect
|
||||
bind:value={expirationSelect}
|
||||
options={[
|
||||
{
|
||||
label: 'Never',
|
||||
value: null
|
||||
},
|
||||
{
|
||||
label: '7 Days',
|
||||
value: incrementToday(7, 'day')
|
||||
},
|
||||
{
|
||||
label: '30 days',
|
||||
value: incrementToday(30, 'day')
|
||||
},
|
||||
{
|
||||
label: '90 days',
|
||||
value: incrementToday(90, 'day')
|
||||
},
|
||||
{
|
||||
label: '1 Year',
|
||||
value: incrementToday(1, 'year')
|
||||
},
|
||||
{
|
||||
label: 'Custom Date',
|
||||
value: 'custom'
|
||||
}
|
||||
]}
|
||||
id="preset"
|
||||
label="Expiration Date" />
|
||||
{#if expirationSelect === 'custom'}
|
||||
<InputDateTime id="expire" label="" bind:value={expirationCustom} showLabel={false} />
|
||||
{/if}
|
||||
@@ -1,45 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { FormList, InputText, InputDateTime, InputSelect } from '$lib/elements/forms';
|
||||
import { FormList, InputText } from '$lib/elements/forms';
|
||||
import { WizardStep } from '$lib/layout';
|
||||
import ExpirationInput from '../expirationInput.svelte';
|
||||
import { key } from './store';
|
||||
|
||||
let preset = 'never';
|
||||
function incrementToday(value: number, type: 'day' | 'month' | 'year'): string {
|
||||
const date = new Date();
|
||||
switch (type) {
|
||||
case 'day':
|
||||
date.setDate(date.getDate() + value);
|
||||
break;
|
||||
case 'month':
|
||||
date.setMonth(date.getMonth() + value);
|
||||
break;
|
||||
case 'year':
|
||||
date.setMonth(date.getMonth() + value * 12);
|
||||
break;
|
||||
}
|
||||
|
||||
return date.toISOString();
|
||||
}
|
||||
|
||||
$: {
|
||||
switch (preset) {
|
||||
case 'never':
|
||||
$key.expire = null;
|
||||
break;
|
||||
case '1day':
|
||||
$key.expire = incrementToday(1, 'day');
|
||||
break;
|
||||
case '7days':
|
||||
$key.expire = incrementToday(7, 'day');
|
||||
break;
|
||||
case '1month':
|
||||
$key.expire = incrementToday(1, 'month');
|
||||
break;
|
||||
case '1year':
|
||||
$key.expire = incrementToday(1, 'year');
|
||||
break;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<WizardStep>
|
||||
@@ -52,38 +15,6 @@
|
||||
placeholder="API Key Name"
|
||||
required
|
||||
bind:value={$key.name} />
|
||||
<InputSelect
|
||||
bind:value={preset}
|
||||
options={[
|
||||
{
|
||||
label: 'Never',
|
||||
value: 'never'
|
||||
},
|
||||
{
|
||||
label: '1 Day',
|
||||
value: '1day'
|
||||
},
|
||||
{
|
||||
label: '7 Days',
|
||||
value: '7days'
|
||||
},
|
||||
{
|
||||
label: '1 Month',
|
||||
value: '1month'
|
||||
},
|
||||
{
|
||||
label: '1 Year',
|
||||
value: '1year'
|
||||
},
|
||||
{
|
||||
label: 'Custom Date',
|
||||
value: 'custom'
|
||||
}
|
||||
]}
|
||||
id="preset"
|
||||
label="Expiration Date" />
|
||||
{#if preset === 'custom'}
|
||||
<InputDateTime id="expire" label="" bind:value={$key.expire} showLabel={false} />
|
||||
{/if}
|
||||
<ExpirationInput bind:value={$key.expire} />
|
||||
</FormList>
|
||||
</WizardStep>
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
import { page } from '$app/stores';
|
||||
import { uploader } from '$lib/stores/uploader';
|
||||
import { bucket } from './store';
|
||||
import { calculateSize } from '$lib/helpers/sizeConvertion';
|
||||
import { Permissions } from '$lib/components/permissions';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
@@ -50,8 +49,10 @@
|
||||
<svelte:fragment slot="header">Create File</svelte:fragment>
|
||||
<FormList>
|
||||
<div>
|
||||
<InputFile bind:files allowedFileExtensions={$bucket.allowedFileExtensions} />
|
||||
<p>Max file size: {calculateSize($bucket.maximumFileSize)}</p>
|
||||
<InputFile
|
||||
bind:files
|
||||
allowedFileExtensions={$bucket.allowedFileExtensions}
|
||||
maxSize={$bucket.maximumFileSize} />
|
||||
</div>
|
||||
|
||||
{#if !showCustomId}
|
||||
@@ -72,7 +73,9 @@
|
||||
href="https://appwrite.io/docs/permissions"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link">Permissions Guide</a> in our documentation.
|
||||
class="link">
|
||||
Permissions Guide
|
||||
</a>.
|
||||
</p>
|
||||
{#if $bucket.fileSecurity}
|
||||
<div class="common-section">
|
||||
|
||||
+1
-4
@@ -152,10 +152,7 @@
|
||||
|
||||
<CardGrid danger>
|
||||
<Heading tag="h6" size="7">Delete File</Heading>
|
||||
<p>
|
||||
The file will be permanently deleted, including all the files within it. This action
|
||||
is irreversible.
|
||||
</p>
|
||||
<p>The file will be permanently deleted. This action is irreversible.</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<Box>
|
||||
<svelte:fragment slot="title">
|
||||
|
||||
@@ -295,7 +295,9 @@
|
||||
href="https://appwrite.io/docs/permissions"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link">Permissions Guide</a> in our documentation.
|
||||
class="link">
|
||||
Permissions Guide
|
||||
</a>.
|
||||
</p>
|
||||
<svelte:fragment slot="aside">
|
||||
{#if bucketPermissions}
|
||||
@@ -349,7 +351,7 @@
|
||||
<label class="choice-item" for="encryption">
|
||||
<div class="input-text-wrapper">
|
||||
<input
|
||||
label="Encryption"
|
||||
name="encryption"
|
||||
id="encryption"
|
||||
type="checkbox"
|
||||
class="switch"
|
||||
@@ -372,7 +374,7 @@
|
||||
<label class="choice-item" for="antivirus">
|
||||
<div class="input-text-wrapper">
|
||||
<input
|
||||
label="Antivirus"
|
||||
name="antivirus"
|
||||
id="antivirus"
|
||||
type="checkbox"
|
||||
class="switch"
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { Alert } from '$lib/components';
|
||||
|
||||
let teamId: string, membershipId: string, userId: string, secret: string;
|
||||
let terms = false;
|
||||
@@ -42,10 +43,22 @@
|
||||
</svelte:head>
|
||||
|
||||
<Unauthenticated>
|
||||
<svelte:fragment slot="title">Invite</svelte:fragment>
|
||||
<svelte:fragment slot="title">
|
||||
{#if !userId || !secret || !membershipId || !teamId}
|
||||
Invalid invite
|
||||
{:else}
|
||||
Invite
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment>
|
||||
{#if !userId || !secret || !membershipId || !teamId}
|
||||
<p class="text">Invalid invite link.</p>
|
||||
<Alert type="warning">
|
||||
<svelte:fragment slot="title">The invite link is not valid</svelte:fragment>
|
||||
Please ask the project owner to send you a new invite.
|
||||
</Alert>
|
||||
<div class="u-flex u-main-end u-margin-block-start-40 ">
|
||||
<Button href={`${base}/register`}>Sign up to Appwrite</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text">You have been invited to join a team project on Appwrite</p>
|
||||
<Form on:submit={acceptInvite}>
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import { vi } from 'vitest';
|
||||
import { render, fireEvent } from '@testing-library/svelte';
|
||||
import { DropTabsItem } from '../../../src/lib/components';
|
||||
import { SecondaryTabsItem } from '../../../src/lib/components';
|
||||
|
||||
test('shows button', () => {
|
||||
const { getByRole } = render(DropTabsItem);
|
||||
const { getByRole } = render(SecondaryTabsItem);
|
||||
|
||||
expect(getByRole('button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('shows link', () => {
|
||||
render(SecondaryTabsItem, { href: 'https://www.appwrite.io' });
|
||||
const href = document.querySelector('a');
|
||||
expect(href).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('shows button - disabled', () => {
|
||||
const { getByRole } = render(DropTabsItem, { disabled: true });
|
||||
const { getByRole } = render(SecondaryTabsItem, { href: null, disabled: true });
|
||||
|
||||
expect(getByRole('button')).toBeDisabled();
|
||||
});
|
||||
|
||||
test('shows button - on:click', async () => {
|
||||
const { getByRole, component } = render(DropTabsItem);
|
||||
const { getByRole, component } = render(SecondaryTabsItem);
|
||||
const button = getByRole('button');
|
||||
|
||||
const callback = vi.fn();
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import { render } from '@testing-library/svelte';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { InputId } from '../../../src/lib/elements/forms';
|
||||
|
||||
test('shows id input', () => {
|
||||
const { getByPlaceholderText } = render(InputId);
|
||||
const input = getByPlaceholderText('Enter ID');
|
||||
|
||||
expect(input).toBeInTheDocument();
|
||||
expect(input).toHaveAttribute('type', 'text');
|
||||
});
|
||||
|
||||
test('state', async () => {
|
||||
const { component, getByPlaceholderText } = render(InputId, { value: '' });
|
||||
const input = getByPlaceholderText('Enter ID');
|
||||
|
||||
console.log(component);
|
||||
expect(component.value).toEqual('');
|
||||
await userEvent.type(input, 'lorem');
|
||||
expect(component.value).toEqual('lorem');
|
||||
});
|
||||
@@ -59,7 +59,7 @@ test('shows next step with submit', async () => {
|
||||
expect(step1Required).toBeInTheDocument();
|
||||
expect(step1Optional).toBeInTheDocument();
|
||||
|
||||
form.submit();
|
||||
form.dispatchEvent(new Event('submit'));
|
||||
await tick();
|
||||
|
||||
expect(step1).not.toBeInTheDocument();
|
||||
@@ -72,7 +72,7 @@ test('shows next step with submit', async () => {
|
||||
expect(step2First).toBeInTheDocument();
|
||||
expect(step2Second).toBeInTheDocument();
|
||||
|
||||
form.submit();
|
||||
form.dispatchEvent(new Event('submit'));
|
||||
await tick();
|
||||
|
||||
expect(form).not.toBeInTheDocument();
|
||||
@@ -96,7 +96,8 @@ test('intercepts submit', async () => {
|
||||
expect(step1Optional).toBeInTheDocument();
|
||||
|
||||
await userEvent.type(step1Required, 'fail');
|
||||
form.submit();
|
||||
|
||||
form.dispatchEvent(new Event('submit'));
|
||||
await tick();
|
||||
|
||||
expect(step1).toBeInTheDocument();
|
||||
@@ -104,7 +105,7 @@ test('intercepts submit', async () => {
|
||||
expect(step1Optional).toBeInTheDocument();
|
||||
|
||||
await userEvent.type(step1Required, 'works');
|
||||
form.submit();
|
||||
form.dispatchEvent(new Event('submit'));
|
||||
await tick();
|
||||
|
||||
expect(step1).not.toBeInTheDocument();
|
||||
@@ -117,7 +118,7 @@ test('intercepts submit', async () => {
|
||||
expect(step2First).toBeInTheDocument();
|
||||
expect(step2Second).toBeInTheDocument();
|
||||
|
||||
form.submit();
|
||||
form.dispatchEvent(new Event('submit'));
|
||||
await tick();
|
||||
|
||||
expect(form).not.toBeInTheDocument();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { vi } from 'vitest';
|
||||
import * as svelteinternal from 'svelte/internal';
|
||||
|
||||
beforeAll(() => {
|
||||
vi.mock('$app/environment', () => ({
|
||||
@@ -10,4 +11,5 @@ beforeAll(() => {
|
||||
vi.mock('$app/navigation', () => ({
|
||||
goto: vi.fn()
|
||||
}));
|
||||
vi.mock('svelte', () => svelteinternal);
|
||||
});
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ const config = {
|
||||
include: ['echarts', 'prismjs']
|
||||
},
|
||||
ssr: {
|
||||
noExternal: ['echarts', 'prismjs']
|
||||
noExternal: ['echarts', 'prismjs', '@analytics/google-analytics', 'analytics']
|
||||
},
|
||||
define: {
|
||||
'import.meta.env.VERCEL_ANALYTICS_ID': JSON.stringify(process.env.VERCEL_ANALYTICS_ID)
|
||||
|
||||
Reference in New Issue
Block a user