Compare commits

...

38 Commits

Author SHA1 Message Date
Ruben Fiszel
ec528fce67 chore(main): release 1.7.0 (#45)
* chore(main): release 1.7.0

* Apply automatic changes

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2022-05-14 14:58:31 +02:00
Tomasz Wsuł
5b413d7e04 feat: self host github oauth (#46) 2022-05-14 14:54:53 +02:00
Ruben Fiszel
02c8bea084 fix: better error message when saving script 2022-05-11 13:29:21 +02:00
Ruben Fiszel
bb31c80378 fix README docker-compose reference 2022-05-11 13:05:22 +02:00
Ruben Fiszel
91045e73cc BUG_ISSUE instructions 2022-05-11 08:10:51 +02:00
dependabot[bot]
9219b651a3 chore(deps-dev): bump @sveltejs/kit in /frontend (#25)
Bumps [@sveltejs/kit](https://github.com/sveltejs/kit/tree/HEAD/packages/kit) from 1.0.0-next.324 to 1.0.0-next.326.
- [Release notes](https://github.com/sveltejs/kit/releases)
- [Changelog](https://github.com/sveltejs/kit/blob/master/packages/kit/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/kit@1.0.0-next.326/packages/kit)

---
updated-dependencies:
- dependency-name: "@sveltejs/kit"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2022-05-11 01:27:27 +02:00
Ruben Fiszel
7f21d03d00 chore(main): release 1.6.1 (#34)
* chore(main): release 1.6.1

* Apply automatic changes

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2022-05-10 21:38:59 +02:00
dependabot[bot]
a62e6e5ee3 chore(deps): bump serde_json from 1.0.79 to 1.0.81 in /backend (#26)
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.79 to 1.0.81.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.79...v1.0.81)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2022-05-10 21:32:22 +02:00
Ruben Fiszel
2c28031e44 fix: also store and display "started at" for completed jobs (#33) 2022-05-10 21:32:07 +02:00
Ruben Fiszel
ca8de69126 run prettier 2022-05-10 21:29:54 +02:00
dependabot[bot]
98071bd68b chore(deps): bump tower-http from 0.2.5 to 0.3.3 in /backend (#27)
Bumps [tower-http](https://github.com/tower-rs/tower-http) from 0.2.5 to 0.3.3.
- [Release notes](https://github.com/tower-rs/tower-http/releases)
- [Commits](https://github.com/tower-rs/tower-http/compare/tower-http-0.2.5...tower-http-0.3.3)

---
updated-dependencies:
- dependency-name: tower-http
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2022-05-10 21:18:04 +02:00
dependabot[bot]
128dde4fb3 chore(deps): bump thiserror from 1.0.30 to 1.0.31 in /backend (#30)
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.30 to 1.0.31.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.30...1.0.31)

---
updated-dependencies:
- dependency-name: thiserror
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2022-05-10 21:07:45 +02:00
dependabot[bot]
f090945b27 chore(deps): bump serde from 1.0.136 to 1.0.137 in /backend (#32) 2022-05-10 21:07:29 +02:00
dependabot[bot]
60729d80b9 chore(deps): bump mhart/alpine-node from 14 to 16 (#21)
Bumps mhart/alpine-node from 14 to 16.

---
updated-dependencies:
- dependency-name: mhart/alpine-node
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2022-05-10 17:28:49 +02:00
Ruben Fiszel
e228beec2a ci: push to private registry builded image no matter what 2022-05-10 17:15:16 +02:00
dependabot[bot]
4dbf562fb7 chore(deps): bump GoogleCloudPlatform/release-please-action from 2 to 3 (#20) 2022-05-10 14:41:11 +02:00
dependabot[bot]
4952290296 chore(deps): bump actions/checkout from 2 to 3 (#19)
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-10 14:24:17 +02:00
Juan Calderon-Perez
f53eb71e4a ci: add support for dependabot (#9)
* Add support for dependabot

* Add dependabot support for Python clients

* move to a weekly schedule

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2022-05-10 12:14:38 +00:00
Ruben Fiszel
96f54f5f44 chore: release 1.6.0 (#6)
* Apply automatic changes

* Update version.txt

* Apply automatic changes

* Update CHANGELOG.md

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2022-05-10 12:48:04 +02:00
Ruben Fiszel
0863e12e6a ci: add codeowners 2022-05-10 09:41:44 +02:00
Ruben Fiszel
d03266b0a4 ci: add CLA 2022-05-10 09:12:24 +02:00
Ruben Fiszel
4a4eaa90e2 ci: add CLA 2022-05-10 09:02:28 +02:00
Ruben Fiszel
5e7c14b722 ci: add CLA 2022-05-10 08:52:11 +02:00
Ruben Fiszel
55b5695673 fix: display more than default 30 workspaces as superadmin 2022-05-09 15:18:28 +02:00
Ruben Fiszel
8596ac50b9 delete starter script without lock files 2022-05-08 17:56:16 +02:00
Ruben Fiszel
13fb52117b feat: self host minimal 2 2022-05-08 17:51:33 +02:00
Ruben Fiszel
2c70a15594 feat: self host minimal 2022-05-08 17:26:51 +02:00
Ruben Fiszel
7a51f842f0 feat: superadmin settings 2022-05-08 17:03:13 +02:00
Ruben Fiszel
a130806e19 feat: user settings is now at workspace level 2022-05-08 12:58:58 +02:00
Ruben Fiszel
fd1f05dd16 ci: refactor + dockerhub 2022-05-08 11:57:37 +02:00
Ruben Fiszel
48e51733e0 docs: add main ci badge 2022-05-06 14:59:42 +02:00
Ruben Fiszel
e7817e6c9f alpha.windmill -> app.windmill 2022-05-06 13:55:14 +02:00
Ruben Fiszel
51ad6edfcb docs: typos 2022-05-05 15:59:59 +02:00
Ruben Fiszel
315f7edd64 docs: windmill imgs 2022-05-05 15:53:40 +02:00
Ruben Fiszel
a2c3deab74 docs: README general idea 2022-05-05 15:24:35 +02:00
Ruben Fiszel
891b7eb93a docs: architecture diagram 2022-05-05 13:22:13 +02:00
Ruben Fiszel
7efd87be79 docs: architecture diagram 2022-05-05 13:20:42 +02:00
Ruben Fiszel
5acbc8b48c Create FUNDING.yml 2022-05-05 10:50:54 +02:00
161 changed files with 3360 additions and 3032 deletions

5
.env
View File

@@ -1,3 +1,6 @@
SITE_URL=localhost
DB_PASSWORD=changeme
POSTGRES_VERSION=13.3.0
# GitHub OAuth- https://docs.github.com/en/developers/apps/building-oauth-apps/creating-an-oauth-app
GITHUB_OAUTH_CLIENT_ID=yours_client_id
GITHUB_OAUTH_CLIENT_SECRET=yours_client_sected

1
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1 @@
* @rubenfiszel

3
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,3 @@
# These are supported funding model platforms
github: [rubenfiszel]

View File

@@ -1,38 +1,27 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
title: 'bug:'
labels: 'bug'
assignees: 'rubenfiszel'
---
**Describe the bug**
A clear and concise description of what the bug is.
**Describe the bug** A clear and concise description of what the bug is.
**To Reproduce** Steps to reproduce the behavior:
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Expected behavior** A clear and concise description of what you expected to
happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Screenshots** If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Windmill version** Go on the left menu -> <user> -> User Settings and copy the
printed version in "Running windmill version (backend): XXX".
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
**Additional context** Add any other context about the problem here.

View File

@@ -0,0 +1,8 @@
---
name: Feature Request
about: Create a feature request
title: 'feature: '
labels: 'feature'
assignees: 'rubenfiszel'
---

39
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,39 @@
# Basic set up for three package managers
version: 2
updates:
# Maintain dependencies for GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
# Maintain dependencies for npm
- package-ecosystem: "npm"
directory: "/frontend"
schedule:
interval: "weekly"
# Maintain dependencies for cargo
- package-ecosystem: "cargo"
directory: "/backend"
schedule:
interval: "weekly"
# Maintain dependencies for Docker
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
# Maintain dependencies for wmill python client
- package-ecosystem: "pip"
directory: "/python-client/wmill"
schedule:
interval: "weekly"
# Maintain dependencies for wmill_pg python client
- package-ecosystem: "pip"
directory: "/python-client/wmill_pg"
schedule:
interval: "weekly"

View File

@@ -8,7 +8,7 @@ jobs:
change_version:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Change versions
run: ./.github/change-versions.sh "$(cat version.txt)"
- uses: stefanzweifel/git-auto-commit-action@v4

View File

@@ -3,6 +3,8 @@ name: Deploy to windmill.dev
on:
push:
branches: [main]
paths:
- "community/*"
jobs:
deploy:

View File

@@ -1,7 +1,13 @@
name: Docker Image CI
env:
LOCAL_REGISTRY: registry.wimill.xyz
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
name: Build and push docker image
on:
push:
branches: [main]
tags: ["*"]
pull_request:
types: [opened, synchronize, reopened]
@@ -12,28 +18,66 @@ concurrency:
jobs:
build:
runs-on: [self-hosted, new]
env:
DOCKER_BUILDKIT: 1
steps:
- name: Wait for release to succeed
if: github.ref == 'refs/heads/main'
uses: lewagon/wait-on-check-action@v1.0.0
with:
ref: ${{ github.ref }}
check-name: "Release please"
repo-token: ${{ secrets.GITHUB_TOKEN }}
wait-interval: 10
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: deploy staging stack
run: |
docker build . --cache-from "registry.wimill.xyz/windmill:staging" -t "registry.wimill.xyz/windmill:staging" --build-arg BUILDKIT_INLINE_CACHE=1
docker push "registry.wimill.xyz/windmill:staging"
- name: deploy demo stack
if: github.ref == 'refs/heads/main'
run: |
docker tag registry.wimill.xyz/windmill:staging registry.wimill.xyz/windmill:main
docker push registry.wimill.xyz/windmill:main
# - name: pruning unused images
# run: sudo docker image prune -a
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Docker meta local
id: metalocal
uses: docker/metadata-action@v4
with:
images: ${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Build and push privately
uses: docker/build-push-action@v3
if: github.event_name == 'pull_request'
with:
context: .
push: true
tags: |
${{ steps.metalocal.outputs.tags }}
labels: ${{ steps.metalocal.outputs.labels }}
cache-from: type=registry,ref=registry.wimill.xyz/windmilllabs/windmill:buildcache
cache-to: type=registry,ref=registry.wimill.xyz/windmilllabs/windmill:buildcache,mode=max
- name: Docker meta
if: github.event_name != 'pull_request'
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Login to registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push publically
uses: docker/build-push-action@v3
if: github.event_name != 'pull_request'
with:
context: .
push: true
tags: |
${{ steps.metalocal.outputs.tags }}
${{ steps.meta.outputs.tags }}
labels: ${{ steps.metalocal.outputs.labels }}
cache-from: type=registry,ref=registry.wimill.xyz/windmilllabs/windmill:buildcache
cache-to: type=registry,ref=registry.wimill.xyz/windmilllabs/windmill:buildcache,mode=max

37
.github/workflows/lsp_on_release.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: Publish LSP Server
on:
push:
branches: [main]
paths:
- "python-client/*W"
- "lsp/*"
tags:
- "*"
jobs:
build:
runs-on: [self-hosted, new]
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Docker meta local
id: metalocal
uses: docker/metadata-action@v4
with:
images: registry.wimill.xyz/windmilllabs/lsp
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Build and push
uses: docker/build-push-action@v3
with:
context: "{{defaultContext}}:lsp"
push: true
tags: ${{ steps.metalocal.outputs.tags }}
labels: ${{ steps.metalocal.outputs.labels }}
cache-from: type=registry,ref=registry.wimill.xyz/windmilllabs/lsp:buildcache
cache-to: type=registry,ref=registry.wimill.xyz/windmilllabs/lsp:buildcache,mode=max

View File

@@ -1,38 +0,0 @@
name: Build LSP Docker
on:
push:
branches: [main]
paths:
- "python-client/**"
- "Pipfile"
- ".github/workflows/on-release.yml"
jobs:
build_lsp:
runs-on: [self-hosted, new]
steps:
- name: Wait for release to succeed
if: github.ref == 'refs/heads/main'
uses: lewagon/wait-on-check-action@v1.0.0
with:
ref: ${{ github.ref }}
check-name: "Release please"
repo-token: ${{ secrets.GITHUB_TOKEN }}
wait-interval: 10
- uses: actions/checkout@v2
- name: Upload python client
env:
PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: |
cd python-client
export PATH=$PATH:/usr/local/bin
export PATH=$PATH:/root/.local/bin
./publish.sh
- name: Build the Docker image
run: |
cd lsp
sudo docker pull "registry.wimill.xyz/lsp:main" || true
sudo docker build . --cache-from "registry.wimill.xyz/lsp:main" -t "registry.wimill.xyz/lsp:main" --build-arg BUILDKIT_INLINE_CACHE=1
- name: push to registry
run: |
sudo docker push "registry.wimill.xyz/lsp:main"

19
.github/workflows/pypi_on_release.yml vendored Normal file
View File

@@ -0,0 +1,19 @@
name: Publish python-client
on:
push:
tags:
- "v*"
jobs:
build_lsp:
runs-on: [self-hosted, new]
steps:
- uses: actions/checkout@v3
- name: Upload python client
env:
PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: |
cd python-client
export PATH=$PATH:/usr/local/bin
export PATH=$PATH:/root/.local/bin
./publish.sh

View File

@@ -1,14 +1,14 @@
on:
push:
branches:
- main
branches: [main]
name: release-please
jobs:
release-please:
name: "Release please"
runs-on: ubuntu-latest
steps:
- uses: GoogleCloudPlatform/release-please-action@v2
- uses: GoogleCloudPlatform/release-please-action@v3
with:
release-type: simple
package-name: windmill

34
.github/workflows/sign-cla.yml vendored Normal file
View File

@@ -0,0 +1,34 @@
name: "CLA Assistant"
on:
issue_comment:
types: [created]
pull_request_target:
types: [opened, closed, synchronize]
jobs:
CLAssistant:
runs-on: ubuntu-latest
steps:
- name: "CLA Assistant"
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
# Beta Release
uses: cla-assistant/github-action@v2.1.3-beta
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PERSONAL_ACCESS_TOKEN: ${{ secrets.CLA_PAT }}
with:
path-to-signatures: "signatures/cla.json"
path-to-document: "https://github.com/windmill-labs/windmill/blob/master/CLA.md"
branch: "signatures"
allowlist: rubenfiszel,bot*
#below are the optional inputs - If the optional inputs are not given, then default values will be taken
#remote-organization-name: enter the remote organization name where the signatures should be stored (Default is storing the signatures in the same repository)
#remote-repository-name: enter the remote repository name where the signatures should be stored (Default is storing the signatures in the same repository)
#create-file-commit-message: 'For example: Creating file for storing CLA Signatures'
#signed-commit-message: 'For example: $contributorName has signed the CLA in #$pullRequestNo'
#custom-notsigned-prcomment: 'pull request comment with Introductory message to ask new contributors to sign'
#custom-pr-sign-comment: 'The signature to be committed in order to sign the CLA'
#custom-allsigned-prcomment: 'pull request comment when all contributors has signed, defaults to **CLA Assistant Lite bot** All Contributors have signed the CLA.'
#lock-pullrequest-aftermerge: false - if you don't want this bot to automatically lock the pull request after merging (default - true)
#use-dco-flag: true - If you are using DCO instead of CLA

View File

@@ -1,3 +1,34 @@
# Changelog
## [1.7.0](https://github.com/windmill-labs/windmill/compare/v1.6.1...v1.7.0) (2022-05-14)
### Features
* self host github oauth ([#46](https://github.com/windmill-labs/windmill/issues/46)) ([5b413d7](https://github.com/windmill-labs/windmill/commit/5b413d7e045d09dc5c5916cb22d82438ec6c92ad))
### Bug Fixes
* better error message when saving script ([02c8bea](https://github.com/windmill-labs/windmill/commit/02c8bea0840e492c31ccb8ddd1e5ae9676a534b1))
### [1.6.1](https://github.com/windmill-labs/windmill/compare/v1.6.0...v1.6.1) (2022-05-10)
### Bug Fixes
* also store and display "started at" for completed jobs ([#33](https://github.com/windmill-labs/windmill/issues/33)) ([2c28031](https://github.com/windmill-labs/windmill/commit/2c28031e44453740ad8c4b7e3c248173eab34b9c))
## 1.6.0 (2022-05-10)
### Features
* superadmin settings ([7a51f84](https://www.github.com/windmill-labs/windmill/commit/7a51f842f01e17c4d230c060fa0de558553ad3ed))
* user settings is now at workspace level ([a130806](https://www.github.com/windmill-labs/windmill/commit/a130806e1929267ee40ca443e3dac6e1a5d80da3))
### Bug Fixes
* display more than default 30 workspaces as superadmin ([55b5695](https://www.github.com/windmill-labs/windmill/commit/55b5695673912ffe040d3011c020b1002b4e3268))
## [1.5.0](https://www.github.com/windmill-labs/windmill/v1.5.0) (2022-05-02)

145
CLA.md Normal file
View File

@@ -0,0 +1,145 @@
## Contributor Agreement
## Individual Contributor Non-Exclusive License Agreement
Thank you for your interest in contributing to Ruben Fiszel's Windmill ("We" or
"Us").
The purpose of this contributor agreement ("Agreement") is to clarify and
document the rights granted by contributors to Us.
### 1\. Definitions
**"You"** means the individual Copyright owner who Submits a Contribution to Us.
**"Legal Entity"** means an entity that is not a natural person.
**"Affiliate"** means any other Legal Entity that controls, is controlled by, or
under common control with that Legal Entity. For the purposes of this
definition, "control" means (i) the power, direct or indirect, to cause the
direction or management of such Legal Entity, whether by contract or otherwise,
(ii) ownership of fifty percent (50%) or more of the outstanding shares or
securities that vote to elect the management or other persons who direct such
Legal Entity or (iii) beneficial ownership of such entity.
**"Contribution"** means any original work of authorship, including any original
modifications or additions to an existing work of authorship, Submitted by You
to Us, in which You own the Copyright.
**"Copyright"** means all rights protecting works of authorship, including
copyright, moral and neighboring rights, as appropriate, for the full term of
their existence.
**"Material"** means the software or documentation made available by Us to third
parties.
**"Submit"** means any act by which a Contribution is transferred to Us by You
by means of tangible or intangible media, including but not limited to
electronic mailing lists, source code control systems, and issue tracking
systems that are managed by, or on behalf of, Us, but excluding any transfer
that is conspicuously marked or otherwise designated in writing by You as "Not a
Contribution."
**"Documentation"** means any non-software portion of a Contribution.
### 2\. License grant
#### 2.1 Copyright license to Us
Subject to the terms and conditions of this Agreement, You hereby grant to Us a
worldwide, royalty-free, NON-exclusive, perpetual and irrevocable (except as
stated in Section 8.2) license, with the right to transfer an unlimited number
of non-exclusive licenses or to grant sublicenses to third parties, under the
Copyright covering the Contribution to use the Contribution by all means,
including, but not limited to:
- publish the Contribution,
- modify the Contribution,
- prepare derivative works based upon or containing the Contribution and/or to
combine the Contribution with other Materials,
- reproduce the Contribution in original or modified form,
- distribute, to make the Contribution available to the public, display and
publicly perform the Contribution in original or modified form.
#### 2.2 Moral rights
Moral Rights remain unaffected to the extent they are recognized and not
waivable by applicable law. Notwithstanding, You may add your name to the
attribution mechanism customary used in the Materials you Contribute to, such as
the header of the source code files of Your Contribution, and We will respect
this attribution when using Your Contribution.
### 3\. Patents
#### 3.1 Patent license
Subject to the terms and conditions of this Agreement You hereby grant to Us and
to recipients of Materials distributed by Us a worldwide, royalty-free,
non-exclusive, perpetual and irrevocable (except as stated in Section 3.2)
patent license, with the right to transfer an unlimited number of non-exclusive
licenses or to grant sublicenses to third parties, to make, have made, use,
sell, offer for sale, import and otherwise transfer the Contribution and the
Contribution in combination with any Material (and portions of such
combination). This license applies to all patents owned or controlled by You,
whether already acquired or hereafter acquired, that would be infringed by
making, having made, using, selling, offering for sale, importing or otherwise
transferring of Your Contribution(s) alone or by combination of Your
Contribution(s) with any Material.
### 4. Disclaimer
THE CONTRIBUTION IS PROVIDED "AS IS". MORE PARTICULARLY, ALL EXPRESS OR IMPLIED
WARRANTIES INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTY OF SATISFACTORY
QUALITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT ARE EXPRESSLY
DISCLAIMED BY YOU TO US AND BY US TO YOU. TO THE EXTENT THAT ANY SUCH WARRANTIES
CANNOT BE DISCLAIMED, SUCH WARRANTY IS LIMITED IN DURATION AND EXTENT TO THE
MINIMUM PERIOD AND EXTENT PERMITTED BY LAW.
### 5. Consequential damage waiver
TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT WILL YOU OR WE BE
LIABLE FOR ANY LOSS OF PROFITS, LOSS OF ANTICIPATED SAVINGS, LOSS OF DATA,
INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL AND EXEMPLARY DAMAGES ARISING OUT
OF THIS AGREEMENT REGARDLESS OF THE LEGAL OR EQUITABLE THEORY (CONTRACT, TORT OR
OTHERWISE) UPON WHICH THE CLAIM IS BASED.
### 6. Approximation of disclaimer and damage waiver
IF THE DISCLAIMER AND DAMAGE WAIVER MENTIONED IN SECTION 4. AND SECTION 5.
CANNOT BE GIVEN LEGAL EFFECT UNDER APPLICABLE LOCAL LAW, REVIEWING COURTS SHALL
APPLY LOCAL LAW THAT MOST CLOSELY APPROXIMATES AN ABSOLUTE WAIVER OF ALL CIVIL
OR CONTRACTUAL LIABILITY IN CONNECTION WITH THE CONTRIBUTION.
### 7. Term
7.1 This Agreement shall come into effect upon Your acceptance of the terms and
conditions.
7.3 In the event of a termination of this Agreement Sections 4, 5, 6, 7 and 8
shall survive such termination and shall remain in full force thereafter. For
the avoidance of doubt, Free and Open Source Software (sub)licenses that have
already been granted for Contributions at the date of the termination shall
remain in full force after the termination of this Agreement.
### 8 Miscellaneous
8.1 This Agreement and all disputes, claims, actions, suits or other proceedings
arising out of this agreement or relating in any way to it shall be governed by
the laws of France excluding its private international law provisions.
8.2 This Agreement sets out the entire agreement between You and Us for Your
Contributions to Us and overrides all other agreements or understandings.
8.3 In case of Your death, this agreement shall continue with Your heirs. In
case of more than one heir, all heirs must exercise their rights through a
commonly authorized person.
8.4 If any provision of this Agreement is found void and unenforceable, such
provision will be replaced to the extent possible with a provision that comes
closest to the meaning of the original provision and that is enforceable. The
terms and conditions set forth in this Agreement shall apply notwithstanding any
failure of essential purpose of this Agreement or any limited remedy to the
maximum extent possible under law.
8.5 You agree to notify Us of any facts or circumstances of which you become
aware that would make this Agreement inaccurate in any respect.

View File

@@ -1,4 +1,4 @@
{$SITE_URL} {
bind {$ADDRESS}
reverse_proxy /* server:8000
reverse_proxy /* windmill:8000
}

View File

@@ -19,7 +19,7 @@ RUN git clone -b master --single-branch https://github.com/google/nsjail.git . \
&& git checkout dccf911fd2659e7b08ce9507c25b2b38ec2c5800
RUN make
FROM mhart/alpine-node:14 as frontend
FROM mhart/alpine-node:16 as frontend
# install dependencies
WORKDIR /frontend

View File

@@ -1,10 +1,13 @@
<p align="center">
<a href="https://alpha.windmill.dev"><img src="./windmill.svg" alt="windmill.dev"></a>
<a href="https://app.windmill.dev"><img src="./imgs/windmill.svg" alt="windmill.dev"></a>
</p>
<p align="center">
<em>Windmill.dev is an OSS developer platform to quickly build production-grade multi-steps automations and internal apps from minimal Python and Typescript scripts.</em>
</p>
<p align="center">
<a href="https://github.com/windmill-labs/windmill/actions/workflows/docker-image.yml" target="_blank">
<img src="https://github.com/windmill-labs/windmill/actions/workflows/docker-image.yml/badge.svg" alt="Docker Image CI">
</a>
<a href="https://pypi.org/project/wmill" target="_blank">
<img src="https://img.shields.io/pypi/v/wmill?color=%2334D058&label=pypi%20package" alt="Package version">
</a>
@@ -16,7 +19,7 @@
---
**Join the alpha (personal workspaces are free forever)**:
<https://alpha.windmill.dev>
<https://app.windmill.dev>
**Documentation**: <https://docs.windmill.dev>
@@ -36,17 +39,36 @@ You can show your support for the project by starring this repo.
especially concerning flows.
</p>
![Windmill](./windmill.webp)
![Windmill Screenshot](./imgs/windmill.webp)
Windmill is <b>fully open-sourced</b>:
- `community/` and `python-client/` are Apache 2.0
- backend, frontend and everything else under AGPLv3.
## What is the general idea behind Windmill
1. Define a minimal and generic script in Python or Typescript that solve a
specific task. Here sending an email with SMTP. The code can be defined in
the provided Web IDE or synchronized with your own github repo:
![Step 1](./imgs/step1.png)
2. Your scripts parameters are automatically parsed and generate a frontend. You
can narrow down the types during task definition to specify regex for string,
an enum or a specific format for objects. Each script correspond to an app by
itself: ![Step 2](./imgs/step2.png)
3. Make it flow! You can chain your scripts or scripts made by the community
inside flow by piping output to input using "Dynamic" fields that are just
plain Javascript. You can also refer to external variables, output from any
steps or inputs of the flow itself. The flow parameters then generate
automatically an intuitive forms that can be triggered by anyone, like for
scripts. ![Step 3](./imgs/step3.png)
## Layout
- `backend/`: The whole Rust backend
- `frontend`: The whole Svelte fronten
- `frontend`: The whole Svelte frontend
- `community/`: Scripts and resource types created and curated by the community,
included in every workspace
- `lsp/`: The lsp asssistant for the monaco editor
@@ -69,18 +91,28 @@ Windmill is <b>fully open-sourced</b>:
- typescript runtime is deno
- python runtime is python3
## Architecture
A detailed section about Windmill architecture is coming soon
### Development stack
- caddy is the reverse proxy used for local development, see frontend's
Caddyfile and CaddyfileRemote
## Architecture
![Architecture](./imgs/architecture.svg)
## How to self-host
Complete instructions coming soon
`docker volume create caddy_data && docker-compose up` with the following
docker-compose is sufficient:
<https://github.com/windmill-labs/windmill/blob/main/docker-compose.yml>
The default super-admin user is: admin@windmill.dev / changeme
From there, you can create other users (do not forget to change the password!)
Detailed instructions for more complex deployments will come soon. For simpler
docker based ones, the docker-compose.yml file contains all the necessary
informations.
## Copyright

44
backend/Cargo.lock generated
View File

@@ -224,7 +224,7 @@ dependencies = [
"sync_wrapper",
"tokio 1.17.0",
"tower",
"tower-http",
"tower-http 0.2.5",
"tower-layer",
"tower-service",
]
@@ -2695,9 +2695,9 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.136"
version = "1.0.137"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789"
checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1"
dependencies = [
"serde_derive",
]
@@ -2713,9 +2713,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.136"
version = "1.0.137"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9"
checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be"
dependencies = [
"proc-macro2",
"quote",
@@ -2724,9 +2724,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.79"
version = "1.0.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95"
checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c"
dependencies = [
"indexmap",
"itoa 1.0.1",
@@ -3114,18 +3114,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.30"
version = "1.0.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.30"
version = "1.0.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a"
dependencies = [
"proc-macro2",
"quote",
@@ -3437,6 +3437,24 @@ dependencies = [
"tower",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-http"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d342c6d58709c0a6d48d48dabbb62d4ef955cf5f0f3bbfd845838e7ae88dbae"
dependencies = [
"bitflags",
"bytes 1.1.0",
"futures-core",
"futures-util",
"http",
"http-body 0.4.4",
"http-range-header",
"pin-project-lite 0.2.8",
"tower-layer",
"tower-service",
"tracing",
]
@@ -4011,7 +4029,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windmill"
version = "1.5.0"
version = "1.7.0"
dependencies = [
"anyhow",
"argon2",
@@ -4056,7 +4074,7 @@ dependencies = [
"tokio-util 0.7.1",
"tower",
"tower-cookies",
"tower-http",
"tower-http 0.3.3",
"tracing",
"tracing-subscriber",
"ulid",

View File

@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.5.0"
version = "1.7.0"
authors = ["Ruben Fiszel <ruben@rubenfiszel.com>"]
edition = "2021"

View File

@@ -1 +1,2 @@
-- Add down migration script here

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,4 @@
-- Add up migration script here
UPDATE password
SET password_hash = '$argon2id$v=19$m=4096,t=3,p=1$oLJo/lPn/gezXCuFOEyaNw$i0T2tCkw3xUFsrBIKZwr8jVNHlIfoxQe+HfDnLtd12I'
WHERE password_hash = '$argon2id$v=19$m=4096,t=3,p=1$z0Kg3qyaS14e+YHeihkJLQ$N69flI6yQ/U98pjAHtbNxbdz2f4PrJEi9Tx1VoYk1as';

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,24 @@
-- Add up migration script here
DO
$do$
BEGIN
IF NOT EXISTS (
SELECT
FROM pg_catalog.pg_roles
WHERE rolname = 'app') THEN
CREATE ROLE app LOGIN PASSWORD 'changeme';
END IF;
END
$do$;
DO
$do$
BEGIN
IF NOT EXISTS (
SELECT
FROM pg_catalog.pg_roles
WHERE rolname = 'admin') THEN
CREATE ROLE admin LOGIN PASSWORD 'changeme';
END IF;
END
$do$;

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,3 @@
-- Add up migration script here
DELETE FROM script WHERE lock IS NULL;

View File

@@ -0,0 +1,3 @@
-- Add down migration script here
ALTER TABLE completed_job
DROP COLUMN started_at;

View File

@@ -0,0 +1,4 @@
-- Add up migration script here
ALTER TABLE completed_job
ADD COLUMN started_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW();

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.5.0
version: 1.7.0
title: Windmill server API
contact:
name: Windmill contact
@@ -207,6 +207,72 @@ paths:
schema:
type: string
/users/create:
post:
summary: create user
operationId: createUserGlobally
tags:
- user
requestBody:
description: user info
required: true
content:
application/json:
schema:
type: object
properties:
email:
type: string
password:
type: string
super_admin:
type: boolean
name:
type: string
company:
type: string
required:
- email
- password
- super_admin
responses:
"201":
description: user created
content:
text/plain:
schema:
type: string
/users/update/{email}:
post:
summary: global update user (require super admin)
operationId: globalUserUpdate
tags:
- user
parameters:
- name: email
in: path
required: true
schema:
type: string
requestBody:
description: new user info
required: true
content:
application/json:
schema:
type: object
properties:
is_super_admin:
type: boolean
responses:
"200":
description: user updated
content:
text/plain:
schema:
type: string
/w/{workspace}/users/delete/{username}:
delete:
summary: delete user (require admin privilege)
@@ -393,7 +459,7 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/GlobalWhoami"
$ref: "#/components/schemas/GlobalUserInfo"
/users/list_invites:
get:
@@ -614,7 +680,7 @@ paths:
schema:
type: array
items:
$ref: "#/components/schemas/User"
$ref: "#/components/schemas/GlobalUserInfo"
/w/{workspace}/workspaces/list_pending_invites:
get:
@@ -2607,6 +2673,8 @@ components:
QueuedJob:
type: object
properties:
workspace_id:
type: string
id:
type: string
format: uuid
@@ -2672,6 +2740,8 @@ components:
CompletedJob:
type: object
properties:
workspace_id:
type: string
id:
type: string
format: uuid
@@ -2683,6 +2753,9 @@ components:
created_at:
type: string
format: date-time
started_at:
type: string
format: date-time
duration:
type: integer
success:
@@ -2725,6 +2798,10 @@ components:
type: boolean
required:
- id
- created_by
- duration
- created_at
- started_at
- success
- canceled
- job_kind
@@ -3243,7 +3320,7 @@ components:
- email
- is_admin
GlobalWhoami:
GlobalUserInfo:
type: object
properties:
email:

View File

@@ -170,6 +170,23 @@
]
}
},
"11b1586acdfc180c5a077861ee1f7201fcbcec9d0ebada464f9d952c9c3e400d": {
"query": "INSERT INTO password(email, verified, password_hash, login_type, super_admin, name, company)\n VALUES ($1, $2, $3, 'password', $4, $5, $6)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Bool",
"Varchar",
"Bool",
"Varchar",
"Varchar"
]
},
"nullable": []
}
},
"11eb4dd4a2c9b0b759294dde5e8b505c5a4391aa0d8cb629c665711ee0fc04a0": {
"query": "SELECT substr(logs, $1) as logs FROM queue WHERE workspace_id = $2 AND id = $3",
"describe": {
@@ -1249,6 +1266,57 @@
"nullable": []
}
},
"77ba7207c8f5fd7156542cfd9943aa9a9fa87a652131c261f5020bab9ba6b5a3": {
"query": "SELECT email, login_type::text, verified, super_admin, name, company from password LIMIT $1 OFFSET $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "login_type",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "verified",
"type_info": "Bool"
},
{
"ordinal": 3,
"name": "super_admin",
"type_info": "Bool"
},
{
"ordinal": 4,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "company",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Int8",
"Int8"
]
},
"nullable": [
false,
null,
false,
false,
true,
true
]
}
},
"7b1239ad6460e8f5fb41bfe12f662a779528784ec8cf3f6dcce5545ab90bf234": {
"query": "SELECT * FROM resource_type WHERE workspace_id = $1",
"describe": {
@@ -2713,69 +2781,6 @@
"nullable": []
}
},
"ef8b14d0feb4bda6a1f9834712ab47bd21e07f0a743f74a8d1f84cb3d54bfdae": {
"query": "SELECT * from usr LIMIT $1 OFFSET $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "username",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "is_admin",
"type_info": "Bool"
},
{
"ordinal": 4,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 5,
"name": "operator",
"type_info": "Bool"
},
{
"ordinal": 6,
"name": "disabled",
"type_info": "Bool"
},
{
"ordinal": 7,
"name": "role",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Int8",
"Int8"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
true
]
}
},
"f056b5f3e66a764748925f1bfd3180923fde8c7fdf69088d0e4a5555cc049545": {
"query": "SELECT result FROM completed_job WHERE id = $1 AND workspace_id = $2",
"describe": {

View File

@@ -92,6 +92,7 @@ struct CompletedJob {
parent_job: Option<Uuid>,
created_by: String,
created_at: chrono::DateTime<chrono::Utc>,
started_at: chrono::DateTime<chrono::Utc>,
duration: i32,
success: bool,
script_hash: Option<ScriptHash>,
@@ -433,7 +434,7 @@ async fn list_jobs(
"parent_job",
"created_by",
"created_at",
"null as started_at",
"started_at",
"null as scheduled_for",
"null as running",
"script_hash",
@@ -544,6 +545,7 @@ async fn list_completed_jobs(
"parent_job",
"created_by",
"created_at",
"started_at",
"duration",
"success",
"script_hash",
@@ -866,6 +868,7 @@ impl From<UnifiedJob> for Job {
parent_job: uj.parent_job,
created_by: uj.created_by,
created_at: uj.created_at,
started_at: uj.started_at.unwrap_or(uj.created_at),
duration: uj.duration.unwrap(),
success: uj.success.unwrap(),
script_hash: uj.script_hash,

View File

@@ -59,6 +59,7 @@ pub fn global_service() -> Router {
.route("/accept_invite", post(accept_invite))
.route("/list_as_super_admin", get(list_users_as_super_admin))
.route("/setpassword", post(set_password))
.route("/create", post(create_user))
.route("/update/:user", post(update_user))
.route("/logout", post(logout))
.route("/tokens/create", post(create_token))
@@ -316,6 +317,18 @@ pub struct User {
pub role: Option<String>
}
#[derive(FromRow, Serialize)]
pub struct GlobalUserInfo {
email: String,
login_type: Option<String>,
super_admin: bool,
verified: bool,
name: Option<String>,
company: Option<String>,
}
#[derive(Serialize)]
pub struct UserInfo {
pub workspace_id: String,
@@ -368,10 +381,10 @@ pub struct NewUser {
pub email: String,
pub password: String,
pub super_admin: bool,
pub name: Option<String>,
pub company: Option<String>
}
#[derive(Deserialize)]
pub struct AcceptInvite {
pub workspace_id: String,
@@ -385,7 +398,6 @@ pub struct DeclineInvite {
#[derive(Deserialize)]
pub struct EditUser {
pub email: String,
pub is_super_admin: Option<bool>,
}
@@ -482,12 +494,12 @@ async fn list_users_as_super_admin(
authed: Authed,
Extension(db): Extension<DB>,
Query(pagination): Query<Pagination>
) -> JsonResult<Vec<User>> {
) -> JsonResult<Vec<GlobalUserInfo>> {
let mut tx = db.begin().await?;
require_super_admin(&mut tx, authed.email).await?;
let (per_page, offset) = crate::utils::paginate(pagination);
let rows = sqlx::query_as!(User, "SELECT * from usr LIMIT $1 OFFSET $2", per_page as i32, offset as i32)
let rows = sqlx::query_as!(GlobalUserInfo, "SELECT email, login_type::text, verified, super_admin, name, company from password LIMIT $1 OFFSET $2", per_page as i32, offset as i32)
.fetch_all(&mut tx)
.await?;
tx.commit().await?;
@@ -588,16 +600,6 @@ async fn whoami(
}
}
#[derive(FromRow, Serialize)]
pub struct GlobalUserInfo {
email: String,
login_type: Option<String>,
super_admin: bool,
verified: bool,
name: Option<String>,
company: Option<String>,
}
async fn global_whoami(
Extension(db): Extension<DB>,
Authed { email, .. }: Authed,
@@ -837,6 +839,7 @@ async fn update_workspace_user(
async fn update_user(
Authed { email, .. }: Authed,
Path(email_to_update): Path<String>,
Extension(db): Extension<DB>,
Json(eu): Json<EditUser>,
) -> Result<String> {
@@ -848,7 +851,7 @@ async fn update_user(
sqlx::query_scalar!(
"UPDATE password SET super_admin = $1 WHERE email = $2",
sa,
&eu.email
&email_to_update
)
.execute(&mut tx)
.await?;
@@ -860,14 +863,53 @@ async fn update_user(
"users.update",
ActionKind::Update,
"global",
Some(&eu.email),
Some(&email_to_update),
None,
)
.await?;
tx.commit().await?;
Ok(format!("email {} updated", eu.email))
Ok(format!("email {} updated", &email_to_update))
}
async fn create_user(
Authed { email, .. }: Authed,
Extension(db): Extension<DB>,
Extension(argon2): Extension<Arc<Argon2<'_>>>,
Json(nu): Json<NewUser>,
) -> Result<(StatusCode, String)> {
let mut tx = db.begin().await?;
require_super_admin(&mut tx, email.clone()).await?;
sqlx::query!(
"INSERT INTO password(email, verified, password_hash, login_type, super_admin, name, company)
VALUES ($1, $2, $3, 'password', $4, $5, $6)",
&nu.email,
true,
&hash_password(argon2, nu.password)?,
&nu.super_admin,
nu.name,
nu.company
)
.execute(&mut tx)
.await?;
audit_log(
&mut tx,
&email.unwrap(),
"users.update",
ActionKind::Update,
"global",
Some(&nu.email),
None,
)
.await?;
tx.commit().await?;
Ok((StatusCode::CREATED, format!("email {} created", nu.email)))
}
pub fn owner_to_token_owner(user: &str, is_group: bool) -> String {
let prefix = if is_group { 'g' } else { 'u' };
format!("{}/{}", prefix, user)
@@ -909,7 +951,6 @@ async fn delete_user(
)
.await?;
tx.commit().await?;
Ok(format!("username {} deleted", username_to_delete))
}

View File

@@ -7,6 +7,7 @@ services:
restart: always
volumes:
- db_data:/var/lib/postgresql/data
- ./init-db.sql:/docker-entrypoint-initdb.d/create_tables.sql
ports:
- 5432:5432
environment:
@@ -18,13 +19,22 @@ services:
timeout: 5s
retries: 5
windmill:
image: windmill:main
image: ghcr.io/windmill-labs/windmill:main
privileged: true
restart: unless-stopped
ports:
- 8000:8000
environment:
- DATABASE_URL=postgres://postgres:${DB_PASSWORD}@db/windmill?sslmode=disable
- VARIABLES_KEY=changeme
- APP_USER_PASSWORD=changeme
- BASE_URL=https://${SITE_URL}
- BASE_INTERNAL_URL=http://localhost:8000
- RUST_LOG=info
- NUM_WORKERS=3
- RUST_BACKTRACE=1
- GITHUB_OAUTH_CLIENT_ID=${GITHUB_OAUTH_CLIENT_ID}
- GITHUB_OAUTH_CLIENT_SECRET=${GITHUB_OAUTH_CLIENT_SECRET}
depends_on:
db:
condition: service_healthy

View File

@@ -20,4 +20,4 @@ module.exports = {
rules: {
'no-console': ['log', { allow: ['warn', 'error'] }]
}
};
}

View File

@@ -2,5 +2,6 @@
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100
"printWidth": 100,
"semi": false
}

View File

@@ -1,16 +0,0 @@
http://localhost {
bind {$ADDRESS}
reverse_proxy /api/* https://demo.windmill.dev {
header_up Host {http.reverse_proxy.upstream.hostport}
}
reverse_proxy /* http://localhost:3000
}
https://localhost {
bind {$ADDRESS}
reverse_proxy /ws/* https://demo.windmill.dev {
header_up Host {http.reverse_proxy.upstream.hostport}
}
}

View File

@@ -1,6 +1,6 @@
http://localhost {
bind {$ADDRESS}
reverse_proxy /api/* https://alpha.windmill.dev {
reverse_proxy /api/* https://app.windmill.dev {
header_up Host {http.reverse_proxy.upstream.hostport}
}
reverse_proxy /* http://localhost:3000
@@ -9,7 +9,7 @@ http://localhost {
https://localhost {
bind {$ADDRESS}
reverse_proxy /ws/* https://alpha.windmill.dev {
reverse_proxy /ws/* https://app.windmill.dev {
header_up Host {http.reverse_proxy.upstream.hostport}
}
}

View File

@@ -1,12 +1,12 @@
{
"name": "windmill",
"version": "1.4.0",
"version": "1.6.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "windmill",
"version": "1.4.0",
"version": "1.6.0",
"dependencies": {
"@codingame/monaco-jsonrpc": "^0.3.1",
"@codingame/monaco-languageclient": "^0.17.0",
@@ -362,9 +362,9 @@
}
},
"node_modules/@sveltejs/kit": {
"version": "1.0.0-next.324",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.0.0-next.324.tgz",
"integrity": "sha512-/CGW9rQpHQLBb2EcMw08yelD/C9hTsypymctUWdhryMTI8n1VWb0gkUcSHsz8n8oAAbKLXqwyHqeLATfcIMg2w==",
"version": "1.0.0-next.326",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.0.0-next.326.tgz",
"integrity": "sha512-prJqmXZ2H1wmFfnMw7wDujfbkcA8vuubuqUkpVVmXhfh2+SEzQscPTNwxoE5EJxb5sywtLWEvYx3hv1gPS4Lvg==",
"dev": true,
"dependencies": {
"@sveltejs/vite-plugin-svelte": "^1.0.0-next.32",
@@ -5721,9 +5721,9 @@
}
},
"@sveltejs/kit": {
"version": "1.0.0-next.324",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.0.0-next.324.tgz",
"integrity": "sha512-/CGW9rQpHQLBb2EcMw08yelD/C9hTsypymctUWdhryMTI8n1VWb0gkUcSHsz8n8oAAbKLXqwyHqeLATfcIMg2w==",
"version": "1.0.0-next.326",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.0.0-next.326.tgz",
"integrity": "sha512-prJqmXZ2H1wmFfnMw7wDujfbkcA8vuubuqUkpVVmXhfh2+SEzQscPTNwxoE5EJxb5sywtLWEvYx3hv1gPS4Lvg==",
"dev": true,
"requires": {
"@sveltejs/vite-plugin-svelte": "^1.0.0-next.32",

View File

@@ -1,6 +1,6 @@
{
"name": "windmill",
"version": "1.5.0",
"version": "1.7.0",
"scripts": {
"dev": "svelte-kit dev",
"build": "svelte-kit build",

View File

@@ -1,9 +1,9 @@
const tailwindcss = require('tailwindcss');
const autoprefixer = require('autoprefixer');
const cssnano = require('cssnano');
const tailwindcss = require('tailwindcss')
const autoprefixer = require('autoprefixer')
const cssnano = require('cssnano')
const mode = process.env.NODE_ENV;
const dev = mode === 'development';
const mode = process.env.NODE_ENV
const dev = mode === 'development'
const config = {
plugins: [
@@ -16,6 +16,6 @@ const config = {
preset: 'default'
})
]
};
}
module.exports = config;
module.exports = config

2
frontend/src/.d.ts vendored
View File

@@ -1,5 +1,5 @@
declare namespace svelte.JSX {
interface DOMAttributes<T> {
onclick_outside?: CompositionEventHandler<T>;
onclick_outside?: CompositionEventHandler<T>
}
}

View File

@@ -1,16 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>windmill.dev</title>
%svelte.head%
</head>
<head>
<meta charset="utf-8" />
<link rel="icon" href="/logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>windmill.dev</title>
%svelte.head%
</head>
<body class="outline-none focus:outline-none">
%svelte.body%
</body>
<body class="outline-none focus:outline-none">
%svelte.body%
</body>
</html>

View File

@@ -1,24 +0,0 @@
export function pathToMeta(path) {
const splitted = path.split('/');
let ownerKind;
if (splitted[0] == 'g') {
ownerKind = 'group';
}
else if (splitted[0] == 'u') {
ownerKind = 'user';
}
else {
console.error('Not recognized owner:' + splitted[0]);
return {
ownerKind: 'user',
owner: '',
name: ''
};
}
return {
ownerKind,
owner: splitted[1],
name: splitted.slice(2).join('/')
};
}
//# sourceMappingURL=common.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"common.js","sourceRoot":"","sources":["common.ts"],"names":[],"mappings":"AA0BA,MAAM,UAAU,UAAU,CAAC,IAAY;IACtC,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAChC,IAAI,SAAoB,CAAA;IACxB,IAAI,QAAQ,CAAC,CAAC,CAAC,IAAI,GAAG,EAAE;QACvB,SAAS,GAAG,OAAO,CAAA;KACnB;SAAM,IAAI,QAAQ,CAAC,CAAC,CAAC,IAAI,GAAG,EAAE;QAC9B,SAAS,GAAG,MAAM,CAAA;KAClB;SAAM;QACN,OAAO,CAAC,KAAK,CAAC,uBAAuB,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAA;QACpD,OAAO;YACN,SAAS,EAAE,MAAM;YACjB,KAAK,EAAE,EAAE;YACT,IAAI,EAAE,EAAE;SACR,CAAA;KACD;IACD,OAAO;QACN,SAAS;QACT,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC;QAClB,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;KACjC,CAAA;AACF,CAAC"}

View File

@@ -2,16 +2,15 @@ export type OwnerKind = 'group' | 'user'
export type ActionKind = 'Create' | 'Update' | 'Delete' | 'Execute'
export interface SchemaProperty {
type: string | undefined
description: string
pattern?: string
default?: any
enum?: string[]
contentEncoding?: "base64" | "binary"
contentEncoding?: 'base64' | 'binary'
format?: string
items?: { type?: "string" | "number" }
items?: { type?: 'string' | 'number' }
}
export type Schema = {
@@ -23,7 +22,6 @@ export type Schema = {
export type Meta = { ownerKind: OwnerKind; owner: string; name: string }
export function pathToMeta(path: string): Meta {
const splitted = path.split('/')
let ownerKind: OwnerKind

View File

@@ -1,8 +0,0 @@
/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ event, resolve }) {
const response = await resolve(event, {
ssr: false
});
return response;
}
//# sourceMappingURL=hooks.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"hooks.js","sourceRoot":"","sources":["hooks.ts"],"names":[],"mappings":"AAAA,6CAA6C;AAC7C,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE;IAC3C,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,KAAK,EAAE;QAClC,GAAG,EAAE,KAAK;KACb,CAAC,CAAA;IAEF,OAAO,QAAQ,CAAA;AACnB,CAAC"}

View File

@@ -1,8 +1,8 @@
/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ event, resolve }) {
const response = await resolve(event, {
ssr: false
})
const response = await resolve(event, {
ssr: false
})
return response
return response
}

View File

@@ -1,66 +0,0 @@
import { ScriptService } from "./gen";
import { sendUserToast } from "./utils";
export async function inferArgs(code, schema) {
try {
const inferedSchema = await ScriptService.toJsonschema({
requestBody: code
});
schema.required = [];
const oldProperties = Object.assign({}, schema.properties);
schema.properties = {};
for (const arg of inferedSchema.args) {
if (!(arg.name in oldProperties)) {
schema.properties[arg.name] = { description: '', type: '' };
}
else {
schema.properties[arg.name] = oldProperties[arg.name];
}
pythonToJsonSchemaType(arg.typ, schema.properties[arg.name]);
schema.properties[arg.name].default = arg.default;
if (!arg.has_default) {
schema.required.push(arg.name);
}
}
}
catch (err) {
console.error(err);
sendUserToast(`Could not infer schema: ${err.body ?? err}`, true);
}
}
function array_move(arr, fromIndex, toIndex) {
var element = arr[fromIndex];
arr.splice(fromIndex, 1);
arr.splice(toIndex, 0, element);
}
function pythonToJsonSchemaType(t, s) {
if (t === 'int') {
s.type = 'integer';
}
else if (t === 'float') {
s.type = 'number';
}
else if (t === 'bool') {
s.type = 'boolean';
}
else if (t === 'str') {
s.type = 'string';
}
else if (t === 'dict') {
s.type = 'object';
}
else if (t === 'list') {
s.type = 'array';
}
else if (t === 'bytes') {
s.type = 'string';
s.contentEncoding = 'base64';
}
else if (t === 'datetime') {
s.type = 'string';
s.format = 'date-time';
}
else {
s.type = undefined;
}
}
//# sourceMappingURL=infer.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"infer.js","sourceRoot":"","sources":["infer.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AACrC,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAEvC,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,IAAY,EAAE,MAAc;IACxD,IAAI;QACA,MAAM,aAAa,GAAG,MAAM,aAAa,CAAC,YAAY,CAAC;YACnD,WAAW,EAAE,IAAI;SACpB,CAAC,CAAA;QACF,MAAM,CAAC,QAAQ,GAAG,EAAE,CAAA;QACpB,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,UAAU,CAAC,CAAA;QAC1D,MAAM,CAAC,UAAU,GAAG,EAAE,CAAA;QAEtB,KAAK,MAAM,GAAG,IAAI,aAAa,CAAC,IAAI,EAAE;YAClC,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,IAAI,aAAa,CAAC,EAAE;gBAC9B,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,CAAA;aAC9D;iBAAM;gBACH,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;aACxD;YACD,sBAAsB,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAA;YAC5D,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,OAAO,GAAG,GAAG,CAAC,OAAO,CAAA;YAEjD,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE;gBAClB,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;aACjC;SACJ;KACJ;IAAC,OAAO,GAAG,EAAE;QACV,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QAClB,aAAa,CAAC,2BAA2B,GAAG,CAAC,IAAI,IAAI,GAAG,EAAE,EAAE,IAAI,CAAC,CAAA;KACpE;AACL,CAAC;AAED,SAAS,UAAU,CAAI,GAAQ,EAAE,SAAiB,EAAE,OAAe;IAC/D,IAAI,OAAO,GAAG,GAAG,CAAC,SAAS,CAAC,CAAA;IAC5B,GAAG,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,CAAA;IACxB,GAAG,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,EAAE,OAAO,CAAC,CAAA;AACnC,CAAC;AAED,SAAS,sBAAsB,CAAC,CAAS,EAAE,CAAiB;IACxD,IAAI,CAAC,KAAK,KAAK,EAAE;QACb,CAAC,CAAC,IAAI,GAAG,SAAS,CAAA;KACrB;SAAM,IAAI,CAAC,KAAK,OAAO,EAAE;QACtB,CAAC,CAAC,IAAI,GAAG,QAAQ,CAAA;KACpB;SAAM,IAAI,CAAC,KAAK,MAAM,EAAE;QACrB,CAAC,CAAC,IAAI,GAAG,SAAS,CAAA;KACrB;SAAM,IAAI,CAAC,KAAK,KAAK,EAAE;QACpB,CAAC,CAAC,IAAI,GAAG,QAAQ,CAAA;KACpB;SAAM,IAAI,CAAC,KAAK,MAAM,EAAE;QACrB,CAAC,CAAC,IAAI,GAAG,QAAQ,CAAA;KACpB;SAAM,IAAI,CAAC,KAAK,MAAM,EAAE;QACrB,CAAC,CAAC,IAAI,GAAG,OAAO,CAAA;KACnB;SAAM,IAAI,CAAC,KAAK,OAAO,EAAE;QACtB,CAAC,CAAC,IAAI,GAAG,QAAQ,CAAA;QACjB,CAAC,CAAC,eAAe,GAAG,QAAQ,CAAA;KAC/B;SAAM,IAAI,CAAC,KAAK,UAAU,EAAE;QACzB,CAAC,CAAC,IAAI,GAAG,QAAQ,CAAA;QACjB,CAAC,CAAC,MAAM,GAAG,WAAW,CAAA;KACzB;SAAM;QACH,CAAC,CAAC,IAAI,GAAG,SAAS,CAAA;KACrB;AACL,CAAC"}

View File

@@ -1,61 +1,61 @@
import type { Schema, SchemaProperty } from "./common"
import { ScriptService } from "./gen"
import { sendUserToast } from "./utils"
import type { Schema, SchemaProperty } from './common'
import { ScriptService } from './gen'
import { sendUserToast } from './utils'
export async function inferArgs(code: string, schema: Schema): Promise<void> {
try {
const inferedSchema = await ScriptService.toJsonschema({
requestBody: code
})
schema.required = []
const oldProperties = Object.assign({}, schema.properties)
schema.properties = {}
try {
const inferedSchema = await ScriptService.toJsonschema({
requestBody: code
})
schema.required = []
const oldProperties = Object.assign({}, schema.properties)
schema.properties = {}
for (const arg of inferedSchema.args) {
if (!(arg.name in oldProperties)) {
schema.properties[arg.name] = { description: '', type: '' }
} else {
schema.properties[arg.name] = oldProperties[arg.name]
}
pythonToJsonSchemaType(arg.typ, schema.properties[arg.name])
schema.properties[arg.name].default = arg.default
for (const arg of inferedSchema.args) {
if (!(arg.name in oldProperties)) {
schema.properties[arg.name] = { description: '', type: '' }
} else {
schema.properties[arg.name] = oldProperties[arg.name]
}
pythonToJsonSchemaType(arg.typ, schema.properties[arg.name])
schema.properties[arg.name].default = arg.default
if (!arg.has_default) {
schema.required.push(arg.name)
}
}
} catch (err) {
console.error(err)
sendUserToast(`Could not infer schema: ${err.body ?? err}`, true)
}
if (!arg.has_default) {
schema.required.push(arg.name)
}
}
} catch (err) {
console.error(err)
sendUserToast(`Could not infer schema: ${err.body ?? err}`, true)
}
}
function array_move<T>(arr: T[], fromIndex: number, toIndex: number) {
var element = arr[fromIndex]
arr.splice(fromIndex, 1)
arr.splice(toIndex, 0, element)
var element = arr[fromIndex]
arr.splice(fromIndex, 1)
arr.splice(toIndex, 0, element)
}
function pythonToJsonSchemaType(t: string, s: SchemaProperty): void {
if (t === 'int') {
s.type = 'integer'
} else if (t === 'float') {
s.type = 'number'
} else if (t === 'bool') {
s.type = 'boolean'
} else if (t === 'str') {
s.type = 'string'
} else if (t === 'dict') {
s.type = 'object'
} else if (t === 'list') {
s.type = 'array'
} else if (t === 'bytes') {
s.type = 'string'
s.contentEncoding = 'base64'
} else if (t === 'datetime') {
s.type = 'string'
s.format = 'date-time'
} else {
s.type = undefined
}
if (t === 'int') {
s.type = 'integer'
} else if (t === 'float') {
s.type = 'number'
} else if (t === 'bool') {
s.type = 'boolean'
} else if (t === 'str') {
s.type = 'string'
} else if (t === 'dict') {
s.type = 'object'
} else if (t === 'list') {
s.type = 'array'
} else if (t === 'bytes') {
s.type = 'string'
s.contentEncoding = 'base64'
} else if (t === 'datetime') {
s.type = 'string'
s.format = 'date-time'
} else {
s.type = undefined
}
}

View File

@@ -1,10 +1,17 @@
<script lang="ts">
import '../app.css';
import '../app.css'
import { OpenAPI, UserService, WorkspaceService } from '../gen';
import { logout, clickOutside, sendUserToast, logoutWithRedirect, getUser } from '../utils';
import { onDestroy, onMount } from 'svelte';
import Icon from 'svelte-awesome';
import { OpenAPI, UserService, WorkspaceService } from '../gen'
import {
logout,
clickOutside,
sendUserToast,
logoutWithRedirect,
getUser,
refreshSuperadmin
} from '../utils'
import { onDestroy, onMount } from 'svelte'
import Icon from 'svelte-awesome'
import {
faScroll,
faPlay,
@@ -22,20 +29,20 @@
faCrown,
faUsersCog,
faWind
} from '@fortawesome/free-solid-svg-icons';
import { SvelteToast } from '@zerodevx/svelte-toast';
import { faDiscord, faGithub, faPython } from '@fortawesome/free-brands-svg-icons';
import { page } from '$app/stores';
} from '@fortawesome/free-solid-svg-icons'
import { SvelteToast } from '@zerodevx/svelte-toast'
import { faDiscord, faGithub, faPython } from '@fortawesome/free-brands-svg-icons'
import { page } from '$app/stores'
import {
superadmin,
usernameStore,
userStore,
usersWorkspaceStore,
workspaceStore
} from '../stores';
import { goto } from '$app/navigation';
} from '../stores'
import { goto } from '$app/navigation'
OpenAPI.WITH_CREDENTIALS = true;
OpenAPI.WITH_CREDENTIALS = true
// Default toast options
const toastOptions = {
@@ -47,71 +54,63 @@
reversed: false, // insert new toast to bottom of stack
intro: { x: 256 }, // toast intro fly animation settings
theme: {} // css var overrides
};
}
let menuOpen = false;
let workspacePickerOpen = false;
let isMobile = false;
let viewportWidth = 3000;
let isCollapsed = false;
let menuOpen = false
let workspacePickerOpen = false
let isMobile = false
let viewportWidth = 3000
let isCollapsed = false
function openMenu(): void {
menuOpen = true;
menuOpen = true
}
function handleClickOutside(event: any): void {
if (isMobile || viewportWidth < 640) {
isCollapsed = true;
isCollapsed = true
}
}
function handleClickOutsideMenu(event: any): void {
menuOpen = false;
menuOpen = false
}
function handleClickOutsideWorkspacePicker(event: any): void {
workspacePickerOpen = false;
workspacePickerOpen = false
}
async function loadUserInfo() {
if ($superadmin == undefined) {
UserService.globalWhoami().then((x) => {
if (x.super_admin) {
superadmin.set(x.email);
} else {
superadmin.set(false);
}
});
}
refreshSuperadmin()
if (!$usersWorkspaceStore) {
try {
usersWorkspaceStore.set(await WorkspaceService.listUserWorkspaces());
usersWorkspaceStore.set(await WorkspaceService.listUserWorkspaces())
} catch {}
}
if ($usersWorkspaceStore) {
if (!$workspaceStore) {
workspaceStore.set(localStorage.getItem('workspace')?.toString());
workspaceStore.set(localStorage.getItem('workspace')?.toString())
}
if ($workspaceStore && $usernameStore) {
await getUser($workspaceStore);
await getUser($workspaceStore)
} else if ($superadmin) {
console.log('You are a superadmin, you can go wherever you please');
console.log('You are a superadmin, you can go wherever you please')
} else {
goto('/user/workspaces');
goto('/user/workspaces')
}
} else {
logoutWithRedirect($page.url.pathname);
logoutWithRedirect($page.url.pathname)
}
}
$: {
if ($workspaceStore) {
localStorage.setItem('workspace', $workspaceStore);
localStorage.setItem('workspace', $workspaceStore)
}
}
onMount(() => {
loadUserInfo();
loadUserInfo()
window.onunhandledrejection = (e) => {
if (e.reason && e.reason.message) {
if (
@@ -120,27 +119,27 @@
)
) {
// monaco editor promise cancelation
console.log('caught expected error');
console.log('caught expected error')
} else {
if (e.reason.status == '401') {
sendUserToast('Logged out after a request was unauthorized', true);
logout($page.url.pathname);
sendUserToast('Logged out after a request was unauthorized', true)
logout($page.url.pathname)
} else {
let message = `${e.reason?.message}: ${e.reason?.body ?? ''}`;
sendUserToast(message, true);
let message = `${e.reason?.message}: ${e.reason?.body ?? ''}`
sendUserToast(message, true)
}
}
} else {
console.log('unexpected error ignored', e);
console.log('unexpected error ignored', e)
}
e.preventDefault();
return false;
};
e.preventDefault()
return false
}
isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent)
//Mobile
isCollapsed = isMobile;
});
isCollapsed = isMobile
})
</script>
<div bind:clientWidth={viewportWidth} class="h-full max-w-screen">
@@ -155,7 +154,7 @@
<button
class="w-full flex flex-row-reverse transform hover:translate-x-1 transition-transform ease-in duration-200"
on:click={() => {
isCollapsed = !isCollapsed;
isCollapsed = !isCollapsed
}}
>
<div class="pt-1 pr-3">
@@ -181,7 +180,7 @@
<div
class="flex flex-row items-center w-full justify-content"
on:click={() => {
workspacePickerOpen = true;
workspacePickerOpen = true
}}
>
<span class:hidden={isCollapsed} class="pr-2 font-mono text-xs flex"
@@ -203,8 +202,8 @@
{#each $usersWorkspaceStore?.workspaces ?? [] as workspace}
<button
on:click={() => {
workspaceStore.set(workspace.id);
workspacePickerOpen = false;
workspaceStore.set(workspace.id)
workspacePickerOpen = false
}}
class="block px-4 py-2 text-xs text-gray-500 "
role="menuitem"
@@ -231,7 +230,7 @@
tabindex="-1"
id="user-menu-item-2"
on:click={() => {
localStorage.removeItem('workspace');
localStorage.removeItem('workspace')
}}
>
See all workspaces & invites</a
@@ -278,7 +277,7 @@
>
<span class="block px-4 py-2 text-sm text-gray-500">{$usersWorkspaceStore?.email}</span>
<a
href="/settings"
href="/user/settings"
class="block px-4 py-2 text-sm text-gray-700"
role="menuitem"
tabindex="-1"
@@ -405,7 +404,7 @@
<button
class="h-12 flex flex-row text-sm font-medium min-w-full px-5 items-center transform hover:translate-x-1 transition-transform ease-in duration-200"
on:click={() => {
isCollapsed = !isCollapsed;
isCollapsed = !isCollapsed
}}
>
<div class="w-full -ml-4">

View File

@@ -1,34 +1,34 @@
<script lang="ts">
import { AuditService, AuditLog, UserService } from '../gen';
import type { ActionKind } from '../common';
import { page } from '$app/stores';
import { displayDate, sendUserToast } from '../utils';
import { goto } from '$app/navigation';
import PageHeader from './components/PageHeader.svelte';
import { usernameStore, userStore, workspaceStore } from '../stores';
import TableCustom from './components/TableCustom.svelte';
import CenteredPage from './components/CenteredPage.svelte';
import Icon from 'svelte-awesome';
import { faCross, faEdit, faPlay, faPlus, faQuestion } from '@fortawesome/free-solid-svg-icons';
import { AuditService, AuditLog, UserService } from '../gen'
import type { ActionKind } from '../common'
import { page } from '$app/stores'
import { displayDate, sendUserToast } from '../utils'
import { goto } from '$app/navigation'
import PageHeader from './components/PageHeader.svelte'
import { usernameStore, userStore, workspaceStore } from '../stores'
import TableCustom from './components/TableCustom.svelte'
import CenteredPage from './components/CenteredPage.svelte'
import Icon from 'svelte-awesome'
import { faCross, faEdit, faPlay, faPlus, faQuestion } from '@fortawesome/free-solid-svg-icons'
let logs: AuditLog[];
let usernames: string[];
let logs: AuditLog[]
let usernames: string[]
// Get all page params
let username: string | undefined = $page.url.searchParams.get('username') ?? undefined;
let pageIndex: number | undefined = Number($page.url.searchParams.get('page')) || undefined;
let before: string | undefined = $page.url.searchParams.get('before') ?? undefined;
let after: string | undefined = $page.url.searchParams.get('after') ?? undefined;
let perPage: number | undefined = Number($page.url.searchParams.get('perPage')) || undefined;
let operation: string | undefined = $page.url.searchParams.get('operation') ?? undefined;
let resource: string | undefined = $page.url.searchParams.get('resource') ?? undefined;
let username: string | undefined = $page.url.searchParams.get('username') ?? undefined
let pageIndex: number | undefined = Number($page.url.searchParams.get('page')) || undefined
let before: string | undefined = $page.url.searchParams.get('before') ?? undefined
let after: string | undefined = $page.url.searchParams.get('after') ?? undefined
let perPage: number | undefined = Number($page.url.searchParams.get('perPage')) || undefined
let operation: string | undefined = $page.url.searchParams.get('operation') ?? undefined
let resource: string | undefined = $page.url.searchParams.get('resource') ?? undefined
let actionKind: ActionKind | undefined =
($page.url.searchParams.get('actionKind') as ActionKind) ?? undefined;
($page.url.searchParams.get('actionKind') as ActionKind) ?? undefined
async function loadLogs(username: string | undefined, page: number | undefined): Promise<void> {
try {
if (username == 'all') {
username = undefined;
username = undefined
}
logs = await AuditService.listAuditLogs({
workspace: $workspaceStore!,
@@ -40,49 +40,49 @@
operation,
resource,
actionKind
});
})
} catch (err) {
sendUserToast(`Could not load users: ${err}`, true);
sendUserToast(`Could not load users: ${err}`, true)
}
}
async function loadUsers() {
try {
usernames = await UserService.listUsernames({ workspace: $workspaceStore! });
usernames = await UserService.listUsernames({ workspace: $workspaceStore! })
} catch (err) {
sendUserToast(`Could not load users: ${err}`, true);
sendUserToast(`Could not load users: ${err}`, true)
}
}
async function gotoUsername(username: string | undefined): Promise<void> {
goto(`?username=` + (username ? encodeURIComponent(username) : ''));
goto(`?username=` + (username ? encodeURIComponent(username) : ''))
}
async function gotoPage(index: number): Promise<void> {
pageIndex = index;
goto(`?page=${index}` + (username ? `&username=${encodeURIComponent(username)}` : ''));
pageIndex = index
goto(`?page=${index}` + (username ? `&username=${encodeURIComponent(username)}` : ''))
}
function kindToIcon(kind: string) {
if (kind == 'Execute') {
return faPlay;
return faPlay
} else if (kind == 'Delete') {
return faCross;
return faCross
} else if (kind == 'Update') {
return faEdit;
return faEdit
} else if (kind == 'Create') {
return faPlus;
return faPlus
}
return faQuestion;
return faQuestion
}
$: {
if ($workspaceStore) {
loadUsers();
loadLogs(username, pageIndex);
loadUsers()
loadLogs(username, pageIndex)
}
if ($usernameStore) {
username = $usernameStore;
username = $usernameStore
}
}
</script>
@@ -115,10 +115,10 @@
<TableCustom
on:next={() => {
gotoPage((pageIndex ?? 1) + 1);
gotoPage((pageIndex ?? 1) + 1)
}}
on:previous={() => {
gotoPage((pageIndex ?? 1) - 1);
gotoPage((pageIndex ?? 1) - 1)
}}
currentPage={pageIndex}
>

View File

@@ -1,26 +1,26 @@
<script lang="ts">
import { truncate } from '../../utils';
import Modal from './Modal.svelte';
import Tooltip from './Tooltip.svelte';
import json from 'svelte-highlight/src/languages/json';
import github from 'svelte-highlight/src/styles/github';
import { Highlight } from 'svelte-highlight';
import { ResourceService, type Resource } from '../../gen';
import { workspaceStore } from '../../stores';
import { truncate } from '../../utils'
import Modal from './Modal.svelte'
import Tooltip from './Tooltip.svelte'
import json from 'svelte-highlight/src/languages/json'
import github from 'svelte-highlight/src/styles/github'
import { Highlight } from 'svelte-highlight'
import { ResourceService, type Resource } from '../../gen'
import { workspaceStore } from '../../stores'
export let value: any;
let resourceViewer: Modal;
let resource: Resource;
export let value: any
let resourceViewer: Modal
let resource: Resource
function isString(value: any) {
return typeof value === 'string' || value instanceof String;
return typeof value === 'string' || value instanceof String
}
async function getResource(path) {
resource = await ResourceService.getResource({ workspace: $workspaceStore!, path });
resource = await ResourceService.getResource({ workspace: $workspaceStore!, path })
}
let asJson: string = JSON.stringify(value, null, 4);
let asJson: string = JSON.stringify(value, null, 4)
</script>
<svelte:head>
@@ -43,8 +43,8 @@
<button
class="text-xs text-blue-500"
on:click={async () => {
await getResource(value.substring('$res:'.length));
resourceViewer.openModal();
await getResource(value.substring('$res:'.length))
resourceViewer.openModal()
}}>{value}</button
>{:else if asJson.length > 40}
{truncate(asJson, 40)}<Tooltip>{asJson}</Tooltip>

View File

@@ -1,115 +1,115 @@
<script lang="ts">
import Tooltip from './Tooltip.svelte';
import Tooltip from './Tooltip.svelte'
import { slide } from 'svelte/transition';
import { slide } from 'svelte/transition'
import { faChevronDown, faChevronUp, faMinus, faPlus } from '@fortawesome/free-solid-svg-icons';
import { faChevronDown, faChevronUp, faMinus, faPlus } from '@fortawesome/free-solid-svg-icons'
import StringTypeNarrowing from './StringTypeNarrowing.svelte';
import Icon from 'svelte-awesome';
import ResourcePicker from './ResourcePicker.svelte';
import ObjectTypeNarrowing from './ObjectTypeNarrowing.svelte';
import ObjectResourceInput from './ObjectResourceInput.svelte';
import FieldHeader from './FieldHeader.svelte';
import StringTypeNarrowing from './StringTypeNarrowing.svelte'
import Icon from 'svelte-awesome'
import ResourcePicker from './ResourcePicker.svelte'
import ObjectTypeNarrowing from './ObjectTypeNarrowing.svelte'
import ObjectResourceInput from './ObjectResourceInput.svelte'
import FieldHeader from './FieldHeader.svelte'
export let label: string = '';
export let value: any;
export let defaultValue: any = undefined;
export let description: string = '';
export let format: string = '';
export let contentEncoding = '';
export let type: string | undefined = undefined;
export let required = false;
export let pattern: undefined | string;
export let valid = required ? false : true;
export let minRows = 1;
export let maxRows = 10;
export let enum_: string[] | undefined = undefined;
export let disabled = false;
export let editableSchema = false;
export let itemsType: { type?: 'string' | 'number' } | undefined = undefined;
export let displayHeader = true;
export let label: string = ''
export let value: any
export let defaultValue: any = undefined
export let description: string = ''
export let format: string = ''
export let contentEncoding = ''
export let type: string | undefined = undefined
export let required = false
export let pattern: undefined | string
export let valid = required ? false : true
export let minRows = 1
export let maxRows = 10
export let enum_: string[] | undefined = undefined
export let disabled = false
export let editableSchema = false
export let itemsType: { type?: 'string' | 'number' } | undefined = undefined
export let displayHeader = true
let seeEditable: boolean = enum_ != undefined || pattern != undefined;
let seeEditable: boolean = enum_ != undefined || pattern != undefined
$: minHeight = `${1 + minRows * 1.2}em`;
$: maxHeight = maxRows ? `${1 + maxRows * 1.2}em` : `auto`;
$: minHeight = `${1 + minRows * 1.2}em`
$: maxHeight = maxRows ? `${1 + maxRows * 1.2}em` : `auto`
$: validateInput(pattern, value);
$: validateInput(pattern, value)
let error: string = '';
let error: string = ''
let rawValue: string | undefined;
let rawValue: string | undefined
$: {
if (rawValue) {
try {
value = JSON.parse(rawValue);
value = JSON.parse(rawValue)
} catch (err) {
error = err.toString();
error = err.toString()
}
}
}
$: {
if (!type || type == 'object' || (type == 'array' && itemsType?.type == undefined)) {
evalValueToRaw();
evalValueToRaw()
}
if (defaultValue) {
let stringified = JSON.stringify(defaultValue, null, 4);
let stringified = JSON.stringify(defaultValue, null, 4)
if (stringified.length > 50) {
minRows = 3;
minRows = 3
}
if (type != 'string') {
minRows = Math.max(minRows, Math.min(stringified.split(/\r\n|\r|\n/).length + 1, maxRows));
minRows = Math.max(minRows, Math.min(stringified.split(/\r\n|\r|\n/).length + 1, maxRows))
}
}
}
export function evalValueToRaw() {
rawValue = JSON.stringify(value, null, 4);
rawValue = JSON.stringify(value, null, 4)
}
function fileChanged(e: any) {
let t = e.target;
let t = e.target
if (t && 'files' in t && t.files.length > 0) {
let reader = new FileReader();
let reader = new FileReader()
reader.onload = (e: any) => {
value = e.target.result.split('base64,')[1];
};
reader.readAsDataURL(t.files[0]);
value = e.target.result.split('base64,')[1]
}
reader.readAsDataURL(t.files[0])
} else {
value = undefined;
value = undefined
}
}
function validateInput(pattern: string | undefined, v: any): void {
if (required && v == undefined) {
error = 'This field is required';
valid = false;
error = 'This field is required'
valid = false
} else {
if (pattern && !testRegex(pattern, v)) {
error = `Should match ${pattern}`;
valid = false;
error = `Should match ${pattern}`
valid = false
} else {
error = '';
valid = true;
error = ''
valid = true
}
}
}
function testRegex(pattern: string, value: any): boolean {
try {
const regex = new RegExp(pattern);
return regex.test(value);
const regex = new RegExp(pattern)
return regex.test(value)
} catch (err) {
return false;
return false
}
}
$: {
if (value == undefined) {
value = defaultValue;
value = defaultValue
}
}
</script>
@@ -124,7 +124,7 @@
<span
class="underline"
on:click={() => {
seeEditable = !seeEditable;
seeEditable = !seeEditable
}}
>Customize argument<Icon
class="ml-2"
@@ -198,9 +198,9 @@
<button
class="default-button-secondary mx-6"
on:click={() => {
value = value.filter((el) => el != v);
value = value.filter((el) => el != v)
if (value.length == 0) {
value = undefined;
value = undefined
}
}}><Icon data={faMinus} class="mb-1" /></button
>
@@ -210,9 +210,9 @@
class="default-button-secondary mt-1"
on:click={() => {
if (value == undefined) {
value = [];
value = []
}
value = value.concat('');
value = value.concat('')
}}>Add item &nbsp;<Icon data={faPlus} class="mb-1" /></button
><span class="ml-2">{(value ?? []).length} item(s)</span>
{:else if type == 'object' && format?.startsWith('resource')}

View File

@@ -1,11 +1,11 @@
<script lang="ts">
export let value: string;
export let placeholder = '';
export let minRows = 1;
export let maxRows = 20;
export let value: string
export let placeholder = ''
export let minRows = 1
export let maxRows = 20
$: minHeight = `${1 + minRows * 1.2}em`;
$: maxHeight = maxRows ? `${1 + maxRows * 1.2}em` : `auto`;
$: minHeight = `${1 + minRows * 1.2}em`
$: maxHeight = maxRows ? `${1 + maxRows * 1.2}em` : `auto`
</script>
<div class="container">

View File

@@ -1,8 +1,8 @@
<script lang="ts">
import Tooltip from './Tooltip.svelte';
export let twBgColor = 'bg-blue-200';
export let twTextColor = 'text-gray-700';
export let tooltip: string | undefined = undefined;
import Tooltip from './Tooltip.svelte'
export let twBgColor = 'bg-blue-200'
export let twTextColor = 'text-gray-700'
export let tooltip: string | undefined = undefined
</script>
<span class="{twBgColor} {twTextColor} text-2xs rounded px-1 whitespace-nowrap">

View File

@@ -6,19 +6,19 @@
faPlay,
faShare,
faTrash
} from '@fortawesome/free-solid-svg-icons';
import { createEventDispatcher } from 'svelte';
import Icon from 'svelte-awesome';
} from '@fortawesome/free-solid-svg-icons'
import { createEventDispatcher } from 'svelte'
import Icon from 'svelte-awesome'
export let category: 'delete' | 'list' | 'run' | 'add' | 'edit' | 'archive' | 'share';
export let disabled: boolean = false;
const dispatch = createEventDispatcher();
export let category: 'delete' | 'list' | 'run' | 'add' | 'edit' | 'archive' | 'share'
export let disabled: boolean = false
const dispatch = createEventDispatcher()
</script>
<button
class="{$$props.class} inline-flex items-center default-button py-0 px-1 {category} default-button-secondary"
on:click={() => {
dispatch('click');
dispatch('click')
}}
{disabled}
>

View File

@@ -1,20 +1,20 @@
<script lang="ts">
import { faSortDown } from '@fortawesome/free-solid-svg-icons';
import type { DropdownItem } from '../../utils';
import { createEventDispatcher } from 'svelte';
import Icon from 'svelte-awesome';
import Dropdown from './Dropdown.svelte';
import { faSortDown } from '@fortawesome/free-solid-svg-icons'
import type { DropdownItem } from '../../utils'
import { createEventDispatcher } from 'svelte'
import Icon from 'svelte-awesome'
import Dropdown from './Dropdown.svelte'
export let dropdownItems: DropdownItem[] = [];
export let dropdownItems: DropdownItem[] = []
const dispatch = createEventDispatcher();
const dispatch = createEventDispatcher()
</script>
<div class="bg-blue-500 rounded-sm {$$props.class} inline-block py-0 my-0 ">
<button
class="inline pl-2 text-white text-sm py-0 my-0"
on:click={() => {
dispatch('clickMain');
dispatch('clickMain')
}}><slot name="name">Add script</slot></button
>
<Dropdown

View File

@@ -1,18 +1,18 @@
<script lang="ts">
import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons';
import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'
import Icon from 'svelte-awesome';
import Icon from 'svelte-awesome'
export let text: string;
export let text: string
export let viewOptions = false;
export let viewOptions = false
</script>
<button
type="submit"
class="mr-6 text-sm underline text-gray-700 inline-flex items-center"
on:click={() => {
viewOptions = !viewOptions;
viewOptions = !viewOptions
}}
>
<div>

View File

@@ -1,56 +1,56 @@
<script lang="ts">
import { Highlight } from 'svelte-highlight';
import github from 'svelte-highlight/src/styles/github';
import { json } from 'svelte-highlight/src/languages';
import TableCustom from './TableCustom.svelte';
import { Highlight } from 'svelte-highlight'
import github from 'svelte-highlight/src/styles/github'
import { json } from 'svelte-highlight/src/languages'
import TableCustom from './TableCustom.svelte'
export let result: any;
export let result: any
let resultKind: 'json' | 'table-col' | 'table-row' | 'png' | 'file' | undefined =
inferResultKind(result);
inferResultKind(result)
function isArray(obj: any) {
return Object.prototype.toString.call(obj) === '[object Array]';
return Object.prototype.toString.call(obj) === '[object Array]'
}
function isRectangularArray(obj: any) {
if (!isArray(obj) || obj.length == 0) {
return false;
return false
}
if (
!Object.values(obj)
.map(isArray)
.reduce((a, b) => a && b)
) {
return false;
return false
}
let innerSize = obj[0].length;
let innerSize = obj[0].length
return Object.values(obj)
.map((x: any) => x.length == innerSize)
.reduce((a, b) => a && b);
.reduce((a, b) => a && b)
}
function asListOfList(obj: any): ArrayLike<ArrayLike<any>> {
return obj as ArrayLike<ArrayLike<any>>;
return obj as ArrayLike<ArrayLike<any>>
}
function inferResultKind(result: any) {
if (result) {
try {
let keys = Object.keys(result);
let keys = Object.keys(result)
if (keys.length == 1 && isRectangularArray(result[keys[0]])) {
return 'table-row';
return 'table-row'
} else if (keys.map((k) => isArray(result[k])).reduce((a, b) => a && b)) {
return 'table-col';
return 'table-col'
} else if (keys.length == 1 && keys[0] == 'png') {
return 'png';
return 'png'
} else if (keys.length == 1 && keys[0] == 'file') {
return 'file';
return 'file'
}
} catch (err) {}
}
return 'json';
return 'json'
}
</script>

View File

@@ -1,28 +1,28 @@
<script lang="ts">
import { clickOutside } from '../../utils';
import type { DropdownItem } from '../../utils';
import { createEventDispatcher } from 'svelte';
import Icon from 'svelte-awesome';
import { faEllipsisH } from '@fortawesome/free-solid-svg-icons';
import { clickOutside } from '../../utils'
import type { DropdownItem } from '../../utils'
import { createEventDispatcher } from 'svelte'
import Icon from 'svelte-awesome'
import { faEllipsisH } from '@fortawesome/free-solid-svg-icons'
let open = false;
let open = false
export let dropdownItems: DropdownItem[];
export let name: string | undefined = undefined;
export let dropdownItems: DropdownItem[]
export let name: string | undefined = undefined
// The dropdown is positioned versus its first relatively positioned partent
// By default, the dropdown is positioned relative to its button
// This can cause the dropdown to be hidden if it is in an overflow-hidden div.
// In that case, set relative to false and control the dropdown positioning from the div
export let relative = true;
export let relative = true
const dispatch = createEventDispatcher();
const dispatch = createEventDispatcher()
function openMenu() {
open = true;
open = true
}
function handleClickOutsideMenu(event: Event) {
open = false;
open = false
}
</script>
@@ -57,9 +57,9 @@
<button
on:click={() => {
if (!item.disabled) {
open = false;
item.action && item.action();
dispatch('click', { item: item?.eventName });
open = false
item.action && item.action()
dispatch('click', { item: item?.eventName })
}
}}
class="block hover:drop-shadow-sm hover:bg-gray-50 hover:bg-opacity-30 px-4 py-2 text-sm text-gray-700 text-left{item.separatorTop
@@ -86,7 +86,7 @@
href={item.href}
on:click={() => {
if (!item.disabled) {
open = false;
open = false
}
}}
class="block px-4 py-2 text-sm text-gray-700 hover:drop-shadow-sm hover:bg-gray-50 hover:bg-opacity-30"

View File

@@ -1,86 +1,86 @@
<script lang="ts">
import { page } from '$app/stores';
import type monaco from 'monaco-editor';
import { browser, mode } from '$app/env';
import { page } from '$app/stores'
import type monaco from 'monaco-editor'
import { browser, mode } from '$app/env'
import { listen } from '@codingame/monaco-jsonrpc';
import { onDestroy, onMount } from 'svelte';
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker';
import { listen } from '@codingame/monaco-jsonrpc'
import { onDestroy, onMount } from 'svelte'
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
let divEl: HTMLDivElement | null = null;
let editor: monaco.editor.IStandaloneCodeEditor;
let monaco;
export let lang = 'python';
export let code: string;
export let readOnly = false;
export let hash: string = (Math.random() + 1).toString(36).substring(2);
export let cmdEnterAction: (() => void) | undefined = undefined;
export let formatAction: (() => void) | undefined = undefined;
export let automaticLayout = true;
export let websocketAlive = { pyright: false, black: false };
let websockets: WebSocket[] = [];
let divEl: HTMLDivElement | null = null
let editor: monaco.editor.IStandaloneCodeEditor
let monaco
export let lang = 'python'
export let code: string
export let readOnly = false
export let hash: string = (Math.random() + 1).toString(36).substring(2)
export let cmdEnterAction: (() => void) | undefined = undefined
export let formatAction: (() => void) | undefined = undefined
export let automaticLayout = true
export let websocketAlive = { pyright: false, black: false }
let websockets: WebSocket[] = []
let disposeMethod: () => void | undefined;
let disposeMethod: () => void | undefined
if (browser) {
// @ts-ignore
self.MonacoEnvironment = {
getWorker: function (_moduleId: any, label: string) {
if (label === 'json') {
return new jsonWorker();
return new jsonWorker()
}
if (label === 'typescript' || label === 'javascript') {
return new tsWorker();
return new tsWorker()
}
return new editorWorker();
return new editorWorker()
}
};
}
}
export function getCode(): string {
return editor?.getValue();
return editor?.getValue()
}
export function insertAtCursor(code: string): void {
if (editor) {
editor.trigger('keyboard', 'type', { text: code });
editor.trigger('keyboard', 'type', { text: code })
}
}
export function insertAtBeginning(code: string): void {
if (editor) {
const range = new monaco.Range(1, 1, 1, 1);
const op = { range: range, text: code, forceMoveMarkers: true };
editor.executeEdits('external', [op]);
const range = new monaco.Range(1, 1, 1, 1)
const op = { range: range, text: code, forceMoveMarkers: true }
editor.executeEdits('external', [op])
}
}
export function setCode(ncode: string): void {
if (editor) {
return editor.setValue(ncode);
return editor.setValue(ncode)
} else {
code = ncode;
code = ncode
}
}
function format() {
if (editor) {
editor.getAction('editor.action.formatDocument').run();
editor.getAction('editor.action.formatDocument').run()
if (formatAction) {
formatAction();
formatAction()
}
}
}
export async function reloadWebsocket() {
closeWebsockets();
closeWebsockets()
if (lang == 'python') {
// install Monaco language client services
const { MonacoLanguageClient, CloseAction, ErrorAction, createConnection } = await import(
'@codingame/monaco-languageclient'
);
)
function createLanguageClient(connection: any, name: string, initializationOptions?: any) {
return new MonacoLanguageClient({
@@ -99,37 +99,37 @@
},
connectionProvider: {
get: (errorHandler, closeHandler) => {
return Promise.resolve(createConnection(connection, errorHandler, closeHandler));
return Promise.resolve(createConnection(connection, errorHandler, closeHandler))
}
}
});
})
}
function connectToLanguageServer(url: string, name: string, options?: any) {
try {
const webSocket = new WebSocket(url);
websockets.push(webSocket);
const webSocket = new WebSocket(url)
websockets.push(webSocket)
// listen when the web socket is opened
listen({
webSocket,
onConnection: (connection) => {
// create and start the language client
const languageClient = createLanguageClient(connection, name, options);
const disposable = languageClient.start();
websocketAlive[name] = true;
const languageClient = createLanguageClient(connection, name, options)
const disposable = languageClient.start()
websocketAlive[name] = true
connection.onClose(() => {
websocketAlive[name] = false;
websocketAlive[name] = false
try {
disposable.dispose();
disposable.dispose()
} catch (err) {
console.error('error disposing websocket', err);
console.error('error disposing websocket', err)
}
});
})
}
});
})
} catch (err) {
console.error(`connection to ${name} language server failed`);
console.error(`connection to ${name} language server failed`)
}
}
connectToLanguageServer(`wss://${$page.url.host}/ws/pyright`, 'pyright', {
@@ -141,7 +141,7 @@
extraPaths: []
}
]
});
})
connectToLanguageServer(`wss://${$page.url.host}/ws/black`, 'black', {
formatters: {
@@ -153,21 +153,21 @@
formatFiletypes: {
python: 'black'
}
});
})
}
}
function closeWebsockets() {
websockets.forEach((x) => {
try {
x.close();
x.close()
} catch (err) {
console.log('error disposing websocket', err);
console.log('error disposing websocket', err)
}
});
})
}
async function loadMonaco() {
monaco = await import('monaco-editor');
monaco = await import('monaco-editor')
if (lang == 'python') {
monaco.languages.register({
@@ -175,21 +175,21 @@
extensions: ['.py'],
aliases: ['python'],
mimetypes: ['application/text']
});
})
}
let path: string = 'unknown';
let path: string = 'unknown'
if (lang == 'python') {
path = `${hash}.py`;
path = `${hash}.py`
} else if (lang == 'json') {
path = `${hash}.json`;
path = `${hash}.json`
} else if (lang == 'javascript') {
path = `${hash}.js`;
path = `${hash}.js`
} else if (lang == 'typescript') {
path = `${hash}.ts`;
path = `${hash}.ts`
}
const model = monaco.editor.createModel(code, lang, monaco.Uri.parse(`file:///${path}`));
model.updateOptions({ tabSize: 4, insertSpaces: true });
const model = monaco.editor.createModel(code, lang, monaco.Uri.parse(`file:///${path}`))
model.updateOptions({ tabSize: 4, insertSpaces: true })
editor = monaco.editor.create(divEl as HTMLDivElement, {
model: model,
value: code,
@@ -206,21 +206,21 @@
minimap: {
enabled: false
}
});
})
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, function () {
format();
});
format()
})
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, function () {
if (cmdEnterAction) {
cmdEnterAction();
cmdEnterAction()
}
});
})
editor.onDidChangeModelContent((event) => {
code = getCode();
});
code = getCode()
})
if (lang == 'json') {
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
@@ -228,7 +228,7 @@
allowComments: false,
schemas: [],
enableSchemaRequest: true
});
})
}
if (lang == 'typescript') {
@@ -237,7 +237,7 @@
target: monaco.languages.typescript.ScriptTarget.ES6,
allowNonTsExtensions: true,
noLib: true
});
})
monaco.languages.typescript.typescriptDefaults.addExtraLib(
`
@@ -277,39 +277,39 @@ export const previous_result: any;
export const params: any;
`,
'file:///node_modules/@types/windmill/index.d.ts'
);
)
}
if (lang == 'python') {
const { MonacoServices } = await import('@codingame/monaco-languageclient');
const { MonacoServices } = await import('@codingame/monaco-languageclient')
MonacoServices.install(monaco);
MonacoServices.install(monaco)
}
reloadWebsocket();
reloadWebsocket()
return () => {
if (editor) {
try {
editor.dispose();
editor.dispose()
} catch (err) {
console.log('error disposing editor', err);
console.log('error disposing editor', err)
}
}
};
}
}
onMount(() => {
if (browser) {
loadMonaco().then((x) => (disposeMethod = x));
loadMonaco().then((x) => (disposeMethod = x))
}
});
})
onDestroy(() => {
if (disposeMethod) {
disposeMethod();
disposeMethod()
}
});
})
</script>
<!-- <button class="default-button px-6 max-h-8" type="button" on:click={format}>

View File

@@ -1,12 +1,12 @@
<script lang="ts">
import Required from './Required.svelte';
import Required from './Required.svelte'
export let label: string;
export let format: string = '';
export let contentEncoding = '';
export let type: string | undefined = undefined;
export let required = false;
export let itemsType: { type?: 'string' | 'number' } | undefined = undefined;
export let label: string
export let format: string = ''
export let contentEncoding = ''
export let type: string | undefined = undefined
export let required = false
export let itemsType: { type?: 'string' | 'number' } | undefined = undefined
</script>
<h3>

View File

@@ -1,20 +1,20 @@
<script lang="ts">
import { type Flow, FlowService } from '../../gen';
import { type Flow, FlowService } from '../../gen'
import { sendUserToast } from '../../utils';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import Path from './Path.svelte';
import SvelteMarkdown from 'svelte-markdown';
import { workspaceStore } from '../../stores';
import ScriptSchema from './ScriptSchema.svelte';
import Required from './Required.svelte';
import FlowEditor from './FlowEditor.svelte';
import { sendUserToast } from '../../utils'
import { page } from '$app/stores'
import { goto } from '$app/navigation'
import Path from './Path.svelte'
import SvelteMarkdown from 'svelte-markdown'
import { workspaceStore } from '../../stores'
import ScriptSchema from './ScriptSchema.svelte'
import Required from './Required.svelte'
import FlowEditor from './FlowEditor.svelte'
export let flow: Flow;
export let initialPath: string = '';
export let flow: Flow
export let initialPath: string = ''
$: step = Number($page.url.searchParams.get('step')) || 1;
$: step = Number($page.url.searchParams.get('step')) || 1
async function saveFlow(): Promise<void> {
try {
@@ -28,7 +28,7 @@
value: flow.value,
schema: flow.schema
}
});
})
} else {
await FlowService.updateFlow({
workspace: $workspaceStore!,
@@ -40,16 +40,16 @@
value: flow.value,
schema: flow.schema
}
});
})
}
sendUserToast(`Success! flow saved at ${flow.path}`);
goto(`/flows/get/${flow.path}`);
sendUserToast(`Success! flow saved at ${flow.path}`)
goto(`/flows/get/${flow.path}`)
} catch (error) {
if (error.status === 400) {
sendUserToast(error.body, true);
sendUserToast(error.body, true)
} else {
sendUserToast(`Ooops.Something bad happened: ${error}`, true);
console.error(error);
sendUserToast(`Ooops.Something bad happened: ${error}`, true)
console.error(error)
}
}
}
@@ -59,12 +59,12 @@
}
async function changeStep(step: number) {
goto(`?step=${step}`);
goto(`?step=${step}`)
}
$: {
$page.url.searchParams.set('state', btoa(JSON.stringify(flow)));
history.replaceState({}, '', $page.url);
$page.url.searchParams.set('state', btoa(JSON.stringify(flow)))
history.replaceState({}, '', $page.url)
}
</script>
@@ -78,7 +78,7 @@
? 'default-button-disabled text-gray-700'
: 'default-button-secondary'} min-w-max ml-2"
on:click={() => {
changeStep(1);
changeStep(1)
}}>Step 1: Metadata</button
>
<button
@@ -86,7 +86,7 @@
? 'default-button-disabled text-gray-700'
: 'default-button-secondary'} min-w-max ml-2"
on:click={() => {
changeStep(2);
changeStep(2)
}}>Step 2: Flow</button
>
<button
@@ -94,7 +94,7 @@
? 'default-button-disabled text-gray-700'
: 'default-button-secondary'} min-w-max ml-2"
on:click={() => {
changeStep(3);
changeStep(3)
}}>Step 3: UI customisation</button
>
</div>
@@ -105,7 +105,7 @@
(flow.path == undefined || flow.path == '' || flow.path.split('/')[2] == '')}
class="default-button px-6 max-h-8"
on:click={() => {
changeStep(step + 1);
changeStep(step + 1)
}}>Next</button
>
{#if step == 2}

View File

@@ -1,22 +1,22 @@
<script lang="ts">
import { faPlus } from '@fortawesome/free-solid-svg-icons';
import type { Schema } from '../../common';
import { emptySchema } from '../../utils';
import { faPlus } from '@fortawesome/free-solid-svg-icons'
import type { Schema } from '../../common'
import { emptySchema } from '../../utils'
import Icon from 'svelte-awesome';
import Icon from 'svelte-awesome'
import { type Flow, FlowModuleValue, ScriptService } from '../../gen';
import SchemaEditor from './SchemaEditor.svelte';
import type SchemaForm from './SchemaForm.svelte';
import { workspaceStore } from '../../stores';
import ModuleStep from './ModuleStep.svelte';
import FlowPreview from './FlowPreview.svelte';
import { type Flow, FlowModuleValue, ScriptService } from '../../gen'
import SchemaEditor from './SchemaEditor.svelte'
import type SchemaForm from './SchemaForm.svelte'
import { workspaceStore } from '../../stores'
import ModuleStep from './ModuleStep.svelte'
import FlowPreview from './FlowPreview.svelte'
export let flow: Flow;
export let flow: Flow
let args: Record<string, any> = {};
let schemas: Schema[] = [];
let schemaForms: (SchemaForm | undefined)[] = [];
let args: Record<string, any> = {}
let schemas: Schema[] = []
let schemaForms: (SchemaForm | undefined)[] = []
export async function loadSchemas() {
await Promise.all(
@@ -25,47 +25,47 @@
const script = await ScriptService.getScriptByPath({
workspace: $workspaceStore!,
path: x.value.path ?? ''
});
})
if (
JSON.stringify(Object.keys(script.schema?.properties ?? {}).sort()) !=
JSON.stringify(Object.keys(x.input_transform).sort())
) {
let it = {};
let it = {}
Object.keys(script.schema?.properties ?? {}).map(
(x) =>
(it[x] = {
type: 'static',
value: ''
})
);
schemaForms[i]?.setArgs(it);
)
schemaForms[i]?.setArgs(it)
}
schemas[i] = script.schema ?? emptySchema();
schemas[i] = script.schema ?? emptySchema()
} else {
schemaForms[i]?.setArgs({});
schemas[i] = emptySchema();
schemaForms[i]?.setArgs({})
schemas[i] = emptySchema()
}
})
);
schemas = schemas;
)
schemas = schemas
if (flow.value.modules.length == 0) {
addModule();
addModule()
}
}
function addModule() {
schemaForms.push(undefined);
schemaForms.push(undefined)
let newModule = {
value: { type: FlowModuleValue.type.SCRIPT, path: '' },
input_transform: {}
};
flow.value.modules = flow.value.modules.concat(newModule);
schemas.push(emptySchema());
}
flow.value.modules = flow.value.modules.concat(newModule)
schemas.push(emptySchema())
}
$: $workspaceStore && loadSchemas();
$: $workspaceStore && loadSchemas()
</script>
<!-- <PageHeader title="Flow" /> -->
@@ -87,8 +87,8 @@
const script = await ScriptService.getScriptByPath({
workspace: $workspaceStore ?? '',
path: flow.value.modules[0].value.path ?? ''
});
flow.schema = script.schema;
})
flow.schema = script.schema
}}
>Copy from step 1's schema
</button>

View File

@@ -1,41 +1,41 @@
<script lang="ts">
import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons';
import { sendUserToast, truncateRev } from '../../utils';
import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'
import { sendUserToast, truncateRev } from '../../utils'
import Icon from 'svelte-awesome';
import Icon from 'svelte-awesome'
import { type Flow, Job, JobService, InputTransform } from '../../gen';
import { type Flow, Job, JobService, InputTransform } from '../../gen'
import { workspaceStore } from '../../stores';
import RunForm from './RunForm.svelte';
import FlowStatusViewer from './FlowStatusViewer.svelte';
import { onDestroy } from 'svelte';
import ChevronButton from './ChevronButton.svelte';
import DisplayResult from './DisplayResult.svelte';
import Tabs from './Tabs.svelte';
import type { Schema } from '../../common';
import { workspaceStore } from '../../stores'
import RunForm from './RunForm.svelte'
import FlowStatusViewer from './FlowStatusViewer.svelte'
import { onDestroy } from 'svelte'
import ChevronButton from './ChevronButton.svelte'
import DisplayResult from './DisplayResult.svelte'
import Tabs from './Tabs.svelte'
import type { Schema } from '../../common'
export let i: number;
export let flow: Flow;
export let schemas: Schema[] = [];
export let i: number
export let flow: Flow
export let schemas: Schema[] = []
export let args: Record<string, any> = {};
export let args: Record<string, any> = {}
let stepArgs: Record<string, any> = {};
let stepArgs: Record<string, any> = {}
let tab: 'upto' | 'justthis' = 'upto';
let viewPreview = false;
let intervalId: NodeJS.Timer;
let tab: 'upto' | 'justthis' = 'upto'
let viewPreview = false
let intervalId: NodeJS.Timer
let uptoText =
i == flow.value.modules.length - 1 ? 'Preview whole flow' : 'Preview up to this step';
let job: Job | undefined;
let jobs = [];
let jobId: string;
i == flow.value.modules.length - 1 ? 'Preview whole flow' : 'Preview up to this step'
let job: Job | undefined
let jobs = []
let jobId: string
async function runPreview(args) {
intervalId && clearInterval(intervalId);
const newFlow = tab == 'upto' ? truncateFlow(flow) : extractStep(flow);
intervalId && clearInterval(intervalId)
const newFlow = tab == 'upto' ? truncateFlow(flow) : extractStep(flow)
jobId = await JobService.runFlowPreview({
workspace: $workspaceStore ?? '',
requestBody: {
@@ -43,46 +43,46 @@
value: newFlow.value,
path: newFlow.path
}
});
jobs = [];
intervalId = setInterval(loadJob, 1000);
sendUserToast(`started preview ${truncateRev(jobId, 10)}`);
})
jobs = []
intervalId = setInterval(loadJob, 1000)
sendUserToast(`started preview ${truncateRev(jobId, 10)}`)
}
function truncateFlow(flow: Flow): Flow {
const localFlow = JSON.parse(JSON.stringify(flow));
localFlow.value.modules = flow.value.modules.slice(0, i + 1);
return localFlow;
const localFlow = JSON.parse(JSON.stringify(flow))
localFlow.value.modules = flow.value.modules.slice(0, i + 1)
return localFlow
}
function extractStep(flow: Flow): Flow {
const localFlow = JSON.parse(JSON.stringify(flow));
localFlow.value.modules = flow.value.modules.slice(i, i + 1);
localFlow.schema = schemas[i];
stepArgs = {};
const localFlow = JSON.parse(JSON.stringify(flow))
localFlow.value.modules = flow.value.modules.slice(i, i + 1)
localFlow.schema = schemas[i]
stepArgs = {}
Object.entries(flow.value.modules[i].input_transform).forEach((x) => {
if (x[1].type == InputTransform.type.STATIC) {
stepArgs[x[0]] = x[1].value;
stepArgs[x[0]] = x[1].value
}
});
return localFlow;
})
return localFlow
}
async function loadJob() {
try {
job = await JobService.getJob({ workspace: $workspaceStore!, id: jobId });
job = await JobService.getJob({ workspace: $workspaceStore!, id: jobId })
if (job?.type == 'CompletedJob') {
//only CompletedJob has success property
clearInterval(intervalId);
clearInterval(intervalId)
}
} catch (err) {
sendUserToast(err, true);
sendUserToast(err, true)
}
}
onDestroy(() => {
intervalId && clearInterval(intervalId);
});
intervalId && clearInterval(intervalId)
})
</script>
<h2 class="mb-5 mt-2">
@@ -90,7 +90,7 @@
type="submit"
class="underline text-gray-700 inline-flex items-center"
on:click={() => {
viewPreview = !viewPreview;
viewPreview = !viewPreview
}}
>
<div>

View File

@@ -1,24 +1,18 @@
<script lang="ts">
import { faHourglassHalf, faSpinner, faTimes } from '@fortawesome/free-solid-svg-icons';
import { truncateRev } from '../../utils';
import { faHourglassHalf, faSpinner, faTimes } from '@fortawesome/free-solid-svg-icons'
import { truncateRev } from '../../utils'
import Icon from 'svelte-awesome';
import { check } from 'svelte-awesome/icons';
import Icon from 'svelte-awesome'
import { check } from 'svelte-awesome/icons'
import {
CompletedJob,
FlowModuleValue,
FlowStatusModule,
JobService,
QueuedJob
} from '../../gen';
import { workspaceStore } from '../../stores';
import DisplayResult from './DisplayResult.svelte';
import ChevronButton from './ChevronButton.svelte';
import JobStatus from './JobStatus.svelte';
import { CompletedJob, FlowModuleValue, FlowStatusModule, JobService, QueuedJob } from '../../gen'
import { workspaceStore } from '../../stores'
import DisplayResult from './DisplayResult.svelte'
import ChevronButton from './ChevronButton.svelte'
import JobStatus from './JobStatus.svelte'
export let job: QueuedJob | CompletedJob;
export let jobs: (CompletedJob | undefined)[];
export let job: QueuedJob | CompletedJob
export let jobs: (CompletedJob | undefined)[]
function loadResults() {
job?.flow_status?.modules?.forEach(async (x, i) => {
@@ -26,13 +20,13 @@
(i >= jobs.length && x.type == FlowStatusModule.type.SUCCESS) ||
x.type == FlowStatusModule.type.FAILURE
) {
jobs.push(undefined);
jobs[i] = await JobService.getCompletedJob({ workspace: $workspaceStore!, id: x.job! });
jobs.push(undefined)
jobs[i] = await JobService.getCompletedJob({ workspace: $workspaceStore!, id: x.job! })
}
});
})
}
$: $workspaceStore && job && loadResults();
$: $workspaceStore && job && loadResults()
</script>
<div class="flow-root max-w-lg w-full p-4">

View File

@@ -1,26 +1,26 @@
<script lang="ts">
import { workspaceStore } from '../../stores';
import { workspaceStore } from '../../stores'
import { createEventDispatcher } from 'svelte';
import { createEventDispatcher } from 'svelte'
import Modal from '../../routes/components/Modal.svelte';
import { Group, GroupService, UserService } from '../../gen';
import AutoComplete from 'simple-svelte-autocomplete';
import PageHeader from './PageHeader.svelte';
import TableCustom from './TableCustom.svelte';
import { canWrite, getUser } from '../../utils';
import Modal from '../../routes/components/Modal.svelte'
import { Group, GroupService, UserService } from '../../gen'
import AutoComplete from 'simple-svelte-autocomplete'
import PageHeader from './PageHeader.svelte'
import TableCustom from './TableCustom.svelte'
import { canWrite, getUser } from '../../utils'
let name = '';
let modal: Modal;
let can_write = false;
let name = ''
let modal: Modal
let can_write = false
let group: Group | undefined;
let members: { name: string; isAdmin: boolean }[] = [];
let usernames: string[] = [];
let username: string = '';
let group: Group | undefined
let members: { name: string; isAdmin: boolean }[] = []
let usernames: string[] = []
let username: string = ''
async function loadUsernames(): Promise<void> {
usernames = await UserService.listUsernames({ workspace: $workspaceStore! });
usernames = await UserService.listUsernames({ workspace: $workspaceStore! })
}
$: {
@@ -29,15 +29,15 @@
return {
name: x,
isAdmin: x in (group?.extra_perms ?? {}) && (group?.extra_perms ?? {})[name]
};
});
}
})
}
}
export function openModal(newName: string): void {
name = newName;
loadGroup();
loadUsernames();
modal.openModal();
name = newName
loadGroup()
loadUsernames()
modal.openModal()
}
async function addToGroup() {
@@ -45,14 +45,14 @@
workspace: $workspaceStore ?? '',
name,
requestBody: { username }
});
loadGroup();
})
loadGroup()
}
async function loadGroup(): Promise<void> {
group = await GroupService.getGroup({ workspace: $workspaceStore!, name });
const user = await getUser($workspaceStore!);
can_write = canWrite(group.name!, group.extra_perms ?? {}, user);
group = await GroupService.getGroup({ workspace: $workspaceStore!, name })
const user = await getUser($workspaceStore!)
can_write = canWrite(group.name!, group.extra_perms ?? {}, user)
}
</script>
@@ -88,8 +88,8 @@
workspace: $workspaceStore ?? '',
name: group?.name ?? '',
requestBody: { username: name }
});
loadGroup();
})
loadGroup()
}}>remove</button
>
{/if}</td

View File

@@ -1,16 +1,16 @@
<script lang="ts">
import Mysql from './icons/Mysql.svelte';
import Mail from './icons/Mail.svelte';
import DbIcon from './icons/DbIcon.svelte';
import PostgresIcon from './icons/PostgresIcon.svelte';
import Icon from 'svelte-awesome';
import { faSlack } from '@fortawesome/free-brands-svg-icons';
import Slack from './icons/Slack.svelte';
import Mysql from './icons/Mysql.svelte'
import Mail from './icons/Mail.svelte'
import DbIcon from './icons/DbIcon.svelte'
import PostgresIcon from './icons/PostgresIcon.svelte'
import Icon from 'svelte-awesome'
import { faSlack } from '@fortawesome/free-brands-svg-icons'
import Slack from './icons/Slack.svelte'
export let name: string;
export let after: boolean = false;
export let height = '24px';
export let width = '24px';
export let name: string
export let after: boolean = false
export let height = '24px'
export let width = '24px'
</script>
<div class="flex flex-row gap-2">

View File

@@ -0,0 +1,66 @@
<script lang="ts">
import { sendUserToast } from '../../utils'
import Switch from '../components/Switch.svelte'
import type Modal from './Modal.svelte'
import { createEventDispatcher } from 'svelte'
import { UserService } from '../../gen'
const dispatch = createEventDispatcher()
let valid = true
let modal: Modal
export function openModal(): void {
modal.openModal()
}
let email: string
let is_super_admin = false
let password: string
let name: string | undefined
let company: string | undefined
function handleKeyUp(event: KeyboardEvent) {
const key = event.key || event.keyCode
if (key === 13 || key === 'Enter') {
event.preventDefault()
addUser()
}
}
async function addUser() {
await UserService.createUserGlobally({
requestBody: {
email,
password,
super_admin: is_super_admin,
name,
company
}
})
sendUserToast(`Successfully added ${email}. Welcome to them!`)
dispatch('new')
}
</script>
<div class="flex flex-row">
<input on:keyup={handleKeyUp} placeholder="email" bind:value={email} />
<Switch class="ml-2" bind:checked={is_super_admin} horizontal={true} label={'admin: '} />
<input on:keyup={handleKeyUp} type="password" placeholder="" bind:value={password} />
<input on:keyup={handleKeyUp} placeholder="name" bind:value={name} />
<input on:keyup={handleKeyUp} placeholder="company" bind:value={company} />
<button
class="ml-4 w-40 {valid ? 'default-button' : 'default-button-disabled'}"
type="button"
on:click={() => {
addUser()
}}
disabled={email == undefined || password == undefined}
>
Add
</button>
</div>

View File

@@ -1,30 +1,30 @@
<script lang="ts">
import { sendUserToast } from '../../utils';
import Switch from '../components/Switch.svelte';
import { sendUserToast } from '../../utils'
import Switch from '../components/Switch.svelte'
import Modal from './Modal.svelte';
import { createEventDispatcher } from 'svelte';
import { workspaceStore } from '../../stores';
import { WorkspaceService } from '../../gen';
import type Modal from './Modal.svelte'
import { createEventDispatcher } from 'svelte'
import { workspaceStore } from '../../stores'
import { WorkspaceService } from '../../gen'
const dispatch = createEventDispatcher();
const dispatch = createEventDispatcher()
let valid = true;
let valid = true
let modal: Modal;
let modal: Modal
export function openModal(): void {
modal.openModal();
modal.openModal()
}
let email: string;
let is_admin = false;
let email: string
let is_admin = false
function handleKeyUp(event: KeyboardEvent) {
const key = event.key || event.keyCode;
const key = event.key || event.keyCode
if (key === 13 || key === 'Enter') {
event.preventDefault();
inviteUser();
event.preventDefault()
inviteUser()
}
}
@@ -35,9 +35,9 @@
email,
is_admin
}
});
sendUserToast(`Successfully invited ${email}. Welcome to them!`);
dispatch('new');
})
sendUserToast(`Successfully invited ${email}. Welcome to them!`)
dispatch('new')
}
</script>
@@ -49,7 +49,7 @@
class="ml-4 w-40 {valid ? 'default-button' : 'default-button-disabled'}"
type="button"
on:click={() => {
inviteUser();
inviteUser()
}}
disabled={email == undefined}
>

View File

@@ -1,37 +1,37 @@
<script lang="ts">
import Fuse from 'fuse.js';
import Fuse from 'fuse.js'
import Modal from './Modal.svelte';
import Modal from './Modal.svelte'
type Item = Record<string, any>;
export let pickCallback: (path: string, f: string) => void;
export let loadItems: () => Promise<Item[]>;
export let extraField: string;
export let itemName: string;
export let closeOnClick = true;
type Item = Record<string, any>
export let pickCallback: (path: string, f: string) => void
export let loadItems: () => Promise<Item[]>
export let extraField: string
export let itemName: string
export let closeOnClick = true
let items: Item[] = [];
let filteredItems: Item[] = [];
let itemsFilter = '';
let items: Item[] = []
let filteredItems: Item[] = []
let itemsFilter = ''
const fuseOptions = {
includeScore: false,
keys: ['path', extraField]
};
const fuse: Fuse<Item> = new Fuse(items, fuseOptions);
}
const fuse: Fuse<Item> = new Fuse(items, fuseOptions)
export function openModal() {
loadItems().then((v) => {
items = v;
fuse.setCollection(items);
});
modal.openModal();
items = v
fuse.setCollection(items)
})
modal.openModal()
}
$: filteredItems =
itemsFilter.length > 0 ? fuse.search(itemsFilter).map((value) => value.item) : items;
itemsFilter.length > 0 ? fuse.search(itemsFilter).map((value) => value.item) : items
let modal: Modal;
let modal: Modal
</script>
<Modal bind:this={modal} z="z-30">
@@ -46,9 +46,9 @@
class="py-4 px-1 gap-1 flex flex-col hover:bg-white hover:border text-black cursor-pointer"
on:click={() => {
if (closeOnClick) {
modal.closeModal();
modal.closeModal()
}
pickCallback(obj['path'], obj[extraField]);
pickCallback(obj['path'], obj[extraField])
}}
>
<p class="text-sm font-semibold">

View File

@@ -5,17 +5,17 @@
faClock,
faHourglassHalf,
faTimes
} from '@fortawesome/free-solid-svg-icons';
import { displayDate, forLater } from '../../utils';
} from '@fortawesome/free-solid-svg-icons'
import { displayDate, forLater } from '../../utils'
import Icon from 'svelte-awesome';
import { check } from 'svelte-awesome/icons';
import Icon from 'svelte-awesome'
import { check } from 'svelte-awesome/icons'
import type { CompletedJob, QueuedJob } from '../../gen';
import type { CompletedJob, QueuedJob } from '../../gen'
const SMALL_ICON_SCALE = 0.7;
const SMALL_ICON_SCALE = 0.7
export let job: QueuedJob | CompletedJob | undefined;
export let job: QueuedJob | CompletedJob | undefined
</script>
{#if job && 'success' in job && job.success}

View File

@@ -1,27 +1,27 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { createEventDispatcher } from 'svelte'
export let open: boolean = false;
export let z = 'z-30';
export let open: boolean = false
export let z = 'z-30'
const dispatch = createEventDispatcher();
const dispatch = createEventDispatcher()
export function closeModal(): void {
open = false;
dispatch('close');
open = false
dispatch('close')
}
export function openModal(): void {
open = true;
dispatch('open');
open = true
dispatch('open')
}
function handleKeyUp(event: KeyboardEvent): void {
const key = event.key || event.keyCode;
const key = event.key || event.keyCode
if (key === 27 || key === 'Escape' || key === 'Esc') {
if (open) {
event.preventDefault();
closeModal();
event.preventDefault()
closeModal()
}
}
}
@@ -39,8 +39,8 @@
<div class="flex flex-row justify-between p-2 bg-white border-b border-gray-200">
<button
on:click={() => {
open = false;
closeModal();
open = false
closeModal()
}}
>
<svg
@@ -68,7 +68,7 @@
<div class="flex flex-row justify-between p-2 ">
<button
on:click={() => {
closeModal();
closeModal()
}}
>
<svg

View File

@@ -1,49 +1,49 @@
<script lang="ts">
import { workspaceStore } from '../../stores';
import { workspaceStore } from '../../stores'
import type { Schema } from '../../common';
import { ScriptService, type Flow, type FlowModule } from '../../gen';
import type { Schema } from '../../common'
import { ScriptService, type Flow, type FlowModule } from '../../gen'
import SchemaForm from './SchemaForm.svelte';
import ScriptPicker from './ScriptPicker.svelte';
import { emptySchema } from '../../utils';
import FlowPreview from './FlowPreview.svelte';
import SchemaForm from './SchemaForm.svelte'
import ScriptPicker from './ScriptPicker.svelte'
import { emptySchema } from '../../utils'
import FlowPreview from './FlowPreview.svelte'
export let flow: Flow;
export let i: number;
export let mod: FlowModule;
export let args: Record<string, any> = {};
export let flow: Flow
export let i: number
export let mod: FlowModule
export let args: Record<string, any> = {}
export let schemas: Schema[] = [];
export let schemaForms: (SchemaForm | undefined)[] = [];
export let schemas: Schema[] = []
export let schemaForms: (SchemaForm | undefined)[] = []
export async function loadSchema() {
if (mod.value.path) {
const script = await ScriptService.getScriptByPath({
workspace: $workspaceStore!,
path: mod.value.path ?? ''
});
})
if (
JSON.stringify(Object.keys(script.schema?.properties ?? {}).sort()) !=
JSON.stringify(Object.keys(mod.input_transform).sort())
) {
let it = {};
let it = {}
Object.keys(script.schema?.properties ?? {}).map(
(x) =>
(it[x] = {
type: 'static',
value: ''
})
);
schemaForms[i]?.setArgs(it);
)
schemaForms[i]?.setArgs(it)
}
schemas[i] = script.schema ?? emptySchema();
schemas[i] = script.schema ?? emptySchema()
} else {
schemaForms[i]?.setArgs({});
schemas[i] = emptySchema();
schemaForms[i]?.setArgs({})
schemas[i] = emptySchema()
}
schemas = schemas;
schemas = schemas
}
</script>
@@ -56,10 +56,10 @@
<button
class="text-xs default-button-secondary max-h-6 place-self-end"
on:click={() => {
flow.value.modules.splice(i, 1);
schemas.splice(i, 1);
schemaForms.splice(i, 1);
flow = flow;
flow.value.modules.splice(i, 1)
schemas.splice(i, 1)
schemaForms.splice(i, 1)
flow = flow
}}
>Remove this step
</button>

View File

@@ -1,11 +1,11 @@
<script>
// @ts-nocheck
import { onMount } from 'svelte';
import { fly } from 'svelte/transition';
export let id = '';
export let value = [];
export let readonly = false;
export let placeholder = '';
import { onMount } from 'svelte'
import { fly } from 'svelte/transition'
export let id = ''
export let value = []
export let readonly = false
export let placeholder = ''
let input,
inputValue,
@@ -14,51 +14,51 @@
showOptions = false,
selected = {},
first = true,
slot;
slot
const iconClearPath =
'M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z';
'M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'
onMount(() => {
slot.querySelectorAll('option').forEach((o) => {
o.selected && !value.includes(o.value) && (value = [...value, o.value]);
options = [...options, { value: o.value, name: o.textContent }];
});
o.selected && !value.includes(o.value) && (value = [...value, o.value])
options = [...options, { value: o.value, name: o.textContent }]
})
value &&
(selected = options.reduce(
(obj, op) => (value.includes(op.value) ? { ...obj, [op.value]: op } : obj),
{}
));
first = false;
});
))
first = false
})
$: if (!first) value = Object.values(selected).map((o) => o.value);
$: if (!first) value = Object.values(selected).map((o) => o.value)
$: filtered = options.filter((o) =>
inputValue ? o.name.toLowerCase().includes(inputValue.toLowerCase()) : o
);
)
$: if ((activeOption && !filtered.includes(activeOption)) || (!activeOption && inputValue))
activeOption = filtered[0];
activeOption = filtered[0]
function add(token) {
if (!readonly) selected[token.value] = token;
if (!readonly) selected[token.value] = token
}
function remove(value) {
if (!readonly) {
const { [value]: val, ...rest } = selected;
selected = rest;
const { [value]: val, ...rest } = selected
selected = rest
}
}
function optionsVisibility(show) {
if (readonly) return;
if (readonly) return
if (typeof show === 'boolean') {
showOptions = show;
show && input.focus();
showOptions = show
show && input.focus()
} else {
showOptions = !showOptions;
showOptions = !showOptions
}
if (!showOptions) {
activeOption = undefined;
activeOption = undefined
}
}
@@ -66,45 +66,45 @@
if (e.keyCode === 13) {
Object.keys(selected).includes(activeOption.value)
? remove(activeOption.value)
: add(activeOption);
inputValue = '';
: add(activeOption)
inputValue = ''
}
if ([38, 40].includes(e.keyCode)) {
// up and down arrows
const increment = e.keyCode === 38 ? -1 : 1;
const calcIndex = filtered.indexOf(activeOption) + increment;
const increment = e.keyCode === 38 ? -1 : 1
const calcIndex = filtered.indexOf(activeOption) + increment
activeOption =
calcIndex < 0
? filtered[filtered.length - 1]
: calcIndex === filtered.length
? filtered[0]
: filtered[calcIndex];
: filtered[calcIndex]
}
}
function handleBlur(e) {
optionsVisibility(false);
optionsVisibility(false)
}
function handleTokenClick(e) {
if (e.target.closest('.token-remove')) {
e.stopPropagation();
remove(e.target.closest('.token').dataset.id);
e.stopPropagation()
remove(e.target.closest('.token').dataset.id)
} else if (e.target.closest('.remove-all')) {
selected = [];
inputValue = '';
selected = []
inputValue = ''
} else {
optionsVisibility(true);
optionsVisibility(true)
}
}
function handleOptionMousedown(e) {
const value = e.target.dataset.value;
const value = e.target.dataset.value
if (selected[value]) {
remove(value);
remove(value)
} else {
add(options.filter((o) => o.value === value)[0]);
input.focus();
add(options.filter((o) => o.value === value)[0])
input.focus()
}
}
</script>

View File

@@ -1,48 +1,48 @@
<script lang="ts">
import { ResourceService } from '../../gen';
import { ResourceService } from '../../gen'
import ResourcePicker from './ResourcePicker.svelte';
import { workspaceStore } from '../../stores';
import SchemaForm from './SchemaForm.svelte';
import RadioButton from './RadioButton.svelte';
import ResourcePicker from './ResourcePicker.svelte'
import { workspaceStore } from '../../stores'
import SchemaForm from './SchemaForm.svelte'
import RadioButton from './RadioButton.svelte'
export let format: string;
export let value: any;
export let format: string
export let value: any
function isString(value: any) {
return typeof value === 'string' || value instanceof String;
return typeof value === 'string' || value instanceof String
}
let path: string =
isString(value) && value.length >= '$res:'.length ? value.substr('$res:'.length) : undefined;
let args: Record<string, any> = {};
isString(value) && value.length >= '$res:'.length ? value.substr('$res:'.length) : undefined
let args: Record<string, any> = {}
if (!isString(value) && value) {
console.log(value);
args = value;
console.log(value)
args = value
}
let schema: any | undefined = undefined;
let isValid = true;
let resourceTypeName: string = '';
let schema: any | undefined = undefined
let isValid = true
let resourceTypeName: string = ''
async function loadSchema(format: string) {
resourceTypeName = format.substring('resource-'.length);
resourceTypeName = format.substring('resource-'.length)
schema = (
await ResourceService.getResourceType({ workspace: $workspaceStore!, path: resourceTypeName })
).schema;
).schema
}
let option: 'resource' | 'raw' = isString(value) || value == undefined ? 'resource' : 'raw';
let option: 'resource' | 'raw' = isString(value) || value == undefined ? 'resource' : 'raw'
$: {
if (option == 'resource') {
value = `$res:${path}`;
value = `$res:${path}`
} else {
value = args;
value = args
}
}
$: format.startsWith('resource-') && loadSchema(format);
$: format.startsWith('resource-') && loadSchema(format)
</script>
<div class="max-w-lg">

View File

@@ -1,17 +1,17 @@
<script lang="ts">
import RadioButton from './RadioButton.svelte';
import ResourceTypePicker from './ResourceTypePicker.svelte';
import RadioButton from './RadioButton.svelte'
import ResourceTypePicker from './ResourceTypePicker.svelte'
export let format: string | undefined;
export let format: string | undefined
let kind: 'resource' | 'none' = format?.startsWith('resource') ? 'resource' : 'none';
let kind: 'resource' | 'none' = format?.startsWith('resource') ? 'resource' : 'none'
let resource: string | undefined = format?.startsWith('resource-')
? format.substring('resource-'.length)
: undefined;
: undefined
$: format =
kind == 'resource' ? (resource != undefined ? `resource-${resource}` : 'resource') : undefined;
kind == 'resource' ? (resource != undefined ? `resource-${resource}` : 'resource') : undefined
</script>
<RadioButton

View File

@@ -1,9 +1,9 @@
<script lang="ts">
import Tooltip from './Tooltip.svelte';
import Tooltip from './Tooltip.svelte'
export let title: string;
export let tooltip: string = '';
export let primary: boolean = true;
export let title: string
export let tooltip: string = ''
export let primary: boolean = true
</script>
<div class="flex flex-col sm:flex-row justify-between mt-4 mb-2">

View File

@@ -1,31 +1,31 @@
<script lang="ts">
// @ts-nocheck
import { onMount } from 'svelte';
export let password: string;
export let label = 'password';
export let placeholder = '******';
import { onMount } from 'svelte'
export let password: string
export let label = 'password'
export let placeholder = '******'
onMount(() => {
const passwordToggle = document.querySelector('.js-password-toggle');
const passwordToggle = document.querySelector('.js-password-toggle')
if (passwordToggle) {
passwordToggle.addEventListener('change', function () {
const password = document.querySelector('.js-password'),
passwordLabel = document.querySelector('.js-password-label');
passwordLabel = document.querySelector('.js-password-label')
if (password.type === 'password') {
password.type = 'text';
passwordLabel.innerHTML = 'hide';
password.type = 'text'
passwordLabel.innerHTML = 'hide'
} else {
password.type = 'password';
passwordLabel.innerHTML = 'show';
password.type = 'password'
passwordLabel.innerHTML = 'show'
}
password.focus();
});
password.focus()
})
} else {
throw Error('Password component is undefined');
throw Error('Password component is undefined')
}
});
})
</script>
<label class="block text-gray-700" for="password"> {label} </label>

View File

@@ -1,75 +1,75 @@
<script lang="ts">
import { type Meta, pathToMeta } from '../../common';
import { type Meta, pathToMeta } from '../../common'
import type { Group } from '../../gen';
import { GroupService } from '../../gen';
import Tooltip from './Tooltip.svelte';
import { userStore, workspaceStore } from '../../stores';
import { sleep } from '../../utils';
import type { Group } from '../../gen'
import { GroupService } from '../../gen'
import Tooltip from './Tooltip.svelte'
import { userStore, workspaceStore } from '../../stores'
import { sleep } from '../../utils'
export let meta: Meta = {
ownerKind: 'user',
owner: '',
name: ''
};
export let namePlaceholder = '';
export let initialPath: string;
export let path = '';
}
export let namePlaceholder = ''
export let initialPath: string
export let path = ''
let groups: Group[] = [];
let error = '';
let groups: Group[] = []
let error = ''
$: {
path = [meta.ownerKind === 'group' ? 'g' : 'u', meta.owner, meta.name].join('/');
path = [meta.ownerKind === 'group' ? 'g' : 'u', meta.owner, meta.name].join('/')
}
export function getPath() {
return path;
return path
}
export async function reset() {
if (path == '' || path == 'u//') {
meta.ownerKind = 'user';
meta.ownerKind = 'user'
while ($userStore == undefined) {
await sleep(500);
await sleep(500)
}
meta.owner = $userStore!.username;
meta.name = '';
meta.owner = $userStore!.username
meta.name = ''
} else {
meta = pathToMeta(path);
meta = pathToMeta(path)
}
}
$: validateName(meta);
$: validateName(meta)
async function loadGroups(): Promise<void> {
groups = await GroupService.listGroups({ workspace: $workspaceStore! });
groups = await GroupService.listGroups({ workspace: $workspaceStore! })
}
function validateName(meta: Meta): void {
if (meta.name == undefined || meta.name == '') {
error = 'choose a name';
return;
error = 'choose a name'
return
}
const regex = new RegExp(/^[\w-]+(\/[\w-]+)*$/);
const regex = new RegExp(/^[\w-]+(\/[\w-]+)*$/)
if (regex.test(meta.name)) {
error = '';
error = ''
} else {
error = 'This name is not valid. ';
error = 'This name is not valid. '
}
}
$: {
if ($workspaceStore) {
loadGroups();
loadGroups()
}
}
$: {
if (initialPath == undefined || initialPath == '') {
reset();
reset()
} else {
meta = pathToMeta(initialPath);
meta = pathToMeta(initialPath)
}
}
</script>
@@ -87,9 +87,9 @@
bind:value={meta.ownerKind}
on:change={() => {
if (meta.ownerKind === 'group') {
meta.owner = 'all';
meta.owner = 'all'
} else {
meta.owner = $userStore?.username ?? '';
meta.owner = $userStore?.username ?? ''
}
}}
>

View File

@@ -1,12 +1,12 @@
<script lang="ts">
export let label = '';
export let options: [string, any][];
export let value: any;
export let small = false;
export let label = ''
export let options: [string, any][]
export let value: any
export let small = false
import { createEventDispatcher } from 'svelte';
import { createEventDispatcher } from 'svelte'
const dispatch = createEventDispatcher();
const dispatch = createEventDispatcher()
</script>
<fieldset class="mt-2 mr-4">

View File

@@ -1,6 +1,6 @@
<script lang="ts">
export let required: boolean;
export let detail = '';
export let required: boolean
export let detail = ''
</script>
{#if required}

View File

@@ -4,67 +4,67 @@
ResourceService,
type ResourceType,
VariableService
} from '../../../src/gen';
import { allTrue, emptySchema, sendUserToast } from '../../../src/utils';
import { createEventDispatcher } from 'svelte';
import type { Schema } from '../../common';
import Modal from './Modal.svelte';
import Path from './Path.svelte';
import ArgInput from './ArgInput.svelte';
import AutosizedTextarea from './AutosizedTextarea.svelte';
import ItemPicker from './ItemPicker.svelte';
import VariableEditor from './VariableEditor.svelte';
import Required from './Required.svelte';
} from '../../../src/gen'
import { allTrue, emptySchema, sendUserToast } from '../../../src/utils'
import { createEventDispatcher } from 'svelte'
import type { Schema } from '../../common'
import Modal from './Modal.svelte'
import Path from './Path.svelte'
import ArgInput from './ArgInput.svelte'
import AutosizedTextarea from './AutosizedTextarea.svelte'
import ItemPicker from './ItemPicker.svelte'
import VariableEditor from './VariableEditor.svelte'
import Required from './Required.svelte'
import { workspaceStore } from '../../stores';
import ResourceTypePicker from './ResourceTypePicker.svelte';
import { workspaceStore } from '../../stores'
import ResourceTypePicker from './ResourceTypePicker.svelte'
let path = '';
let initialPath = '';
let path = ''
let initialPath = ''
let step = 1;
let step = 1
let resourceToEdit: Resource | undefined;
let resourceToEdit: Resource | undefined
let description: string = '';
let description: string = ''
let DESCRIPTION_PLACEHOLDER = `You can use markdown to style your description.
A good way to make resources user friendly is to link to a default script for your resource [example](scripts/add?template=f2d1dc8df796d9e8)`;
let selectedResourceType: string | undefined;
let resourceType: ResourceType;
let resourceSchema: Schema | undefined;
let args: Record<string, any> = {};
A good way to make resources user friendly is to link to a default script for your resource [example](scripts/add?template=f2d1dc8df796d9e8)`
let selectedResourceType: string | undefined
let resourceType: ResourceType
let resourceSchema: Schema | undefined
let args: Record<string, any> = {}
let error: string | undefined;
let error: string | undefined
let pickForField: string | undefined;
let itemPicker: ItemPicker;
let variableEditor: VariableEditor;
let modal: Modal;
let pickForField: string | undefined
let itemPicker: ItemPicker
let variableEditor: VariableEditor
let modal: Modal
const dispatch = createEventDispatcher();
const dispatch = createEventDispatcher()
export async function initNew() {
selectedResourceType = undefined;
step = 1;
args = {};
path = '';
description = '';
initialPath = '';
resourceSchema = emptySchema();
resourceToEdit = undefined;
modal.openModal();
selectedResourceType = undefined
step = 1
args = {}
path = ''
description = ''
initialPath = ''
resourceSchema = emptySchema()
resourceToEdit = undefined
modal.openModal()
}
export async function initEdit(p: string): Promise<void> {
initialPath = p;
path = p;
step = 2;
resourceToEdit = await ResourceService.getResource({ workspace: $workspaceStore!, path: p });
description = resourceToEdit!.description ?? '';
selectedResourceType = resourceToEdit!.resource_type;
await loadResourceType();
args = resourceToEdit!.value;
modal.openModal();
initialPath = p
path = p
step = 2
resourceToEdit = await ResourceService.getResource({ workspace: $workspaceStore!, path: p })
description = resourceToEdit!.description ?? ''
selectedResourceType = resourceToEdit!.resource_type
await loadResourceType()
args = resourceToEdit!.value
modal.openModal()
}
async function createResource(): Promise<void> {
@@ -72,13 +72,13 @@ A good way to make resources user friendly is to link to a default script for yo
await ResourceService.createResource({
workspace: $workspaceStore!,
requestBody: { path, value: args, description, resource_type: resourceType.name }
});
sendUserToast(`Successfully created resource at ${path}`);
})
sendUserToast(`Successfully created resource at ${path}`)
dispatch('refresh');
modal.closeModal();
dispatch('refresh')
modal.closeModal()
} catch (err) {
sendUserToast(`${err}`, true);
sendUserToast(`${err}`, true)
}
}
@@ -89,15 +89,15 @@ A good way to make resources user friendly is to link to a default script for yo
workspace: $workspaceStore!,
path: resourceToEdit.path,
requestBody: { path, value: args, description }
});
sendUserToast(`Successfully updated resource at ${path}`);
dispatch('refresh');
modal.closeModal();
})
sendUserToast(`Successfully updated resource at ${path}`)
dispatch('refresh')
modal.closeModal()
} else {
throw Error('Cannot edit undefined resourceToEdit');
throw Error('Cannot edit undefined resourceToEdit')
}
} catch (err) {
sendUserToast(`${err}`, true);
sendUserToast(`${err}`, true)
}
}
@@ -106,25 +106,25 @@ A good way to make resources user friendly is to link to a default script for yo
resourceType = await ResourceService.getResourceType({
workspace: $workspaceStore!,
path: selectedResourceType
});
})
if (resourceType.schema) {
resourceSchema = resourceType.schema as Schema;
resourceSchema = resourceType.schema as Schema
}
} else {
sendUserToast(`ResourceType cannot be undefined.`, true);
sendUserToast(`ResourceType cannot be undefined.`, true)
}
}
let inputCheck: { [id: string]: boolean } = {};
let inputCheck: { [id: string]: boolean } = {}
$: isValid = allTrue(inputCheck) ?? false;
$: isValid = allTrue(inputCheck) ?? false
</script>
<Modal
bind:this={modal}
on:close={() => {
dispatch('close');
dispatch('close')
}}
>
<div slot="title">{resourceToEdit ? 'Edit ' + resourceToEdit.path : 'Add a resource'}</div>
@@ -161,7 +161,7 @@ A good way to make resources user friendly is to link to a default script for yo
bind:value={selectedResourceType}
notPickable={resourceToEdit != undefined}
on:click={() => {
args = {};
args = {}
}}
/>
</div>
@@ -189,8 +189,8 @@ A good way to make resources user friendly is to link to a default script for yo
<button
class="default-button-secondary min-w-min items-center leading-4 py-0"
on:click={() => {
pickForField = fieldName;
itemPicker.openModal();
pickForField = fieldName
itemPicker.openModal()
}}>insert variable</button
>
</div>
@@ -208,8 +208,8 @@ A good way to make resources user friendly is to link to a default script for yo
<button
class="default-button px-4 py-2 font-semibold"
on:click={async () => {
await loadResourceType();
step = 2;
await loadResourceType()
step = 2
}}
>
Next
@@ -221,7 +221,7 @@ A good way to make resources user friendly is to link to a default script for yo
<button
class="default-button-secondary px-4 py-2 font-semibold"
on:click={() => {
step = 1;
step = 1
}}
>
Back
@@ -231,9 +231,9 @@ A good way to make resources user friendly is to link to a default script for yo
class="default-button px-4 py-2 font-semibold"
on:click={() => {
if (resourceToEdit) {
editResource();
editResource()
} else {
createResource();
createResource()
}
}}
>
@@ -247,7 +247,7 @@ A good way to make resources user friendly is to link to a default script for yo
bind:this={itemPicker}
pickCallback={(path, _) => {
if (pickForField) {
args[pickForField] = '$var:' + path;
args[pickForField] = '$var:' + path
}
}}
itemName="Variable"
@@ -266,7 +266,7 @@ A good way to make resources user friendly is to link to a default script for yo
class="default-button-secondary"
type="button"
on:click={() => {
variableEditor.initNew();
variableEditor.initNew()
}}
>
Create a new variable

View File

@@ -1,20 +1,20 @@
<script lang="ts">
import { type Resource, ResourceService } from '../../gen';
import { workspaceStore } from '../../stores';
import { type Resource, ResourceService } from '../../gen'
import { workspaceStore } from '../../stores'
let resources: Resource[] = [];
let resources: Resource[] = []
export let value: string | undefined;
export let value: string | undefined
export let resourceType: string | undefined;
export let resourceType: string | undefined
async function loadResources(resourceType: string | undefined) {
resources = await ResourceService.listResource({ workspace: $workspaceStore!, resourceType });
resources = await ResourceService.listResource({ workspace: $workspaceStore!, resourceType })
}
$: {
if ($workspaceStore) {
loadResources(resourceType);
loadResources(resourceType)
}
}
</script>

View File

@@ -1,25 +1,25 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { createEventDispatcher } from 'svelte'
import { ResourceService } from '../../gen';
import { workspaceStore } from '../../stores';
import IconedResourceType from './IconedResourceType.svelte';
import { ResourceService } from '../../gen'
import { workspaceStore } from '../../stores'
import IconedResourceType from './IconedResourceType.svelte'
let resources: string[] = [];
let resources: string[] = []
export let value: string | undefined;
export let value: string | undefined
export let notPickable = false;
export let notPickable = false
async function loadResources() {
resources = await ResourceService.listResourceTypeNames({ workspace: $workspaceStore! });
resources = await ResourceService.listResourceTypeNames({ workspace: $workspaceStore! })
}
const dispatch = createEventDispatcher();
const dispatch = createEventDispatcher()
$: {
if ($workspaceStore) {
loadResources();
loadResources()
}
}
</script>
@@ -33,8 +33,8 @@
? 'item-button-disabled'
: 'item-button'}"
on:click={() => {
value = r;
dispatch('click');
value = r
dispatch('click')
}}
>
<IconedResourceType name={r} after={true} />

View File

@@ -1,40 +1,40 @@
<script lang="ts">
import { page } from '$app/stores';
import type { Script, Flow } from '../../gen';
import { getToday } from '../../utils';
import { slide } from 'svelte/transition';
import { page } from '$app/stores'
import type { Script, Flow } from '../../gen'
import { getToday } from '../../utils'
import { slide } from 'svelte/transition'
import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons';
import Icon from 'svelte-awesome';
import Tooltip from './Tooltip.svelte';
import SvelteMarkdown from 'svelte-markdown';
import SchemaForm from './SchemaForm.svelte';
import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'
import Icon from 'svelte-awesome'
import Tooltip from './Tooltip.svelte'
import SvelteMarkdown from 'svelte-markdown'
import SchemaForm from './SchemaForm.svelte'
export let runnable: Script | Flow | undefined;
export let runAction: (scheduledForStr: string | undefined, args: Record<string, any>) => void;
export let buttonText = 'Run';
export let schedulable = true;
export let detailed = true;
export let runnable: Script | Flow | undefined
export let runAction: (scheduledForStr: string | undefined, args: Record<string, any>) => void
export let buttonText = 'Run'
export let schedulable = true
export let detailed = true
export let args: Record<string, any> = {};
export let args: Record<string, any> = {}
let isValid = true;
let isValid = true
let queryArgs = $page.url.searchParams.get('args');
let queryArgs = $page.url.searchParams.get('args')
if (queryArgs) {
const parsed = JSON.parse(atob(queryArgs));
const parsed = JSON.parse(atob(queryArgs))
Object.entries(parsed).forEach(([k, v]) => {
if (v == '<function call>') {
parsed[k] = undefined;
parsed[k] = undefined
}
});
console.log(parsed);
args = parsed;
})
console.log(parsed)
args = parsed
}
// Run later
let viewOptions = false;
let scheduledForStr: string | undefined;
let viewOptions = false
let scheduledForStr: string | undefined
</script>
<div class="max-w-5xl">
@@ -97,7 +97,7 @@
<button
class="default-button-secondary mx-2 mb-1"
on:click={() => {
scheduledForStr = undefined;
scheduledForStr = undefined
}}>clear</button
>
</div>
@@ -109,7 +109,7 @@
type="submit"
class="mr-6 text-sm underline text-gray-700 inline-flex items-center"
on:click={() => {
viewOptions = !viewOptions;
viewOptions = !viewOptions
}}
>
{#if schedulable}
@@ -126,7 +126,7 @@
disabled={!isValid}
class="{isValid ? 'default-button' : 'default-button-disabled'} w-min px-6"
on:click={() => {
runAction(scheduledForStr, args);
runAction(scheduledForStr, args)
}}
>
{scheduledForStr ? 'Schedule run to a later time' : buttonText}

View File

@@ -1,130 +1,126 @@
<script lang="ts">
import SchemaModal, {
DEFAULT_PROPERTY,
modalToSchema,
schemaToModal
} from './SchemaModal.svelte';
import type { ModalSchemaProperty } from './SchemaModal.svelte';
import type { Schema } from '../../common';
import Editor from './Editor.svelte';
import { emptySchema, sendUserToast } from '../../utils';
import Tooltip from './Tooltip.svelte';
import TableCustom from './TableCustom.svelte';
import SchemaModal, { DEFAULT_PROPERTY, modalToSchema, schemaToModal } from './SchemaModal.svelte'
import type { ModalSchemaProperty } from './SchemaModal.svelte'
import type { Schema } from '../../common'
import Editor from './Editor.svelte'
import { emptySchema, sendUserToast } from '../../utils'
import Tooltip from './Tooltip.svelte'
import TableCustom from './TableCustom.svelte'
export let schema: Schema = emptySchema();
export let schema: Schema = emptySchema()
let schemaModal: SchemaModal;
let schemaString: string = '';
let schemaModal: SchemaModal
let schemaString: string = ''
// Internal state: bound to args builder modal
let modalProperty: ModalSchemaProperty = Object.assign({}, DEFAULT_PROPERTY);
let argError = '';
let editing = false;
let oldArgName: string | undefined; // when editing argument and changing name
let modalProperty: ModalSchemaProperty = Object.assign({}, DEFAULT_PROPERTY)
let argError = ''
let editing = false
let oldArgName: string | undefined // when editing argument and changing name
let viewJsonSchema = false;
let editor: Editor;
let viewJsonSchema = false
let editor: Editor
$: schemaString = JSON.stringify(schema, null, '\t');
$: schemaString = JSON.stringify(schema, null, '\t')
export function getEditor(): Editor {
return editor;
return editor
}
// Binding is not enough because monaco Editor does not support two-way binding
export function getSchema(): Schema {
if (viewJsonSchema) {
try {
schema = JSON.parse(editor.getCode());
return schema;
schema = JSON.parse(editor.getCode())
return schema
} catch (err) {
throw Error(`Error: input is not a valid schema: ${err}`);
throw Error(`Error: input is not a valid schema: ${err}`)
}
} else {
try {
editor.setCode(JSON.stringify(schema, null, '\t'));
return schema;
editor.setCode(JSON.stringify(schema, null, '\t'))
return schema
} catch (err) {
throw Error(`Error: input is not a valid schema: ${err}`);
throw Error(`Error: input is not a valid schema: ${err}`)
}
}
}
function handleAddOrEditArgument(): void {
// If editing the arg's name, oldName containing the old argument name must be provided
argError = '';
argError = ''
if (modalProperty.name.length === 0) {
argError = 'Arguments need to have a name';
argError = 'Arguments need to have a name'
} else if (Object.keys(schema.properties).includes(modalProperty.name) && !editing) {
argError = 'There is already an argument with this name';
argError = 'There is already an argument with this name'
} else {
schema.properties[modalProperty.name] = modalToSchema(modalProperty);
schema.properties[modalProperty.name] = modalToSchema(modalProperty)
if (modalProperty.required) {
schema.required = [...schema.required, modalProperty.name];
schema.required = [...schema.required, modalProperty.name]
} else if (schema.required.includes(modalProperty.name)) {
const index = schema.required.indexOf(modalProperty.name, 0);
const index = schema.required.indexOf(modalProperty.name, 0)
if (index > -1) {
schema.required.splice(index, 1);
schema.required.splice(index, 1)
}
}
if (editing && oldArgName && oldArgName !== modalProperty.name) {
handleDeleteArgument(oldArgName);
handleDeleteArgument(oldArgName)
}
modalProperty = Object.assign({}, DEFAULT_PROPERTY);
editing = false;
oldArgName = undefined;
schemaModal.closeModal();
modalProperty = Object.assign({}, DEFAULT_PROPERTY)
editing = false
oldArgName = undefined
schemaModal.closeModal()
}
}
function startEditArgument(argName: string): void {
argError = '';
argError = ''
if (Object.keys(schema.properties).includes(argName)) {
schemaModal.openModal();
editing = true;
schemaModal.openModal()
editing = true
modalProperty = schemaToModal(
schema.properties[argName],
argName,
schema.required.includes(argName)
);
oldArgName = argName;
)
oldArgName = argName
} else {
sendUserToast(`This argument does not exist and can't be edited`, true);
sendUserToast(`This argument does not exist and can't be edited`, true)
}
}
function handleDeleteArgument(argName: string): void {
try {
if (Object.keys(schema.properties).includes(argName)) {
delete schema.properties[argName];
schema = schema; //needed for reactivity, see https://svelte.dev/tutorial/updating-arrays-and-objects
delete schema.properties[argName]
schema = schema //needed for reactivity, see https://svelte.dev/tutorial/updating-arrays-and-objects
} else {
throw Error('Argument not found!');
throw Error('Argument not found!')
}
} catch (err) {
console.error(err);
sendUserToast(`Could not delete argument: ${err}`, true);
console.error(err)
sendUserToast(`Could not delete argument: ${err}`, true)
}
}
function switchTab(): void {
if (viewJsonSchema) {
let schemaString = editor.getCode();
let schemaString = editor.getCode()
if (schemaString === '') {
schemaString = JSON.stringify(emptySchema(), null, 4);
schemaString = JSON.stringify(emptySchema(), null, 4)
}
try {
schema = JSON.parse(schemaString);
viewJsonSchema = false;
schema = JSON.parse(schemaString)
viewJsonSchema = false
} catch (err) {
sendUserToast(err, true);
sendUserToast(err, true)
}
} else {
try {
editor.setCode(JSON.stringify(schema, null, '\t'));
viewJsonSchema = true;
editor.setCode(JSON.stringify(schema, null, '\t'))
viewJsonSchema = true
} catch (err) {
sendUserToast(err, true);
sendUserToast(err, true)
}
}
}
@@ -155,8 +151,8 @@
<button
class="default-button-secondary grow"
on:click={() => {
modalProperty = Object.assign({}, DEFAULT_PROPERTY);
schemaModal.openModal();
modalProperty = Object.assign({}, DEFAULT_PROPERTY)
schemaModal.openModal()
}}>Add argument</button
>
</div>
@@ -203,7 +199,7 @@
<button
class="default-button-secondary text-xs inline-flex"
on:click={() => {
startEditArgument(name);
startEditArgument(name)
}}>edit</button
></td
>

View File

@@ -1,32 +1,32 @@
<script lang="ts">
import { allTrue } from '../../utils';
import { allTrue } from '../../utils'
import { slide } from 'svelte/transition';
import { slide } from 'svelte/transition'
import type { Schema } from '../../common';
import ArgInput from './ArgInput.svelte';
import RadioButton from './RadioButton.svelte';
import Editor from './Editor.svelte';
import FieldHeader from './FieldHeader.svelte';
import Icon from 'svelte-awesome';
import type { Schema } from '../../common'
import ArgInput from './ArgInput.svelte'
import RadioButton from './RadioButton.svelte'
import Editor from './Editor.svelte'
import FieldHeader from './FieldHeader.svelte'
import Icon from 'svelte-awesome'
import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons';
import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'
export let inputTransform = false;
export let schema: Schema;
export let args: Record<string, any> = {};
export let inputTransform = false
export let schema: Schema
export let args: Record<string, any> = {}
export let isValid: boolean = true;
export let editableSchema = false;
export let isValid: boolean = true
export let editableSchema = false
let inputCheck: { [id: string]: boolean } = {};
let seeHelp: { [id: string]: boolean } = {};
let inputCheck: { [id: string]: boolean } = {}
let seeHelp: { [id: string]: boolean } = {}
export function setArgs(nargs: Record<string, any>) {
args = nargs;
args = nargs
}
$: isValid = allTrue(inputCheck) ?? false;
$: isValid = allTrue(inputCheck) ?? false
</script>
<div class="w-full">
@@ -51,13 +51,13 @@
small={true}
bind:value={args[argName].type}
on:change={(e) => {
console.log(e.detail);
console.log(e.detail)
args[argName].expr =
e.detail == 'javascript'
? `import { previous_result, flow_input, step, variable, resource, params } from 'windmill'
previous_result.myfield`
: undefined;
: undefined
}}
/>
</div>
@@ -86,7 +86,7 @@ previous_result.myfield`
<span
class="underline mr-4"
on:click={() => {
seeHelp[argName] = seeHelp[argName] == undefined ? true : !seeHelp[argName];
seeHelp[argName] = seeHelp[argName] == undefined ? true : !seeHelp[argName]
}}
>Help<Icon
class="ml-2"

View File

@@ -1,20 +1,20 @@
<script lang="ts" context="module">
import type { SchemaProperty } from '../../common';
import Modal from '../../routes/components/Modal.svelte';
import type { SchemaProperty } from '../../common'
import Modal from '../../routes/components/Modal.svelte'
export const ARG_TYPES = ['integer', 'number', 'string', 'boolean', 'object', 'array'] as const;
export type ArgType = typeof ARG_TYPES[number];
export const ARG_TYPES = ['integer', 'number', 'string', 'boolean', 'object', 'array'] as const
export type ArgType = typeof ARG_TYPES[number]
export interface ModalSchemaProperty {
selectedType?: string;
description: string;
name: string;
required: boolean;
format?: string;
pattern?: string;
enum_?: string[];
default?: any;
items?: { type?: 'string' | 'number' };
selectedType?: string
description: string
name: string
required: boolean
format?: string
pattern?: string
enum_?: string[]
default?: any
items?: { type?: 'string' | 'number' }
}
export function modalToSchema(schema: ModalSchemaProperty): SchemaProperty {
@@ -25,7 +25,7 @@
default: schema.default,
enum: schema.enum_,
items: schema.items
};
}
}
export function schemaToModal(
@@ -40,7 +40,7 @@
pattern: schema.pattern,
default: schema.default,
required
};
}
}
export const DEFAULT_PROPERTY: ModalSchemaProperty = {
@@ -48,41 +48,41 @@
description: '',
name: '',
required: false
};
}
</script>
<script lang="ts">
import Switch from './Switch.svelte';
import { createEventDispatcher } from 'svelte';
import ArgInput from './ArgInput.svelte';
import StringTypeNarrowing from './StringTypeNarrowing.svelte';
import Required from './Required.svelte';
import Switch from './Switch.svelte'
import { createEventDispatcher } from 'svelte'
import ArgInput from './ArgInput.svelte'
import StringTypeNarrowing from './StringTypeNarrowing.svelte'
import Required from './Required.svelte'
export let property: ModalSchemaProperty = DEFAULT_PROPERTY;
export let error = '';
export let editing = false;
export let oldArgName: string | undefined;
export let property: ModalSchemaProperty = DEFAULT_PROPERTY
export let error = ''
export let editing = false
export let oldArgName: string | undefined
const dispatch = createEventDispatcher();
let modal: Modal;
const dispatch = createEventDispatcher()
let modal: Modal
export function openModal(): void {
modal.openModal();
modal.openModal()
}
export function closeModal(): void {
modal.closeModal();
modal.closeModal()
}
function clearModal(): void {
error = '';
editing = false;
oldArgName = undefined;
property.name = DEFAULT_PROPERTY.name;
property.default = DEFAULT_PROPERTY.default;
property.description = DEFAULT_PROPERTY.description;
property.required = DEFAULT_PROPERTY.required;
property.selectedType = DEFAULT_PROPERTY.selectedType;
error = ''
editing = false
oldArgName = undefined
property.name = DEFAULT_PROPERTY.name
property.default = DEFAULT_PROPERTY.default
property.description = DEFAULT_PROPERTY.description
property.required = DEFAULT_PROPERTY.required
property.selectedType = DEFAULT_PROPERTY.selectedType
}
</script>
@@ -111,14 +111,14 @@
<button
class={argType == property.selectedType ? 'item-button-selected' : 'item-button'}
on:click={() => {
property.selectedType = argType;
property.selectedType = argType
}}>{argType}</button
>
{/each}
<button
class={!property.selectedType ? 'item-button-selected' : 'item-button'}
on:click={() => {
property.selectedType = undefined;
property.selectedType = undefined
}}>any</button
>
</div>
@@ -160,7 +160,7 @@
slot="submission"
class="px-4 py-2 text-white font-semibold bg-blue-500 rounded"
on:click={() => {
dispatch('save');
dispatch('save')
}}
>
Save

View File

@@ -1,15 +1,15 @@
<script lang="ts">
import type { Schema } from '../../common';
import { emptySchema } from '../../utils';
import type { Schema } from '../../common'
import { emptySchema } from '../../utils'
import Highlight from 'svelte-highlight';
import json from 'svelte-highlight/src/languages/json';
import github from 'svelte-highlight/src/styles/github';
import TableCustom from './TableCustom.svelte';
import Highlight from 'svelte-highlight'
import json from 'svelte-highlight/src/languages/json'
import github from 'svelte-highlight/src/styles/github'
import TableCustom from './TableCustom.svelte'
export let schema: Schema | undefined = emptySchema();
export let schema: Schema | undefined = emptySchema()
let viewJsonSchema = false;
let viewJsonSchema = false
</script>
<svelte:head>

View File

@@ -1,28 +1,28 @@
<script lang="ts">
import { ScriptService, type Script } from '../../gen';
import { ScriptService, type Script } from '../../gen'
import { emptySchema, sendUserToast } from '../../utils';
import { onDestroy } from 'svelte';
import ScriptEditor from './ScriptEditor.svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import Path from './Path.svelte';
import SvelteMarkdown from 'svelte-markdown';
import { workspaceStore } from '../../stores';
import ScriptSchema from './ScriptSchema.svelte';
import { inferArgs } from '../../infer';
import Required from './Required.svelte';
import { emptySchema, sendUserToast } from '../../utils'
import { onDestroy } from 'svelte'
import ScriptEditor from './ScriptEditor.svelte'
import { page } from '$app/stores'
import { goto } from '$app/navigation'
import Path from './Path.svelte'
import SvelteMarkdown from 'svelte-markdown'
import { workspaceStore } from '../../stores'
import ScriptSchema from './ScriptSchema.svelte'
import { inferArgs } from '../../infer'
import Required from './Required.svelte'
let editor: ScriptEditor;
let scriptSchema: ScriptSchema;
$: step = Number($page.url.searchParams.get('step')) || 1;
let editor: ScriptEditor
let scriptSchema: ScriptSchema
$: step = Number($page.url.searchParams.get('step')) || 1
export let script: Script;
export let initialPath: string = '';
export let script: Script
export let initialPath: string = ''
$: {
$page.url.searchParams.set('state', btoa(JSON.stringify(script)));
history.replaceState({}, '', $page.url);
$page.url.searchParams.set('state', btoa(JSON.stringify(script)))
history.replaceState({}, '', $page.url)
}
async function editScript(): Promise<void> {
@@ -38,47 +38,42 @@
schema: script.schema,
is_template: script.is_template
}
});
sendUserToast(`Success! New script version created with hash ${newHash}`);
goto(`/scripts/get/${newHash}`);
})
sendUserToast(`Success! New script version created with hash ${newHash}`)
goto(`/scripts/get/${newHash}`)
} catch (error) {
if (error.status === 400) {
sendUserToast(error.body, true);
} else {
sendUserToast(`Ooops.Something bad happened: ${error}`, true);
console.error(error);
}
sendUserToast(`Impossible to save the script: ${error.body}`, true)
}
}
export function setCode(script: Script) {
editor?.getEditor().setCode(script.content);
editor?.getEditor().setCode(script.content)
if (scriptSchema) {
if (script.schema) {
scriptSchema.setSchema(script.schema);
scriptSchema.setSchema(script.schema)
} else {
scriptSchema.setSchema(emptySchema());
scriptSchema.setSchema(emptySchema())
}
}
}
async function inferSchema() {
await inferArgs(script.content, script.schema);
await inferArgs(script.content, script.schema)
}
async function changeStep(step: number) {
if (step == 3) {
script.content = editor?.getEditor().getCode() ?? script.content;
await inferSchema();
script.schema = script.schema;
script.content = editor?.getEditor().getCode() ?? script.content
await inferSchema()
script.schema = script.schema
}
goto(`?step=${step}`);
goto(`?step=${step}`)
}
onDestroy(() => {
editor?.$destroy();
});
editor?.$destroy()
})
</script>
<div class="flex flex-col h-screen max-w-screen-lg xl:-ml-20 xl:pl-4 w-full -mt-4 pt-4 md:mx-10 ">
@@ -91,7 +86,7 @@
? 'default-button-disabled text-gray-700'
: 'default-button-secondary'} min-w-max ml-2"
on:click={() => {
changeStep(1);
changeStep(1)
}}>Step 1: Metadata</button
>
<button
@@ -99,7 +94,7 @@
? 'default-button-disabled text-gray-700'
: 'default-button-secondary'} min-w-max ml-2"
on:click={() => {
changeStep(2);
changeStep(2)
}}>Step 2: Code</button
>
<button
@@ -107,7 +102,7 @@
? 'default-button-disabled text-gray-700'
: 'default-button-secondary'} min-w-max ml-2"
on:click={() => {
changeStep(3);
changeStep(3)
}}>Step 3: UI customisation</button
>
</div>
@@ -118,15 +113,15 @@
(script.path == undefined || script.path == '' || script.path.split('/')[2] == '')}
class="default-button px-6 max-h-8"
on:click={() => {
changeStep(step + 1);
changeStep(step + 1)
}}>Next</button
>
{#if step == 2}
<button
class="default-button-secondary px-6 max-h-8 mr-2"
on:click={async () => {
await inferSchema();
editScript();
await inferSchema()
editScript()
}}>Save (commit)</button
>
{/if}

View File

@@ -6,11 +6,11 @@
VariableService,
ResourceService,
ScriptService
} from '../../gen';
import { sendUserToast, emptySchema, displayDate } from '../../utils';
import type { Schema } from '../../common';
import { fade } from 'svelte/transition';
import Icon from 'svelte-awesome';
} from '../../gen'
import { sendUserToast, emptySchema, displayDate } from '../../utils'
import type { Schema } from '../../common'
import { fade } from 'svelte/transition'
import Icon from 'svelte-awesome'
import {
faCheck,
faChevronDown,
@@ -20,87 +20,87 @@
faSearch,
faSpinner,
faTimes
} from '@fortawesome/free-solid-svg-icons';
import Editor from './Editor.svelte';
import Tooltip from './Tooltip.svelte';
import { onDestroy, onMount } from 'svelte';
import { userStore, workspaceStore } from '../../stores';
import TableCustom from './TableCustom.svelte';
import { check } from 'svelte-awesome/icons';
import Modal from './Modal.svelte';
import { Highlight } from 'svelte-highlight';
import { json, python } from 'svelte-highlight/src/languages';
import github from 'svelte-highlight/src/styles/github';
import ItemPicker from './ItemPicker.svelte';
import VariableEditor from './VariableEditor.svelte';
import ResourceEditor from './ResourceEditor.svelte';
import { inferArgs } from '../../infer';
} from '@fortawesome/free-solid-svg-icons'
import Editor from './Editor.svelte'
import Tooltip from './Tooltip.svelte'
import { onDestroy, onMount } from 'svelte'
import { userStore, workspaceStore } from '../../stores'
import TableCustom from './TableCustom.svelte'
import { check } from 'svelte-awesome/icons'
import Modal from './Modal.svelte'
import { Highlight } from 'svelte-highlight'
import { json, python } from 'svelte-highlight/src/languages'
import github from 'svelte-highlight/src/styles/github'
import ItemPicker from './ItemPicker.svelte'
import VariableEditor from './VariableEditor.svelte'
import ResourceEditor from './ResourceEditor.svelte'
import { inferArgs } from '../../infer'
// @ts-ignore
import { VSplitPane } from 'svelte-split-pane';
import SchemaForm from './SchemaForm.svelte';
import DisplayResult from './DisplayResult.svelte';
import { VSplitPane } from 'svelte-split-pane'
import SchemaForm from './SchemaForm.svelte'
import DisplayResult from './DisplayResult.svelte'
// Exported
export let schema: Schema = emptySchema();
export let schema: Schema = emptySchema()
export let code: string;
export let path: string | undefined;
export let code: string
export let path: string | undefined
// Control Editor layout
export let viewPreview = true;
export let previewTab: 'logs' | 'input' | 'output' | 'history' | 'last_save' = 'logs';
export let viewPreview = true
export let previewTab: 'logs' | 'input' | 'output' | 'history' | 'last_save' = 'logs'
let websocketAlive = { pyright: false, black: false };
let websocketAlive = { pyright: false, black: false }
// Internal state
let editor: Editor;
let editor: Editor
// Preview args input
let args: Record<string, any> = {};
let isValid: boolean = true;
let args: Record<string, any> = {}
let isValid: boolean = true
// Preview
let previewIsLoading = false;
let previewIntervalId: NodeJS.Timer;
let previewJob: Job | undefined;
let pastPreviews: CompletedJob[] = [];
let previewIsLoading = false
let previewIntervalId: NodeJS.Timer
let previewJob: Job | undefined
let pastPreviews: CompletedJob[] = []
let modalViewer: Modal;
let modalViewerTitle: string = '';
let modalViewerContent: any;
let modalViewerMode: 'logs' | 'result' | 'code' = 'logs';
let modalViewer: Modal
let modalViewerTitle: string = ''
let modalViewerContent: any
let modalViewerMode: 'logs' | 'result' | 'code' = 'logs'
let variablePicker: ItemPicker;
let resourcePicker: ItemPicker;
let scriptPicker: ItemPicker;
let variableEditor: VariableEditor;
let resourceEditor: ResourceEditor;
let variablePicker: ItemPicker
let resourcePicker: ItemPicker
let scriptPicker: ItemPicker
let variableEditor: VariableEditor
let resourceEditor: ResourceEditor
let syncIteration: number = 0;
let ITERATIONS_BEFORE_SLOW_REFRESH = 100;
let syncIteration: number = 0
let ITERATIONS_BEFORE_SLOW_REFRESH = 100
let lastSave: string | null;
let lastSave: string | null
$: lastSave = localStorage.getItem(path ?? 'last_save');
$: lastSave = localStorage.getItem(path ?? 'last_save')
export function getEditor(): Editor {
return editor;
return editor
}
export async function runPreview(): Promise<void> {
try {
if (previewIntervalId) {
clearInterval(previewIntervalId);
clearInterval(previewIntervalId)
}
if (previewIsLoading && previewJob) {
JobService.cancelQueuedJob({
workspace: $workspaceStore!,
id: previewJob.id,
requestBody: {}
});
})
}
previewIsLoading = true;
previewIsLoading = true
const previewId = await JobService.runScriptPreview({
workspace: $workspaceStore!,
@@ -109,17 +109,17 @@
content: editor.getCode(),
args: args
}
});
previewJob = undefined;
loadPreviewJob(previewId);
syncIteration = 0;
})
previewJob = undefined
loadPreviewJob(previewId)
syncIteration = 0
previewIntervalId = setInterval(() => {
syncer(previewId);
}, 500);
syncer(previewId)
}, 500)
//TODO fetch preview, every x time, until it's completed
} catch (err) {
previewIsLoading = false;
sendUserToast(`Could not run preview: ${err} `, true);
previewIsLoading = false
sendUserToast(`Could not run preview: ${err} `, true)
}
}
@@ -129,7 +129,7 @@
jobKinds: 'preview',
createdBy: $userStore?.username,
scriptPathExact: path
});
})
}
async function loadPreviewJob(id: string): Promise<void> {
@@ -140,98 +140,98 @@
id,
running: previewJob.running,
logOffset: previewJob.logs?.length ?? 0
});
})
if (previewJobUpdates.new_logs) {
previewJob.logs = (previewJob.logs ?? '').concat(previewJobUpdates.new_logs);
previewJob.logs = (previewJob.logs ?? '').concat(previewJobUpdates.new_logs)
}
if ((previewJobUpdates.running ?? false) || (previewJobUpdates.completed ?? false)) {
previewJob = await JobService.getJob({ workspace: $workspaceStore!, id });
previewJob = await JobService.getJob({ workspace: $workspaceStore!, id })
}
} else {
previewJob = await JobService.getJob({ workspace: $workspaceStore!, id });
previewJob = await JobService.getJob({ workspace: $workspaceStore!, id })
}
if (previewJob?.type === 'CompletedJob') {
//only CompletedJob has success property
clearInterval(previewIntervalId);
previewIsLoading = false;
loadPastPreviews();
clearInterval(previewIntervalId)
previewIsLoading = false
loadPastPreviews()
}
} catch (err) {
console.error(err);
console.error(err)
}
}
async function inferSchema() {
let isDefault: string[] = [];
let isDefault: string[] = []
Object.entries(args).forEach(([k, v]) => {
if (schema.properties[k].default == v) {
isDefault.push(k);
isDefault.push(k)
}
});
await inferArgs(editor.getCode(), schema);
schema = schema;
})
await inferArgs(editor.getCode(), schema)
schema = schema
isDefault.forEach((key) => (args[key] = schema.properties[key].default));
isDefault.forEach((key) => (args[key] = schema.properties[key].default))
for (const key of Object.keys(args)) {
if (schema.properties[key] == undefined) {
delete args[key];
delete args[key]
}
}
}
function syncer(id: string): void {
if (syncIteration > ITERATIONS_BEFORE_SLOW_REFRESH) {
loadPreviewJob(id);
loadPreviewJob(id)
if (previewIntervalId) {
clearInterval(previewIntervalId);
previewIntervalId = setInterval(() => loadPreviewJob(id), 5000);
clearInterval(previewIntervalId)
previewIntervalId = setInterval(() => loadPreviewJob(id), 5000)
}
} else {
syncIteration++;
loadPreviewJob(id);
syncIteration++
loadPreviewJob(id)
}
}
async function loadVariables() {
let r: { name: string; path?: string; description?: string }[] = [];
let r: { name: string; path?: string; description?: string }[] = []
const variables = (
await VariableService.listVariable({ workspace: $workspaceStore ?? 'NO_W' })
).map((x) => {
return { name: x.path, ...x };
});
return { name: x.path, ...x }
})
const rvariables = await VariableService.listContextualVariables({
workspace: $workspaceStore ?? 'NO_W'
});
r = r.concat(variables).concat(rvariables);
return r;
})
r = r.concat(variables).concat(rvariables)
return r
}
async function loadScripts(): Promise<{ path: string; summary?: string }[]> {
return await ScriptService.listScripts({ workspace: $workspaceStore ?? 'NO_W' });
return await ScriptService.listScripts({ workspace: $workspaceStore ?? 'NO_W' })
}
let syncCode: NodeJS.Timer;
let syncCode: NodeJS.Timer
onMount(() => {
syncCode = setInterval(() => {
const newCode = editor?.getCode();
const newCode = editor?.getCode()
if (newCode && code != newCode) {
code = editor.getCode();
code = editor.getCode()
}
}, 3000);
});
}, 3000)
})
onDestroy(() => {
if (editor) {
code = editor.getCode();
code = editor.getCode()
}
if (previewIntervalId) {
clearInterval(previewIntervalId);
clearInterval(previewIntervalId)
}
if (syncCode) {
clearInterval(syncCode);
clearInterval(syncCode)
}
});
})
</script>
<svelte:head>
@@ -241,15 +241,15 @@
<ItemPicker
bind:this={scriptPicker}
pickCallback={async (path, _) => {
modalViewerMode = 'code';
modalViewerTitle = 'Script ' + path;
modalViewerMode = 'code'
modalViewerTitle = 'Script ' + path
modalViewerContent = (
await ScriptService.getScriptByPath({
workspace: $workspaceStore ?? '',
path
})
).content;
modalViewer.openModal();
).content
modalViewer.openModal()
}}
closeOnClick={false}
itemName="script"
@@ -277,16 +277,16 @@
pickCallback={(path, name) => {
if (!path) {
if (!getEditor().getCode().includes('import os')) {
getEditor().insertAtBeginning('import os\n');
getEditor().insertAtBeginning('import os\n')
}
getEditor().insertAtCursor(`os.environ.get("${name}")`);
sendUserToast(`${name} inserted at cursor`);
getEditor().insertAtCursor(`os.environ.get("${name}")`)
sendUserToast(`${name} inserted at cursor`)
} else {
if (!getEditor().getCode().includes('import wmill')) {
getEditor().insertAtBeginning('import wmill\n');
getEditor().insertAtBeginning('import wmill\n')
}
getEditor().insertAtCursor(`wmill.get_variable("${path}")`);
sendUserToast(`${name} inserted at cursor`);
getEditor().insertAtCursor(`wmill.get_variable("${path}")`)
sendUserToast(`${name} inserted at cursor`)
}
}}
itemName="Variable"
@@ -301,7 +301,7 @@
class="default-button-secondary"
type="button"
on:click={() => {
variableEditor.initNew();
variableEditor.initNew()
}}
>
Create a new variable
@@ -312,8 +312,8 @@
<ItemPicker
bind:this={resourcePicker}
pickCallback={(path, _) => {
getEditor().insertAtCursor(`client.get_resource("${path}")`);
sendUserToast(`${path} inserted at cursor`);
getEditor().insertAtCursor(`client.get_resource("${path}")`)
sendUserToast(`${path} inserted at cursor`)
}}
itemName="Resource"
extraField="resource_type"
@@ -328,7 +328,7 @@
class="default-button-secondary"
type="button"
on:click={() => {
resourceEditor.initNew();
resourceEditor.initNew()
}}
>
Create a new resource
@@ -346,7 +346,7 @@
downPanelSize={viewPreview ? '25%' : '10%'}
updateCallback={() => {
if (!viewPreview) {
viewPreview = true;
viewPreview = true
}
}}
>
@@ -357,7 +357,7 @@
<button
class="default-button-secondary font-semibold py-px mr-2 text-xs align-middle max-h-8"
on:click|stopPropagation={() => {
variablePicker.openModal();
variablePicker.openModal()
}}
>Variable picker <Icon data={faSearch} scale={0.7} />
</button>
@@ -365,7 +365,7 @@
<button
class="default-button-secondary font-semibold py-px text-xs mr-2 align-middle max-h-8"
on:click|stopPropagation={() => {
resourcePicker.openModal();
resourcePicker.openModal()
}}
>Resource picker <Icon data={faSearch} scale={0.7} />
</button>
@@ -373,7 +373,7 @@
<button
class="default-button-secondary font-semibold py-px text-xs mr-2 align-middle max-h-8"
on:click|stopPropagation={() => {
scriptPicker.openModal();
scriptPicker.openModal()
}}
>Script explorer <Icon data={faSearch} scale={0.7} />
</button>
@@ -381,7 +381,7 @@
<button
class="default-button-secondary py-px max-h-8 text-xs"
on:click|stopPropagation={() => {
editor.reloadWebsocket();
editor.reloadWebsocket()
}}
>
Reload assistants (status: <span
@@ -396,12 +396,12 @@
bind:websocketAlive
bind:this={editor}
cmdEnterAction={() => {
runPreview();
viewPreview = true;
runPreview()
viewPreview = true
}}
formatAction={() => {
code = getEditor().getCode();
localStorage.setItem(path ?? 'last_save', code);
code = getEditor().getCode()
localStorage.setItem(path ?? 'last_save', code)
}}
class="h-full"
automaticLayout={true}
@@ -414,7 +414,7 @@
<div
class="flex flex-row w-full cursor-pointer h-full"
on:click={() => {
viewPreview = !viewPreview;
viewPreview = !viewPreview
}}
>
<div class="flex flex-row items-baseline">
@@ -437,9 +437,9 @@
? 'underline drop-shadow-md'
: ''}"
on:click|stopPropagation={() => {
previewTab = 'input';
viewPreview = true;
inferSchema();
previewTab = 'input'
viewPreview = true
inferSchema()
}}
>
Inputs
@@ -447,8 +447,8 @@
<button
class="font-semibold my-0 py-0 h-full ml-3 {previewTab === 'logs' ? 'underline' : ''}"
on:click|stopPropagation={() => {
previewTab = 'logs';
viewPreview = true;
previewTab = 'logs'
viewPreview = true
}}
>
Logs
@@ -456,8 +456,8 @@
<button
class="font-semibold my-0 py-0 h-full ml-3 {previewTab === 'output' ? 'underline' : ''}"
on:click|stopPropagation={() => {
previewTab = 'output';
viewPreview = true;
previewTab = 'output'
viewPreview = true
}}
>
Result
@@ -468,10 +468,10 @@
: ''}"
on:click|stopPropagation={() => {
if (pastPreviews.length == 0) {
loadPastPreviews();
loadPastPreviews()
}
previewTab = 'history';
viewPreview = true;
previewTab = 'history'
viewPreview = true
}}
>
History
@@ -481,8 +481,8 @@
? 'underline'
: ''}"
on:click|stopPropagation={() => {
previewTab = 'last_save';
viewPreview = true;
previewTab = 'last_save'
viewPreview = true
}}
>
Local save
@@ -492,16 +492,16 @@
<button
class="mb-1 ml-2"
on:click|stopPropagation={() => {
viewPreview = !viewPreview;
viewPreview = !viewPreview
}}
><Icon data={viewPreview ? faChevronDown : faChevronUp} scale={0.7} />
</button>
<button
class="default-button py-px text-xs mx-2 align-middle max-h-8"
on:click|stopPropagation={() => {
runPreview();
viewPreview = true;
previewTab = 'logs';
runPreview()
viewPreview = true
previewTab = 'logs'
}}
>Run preview
</button>
@@ -559,9 +559,9 @@
href="#last_save"
class="text-xs"
on:click={() => {
modalViewerContent = lastSave;
modalViewerMode = 'code';
modalViewer.openModal();
modalViewerContent = lastSave
modalViewerMode = 'code'
modalViewer.openModal()
}}>View last local save for path {path}</a
>
{:else}No local save{/if}
@@ -595,9 +595,9 @@
href="#result"
class="text-xs"
on:click={() => {
modalViewerContent = result;
modalViewerMode = 'result';
modalViewer.openModal();
modalViewerContent = result
modalViewerMode = 'result'
modalViewer.openModal()
}}>{JSON.stringify(result).substring(0, 30)}...</a
></td
>
@@ -611,9 +611,9 @@
workspace: $workspaceStore ?? 'NO_W',
id
})
).raw_code;
modalViewerMode = 'code';
modalViewer.openModal();
).raw_code
modalViewerMode = 'code'
modalViewer.openModal()
}}
>View code
</a></td
@@ -628,9 +628,9 @@
workspace: $workspaceStore ?? 'NO_W',
id
})
).logs;
modalViewerMode = 'logs';
modalViewer.openModal();
).logs
modalViewerMode = 'logs'
modalViewer.openModal()
}}
>View logs
</a></td

View File

@@ -1,49 +1,49 @@
<script lang="ts">
import { sendUserToast } from '../../utils';
import { ScriptService, FlowService } from '../../gen';
import { sendUserToast } from '../../utils'
import { ScriptService, FlowService } from '../../gen'
import Icon from 'svelte-awesome';
import { faSearch } from '@fortawesome/free-solid-svg-icons';
import { workspaceStore } from '../../stores';
import { createEventDispatcher } from 'svelte';
import ItemPicker from './ItemPicker.svelte';
import RadioButton from './RadioButton.svelte';
import Modal from './Modal.svelte';
import { Highlight } from 'svelte-highlight';
import { python } from 'svelte-highlight/src/languages';
import github from 'svelte-highlight/src/styles/github';
import Icon from 'svelte-awesome'
import { faSearch } from '@fortawesome/free-solid-svg-icons'
import { workspaceStore } from '../../stores'
import { createEventDispatcher } from 'svelte'
import ItemPicker from './ItemPicker.svelte'
import RadioButton from './RadioButton.svelte'
import Modal from './Modal.svelte'
import { Highlight } from 'svelte-highlight'
import { python } from 'svelte-highlight/src/languages'
import github from 'svelte-highlight/src/styles/github'
export let scriptPath: string | undefined = undefined;
export let allowFlow = false;
export let isFlow = false;
export let scriptPath: string | undefined = undefined
export let allowFlow = false
export let isFlow = false
let items: { summary: String; path: String; version?: String }[] = [];
let itemPicker: ItemPicker;
let modalViewer: Modal;
let code: string = '';
let items: { summary: String; path: String; version?: String }[] = []
let itemPicker: ItemPicker
let modalViewer: Modal
let code: string = ''
const dispatch = createEventDispatcher();
const dispatch = createEventDispatcher()
async function getScript() {
code = (await ScriptService.getScriptByPath({ workspace: $workspaceStore!, path: scriptPath! }))
.content;
.content
}
async function loadItems(isFlow: boolean): Promise<void> {
try {
if (isFlow) {
items = await FlowService.listFlows({ workspace: $workspaceStore! });
items = await FlowService.listFlows({ workspace: $workspaceStore! })
} else {
items = await ScriptService.listScripts({ workspace: $workspaceStore! });
items = await ScriptService.listScripts({ workspace: $workspaceStore! })
}
} catch (err) {
sendUserToast(`Could not load items: ${err}`, true);
sendUserToast(`Could not load items: ${err}`, true)
}
}
$: {
if ($workspaceStore) {
loadItems(isFlow);
loadItems(isFlow)
}
}
</script>
@@ -55,12 +55,12 @@
<ItemPicker
bind:this={itemPicker}
pickCallback={(path, _) => {
scriptPath = path;
scriptPath = path
}}
itemName={isFlow ? 'Flow' : 'Script'}
extraField="summary"
loadItems={async () => {
return items;
return items
}}
/>
@@ -77,7 +77,7 @@
<select
bind:value={scriptPath}
on:change={() => {
dispatch('select', { path: scriptPath });
dispatch('select', { path: scriptPath })
}}
class="max-w-lg"
>
@@ -93,8 +93,8 @@
<button
class="text-xs text-blue-500"
on:click={async () => {
await getScript();
modalViewer.openModal();
await getScript()
modalViewer.openModal()
}}>show code</button
>
{/if}

View File

@@ -1,23 +1,23 @@
<script lang="ts">
import type { Schema } from '../../common';
import type { Schema } from '../../common'
import PageHeader from './PageHeader.svelte';
import SchemaForm from './SchemaForm.svelte';
import Tabs from './Tabs.svelte';
import PageHeader from './PageHeader.svelte'
import SchemaForm from './SchemaForm.svelte'
import Tabs from './Tabs.svelte'
import Highlight from 'svelte-highlight';
import json from 'svelte-highlight/src/languages/json';
import github from 'svelte-highlight/src/styles/github';
import SvelteMarkdown from 'svelte-markdown';
import Highlight from 'svelte-highlight'
import json from 'svelte-highlight/src/languages/json'
import github from 'svelte-highlight/src/styles/github'
import SvelteMarkdown from 'svelte-markdown'
export let schema: Schema;
export let summary: string;
export let description: string | undefined;
export let synchronizedHeader = true;
export let schema: Schema
export let summary: string
export let description: string | undefined
export let synchronizedHeader = true
let tab: 'ui' | 'jsonschema' = 'ui';
let tab: 'ui' | 'jsonschema' = 'ui'
export function setSchema(newSchema: Schema) {
schema = newSchema;
schema = newSchema
}
</script>

View File

@@ -1,54 +1,54 @@
<script lang="ts">
import Modal from './Modal.svelte';
import TableCustom from './TableCustom.svelte';
import Modal from './Modal.svelte'
import TableCustom from './TableCustom.svelte'
import { GranularAclService } from '../../gen/services/GranularAclService';
import { sendUserToast } from '../../utils';
import { GroupService, UserService } from '../../gen';
import { createEventDispatcher } from 'svelte';
import AutoComplete from 'simple-svelte-autocomplete';
import { workspaceStore } from '../../stores';
import { GranularAclService } from '../../gen/services/GranularAclService'
import { sendUserToast } from '../../utils'
import { GroupService, UserService } from '../../gen'
import { createEventDispatcher } from 'svelte'
import AutoComplete from 'simple-svelte-autocomplete'
import { workspaceStore } from '../../stores'
const dispatch = createEventDispatcher();
const dispatch = createEventDispatcher()
export let kind: 'script' | 'group_' | 'resource' | 'schedule' | 'variable' | 'flow';
export let path: string = '';
export let kind: 'script' | 'group_' | 'resource' | 'schedule' | 'variable' | 'flow'
export let path: string = ''
let ownerKind: 'user' | 'group' = 'user';
let owner: string = '';
let ownerKind: 'user' | 'group' = 'user'
let owner: string = ''
let newOwner: string = '';
let write: boolean = false;
let acls: [string, boolean][] = [];
let groups: String[] = [];
let usernames: string[] = [];
let newOwner: string = ''
let write: boolean = false
let acls: [string, boolean][] = []
let groups: String[] = []
let usernames: string[] = []
let modal: Modal;
let modal: Modal
$: newOwner = [ownerKind === 'group' ? 'g' : 'u', owner].join('/');
$: newOwner = [ownerKind === 'group' ? 'g' : 'u', owner].join('/')
export async function openModal(newPath?: string) {
if (newPath) {
path = newPath;
path = newPath
}
loadAcls();
loadGroups();
loadUsernames();
modal.openModal();
loadAcls()
loadGroups()
loadUsernames()
modal.openModal()
}
async function loadAcls() {
acls = Object.entries(
await GranularAclService.getGranularAcls({ workspace: $workspaceStore!, path, kind })
);
)
}
async function loadGroups(): Promise<void> {
groups = await GroupService.listGroupNames({ workspace: $workspaceStore! });
groups = await GroupService.listGroupNames({ workspace: $workspaceStore! })
}
async function loadUsernames(): Promise<void> {
usernames = await UserService.listUsernames({ workspace: $workspaceStore! });
usernames = await UserService.listUsernames({ workspace: $workspaceStore! })
}
async function deleteAcl(owner: string) {
@@ -58,11 +58,11 @@
path,
kind,
requestBody: { owner }
});
loadAcls();
dispatch('change');
})
loadAcls()
dispatch('change')
} catch (err) {
sendUserToast(err.toString(), true);
sendUserToast(err.toString(), true)
}
}
@@ -73,11 +73,11 @@
path,
kind,
requestBody: { owner, write }
});
loadAcls();
dispatch('change');
})
loadAcls()
dispatch('change')
} catch (err) {
sendUserToast(err.toString(), true);
sendUserToast(err.toString(), true)
}
}
</script>
@@ -95,9 +95,9 @@
bind:value={ownerKind}
on:change={() => {
if (ownerKind === 'group') {
owner = 'all';
owner = 'all'
} else {
owner = '';
owner = ''
}
}}
>

View File

@@ -1,47 +1,47 @@
<script lang="ts">
import { userStore } from '../../stores';
import { userStore } from '../../stores'
import Badge from './Badge.svelte';
export let extraPerms: Record<string, boolean> = {};
export let canWrite: boolean;
import Badge from './Badge.svelte'
export let extraPerms: Record<string, boolean> = {}
export let canWrite: boolean
let kind: 'read' | 'write' | undefined = undefined;
let reason = '';
let kind: 'read' | 'write' | undefined = undefined
let reason = ''
$: {
let username = $userStore?.username ?? '';
let pgroups = $userStore?.pgroups ?? [];
let pusername = `u/${username}`;
let extraPermsKeys = Object.keys(extraPerms);
let username = $userStore?.username ?? ''
let pgroups = $userStore?.pgroups ?? []
let pusername = `u/${username}`
let extraPermsKeys = Object.keys(extraPerms)
if (pusername in extraPermsKeys) {
if (extraPerms[pusername]) {
kind = 'write';
kind = 'write'
} else {
kind = 'read';
kind = 'read'
}
reason = 'This item was shared to you personally';
reason = 'This item was shared to you personally'
} else {
let writeGroup = pgroups.find((x) => extraPermsKeys.includes(x) && extraPerms[x]);
let writeGroup = pgroups.find((x) => extraPermsKeys.includes(x) && extraPerms[x])
if (pgroups.find((x) => x in extraPermsKeys && extraPerms[x])) {
kind = 'write';
reason = `This item was write shared to the group ${writeGroup} which you are a member of`;
kind = 'write'
reason = `This item was write shared to the group ${writeGroup} which you are a member of`
} else {
let readGroup = pgroups.find((x) => extraPermsKeys.includes(x) && extraPerms[x]);
let readGroup = pgroups.find((x) => extraPermsKeys.includes(x) && extraPerms[x])
if (pgroups.find((x) => extraPermsKeys.includes(x))) {
kind = 'read';
reason = `This item was read-only shared to the group ${readGroup} which you are a member of`;
kind = 'read'
reason = `This item was read-only shared to the group ${readGroup} which you are a member of`
} else {
kind = undefined;
kind = undefined
}
}
}
if (kind == 'read' && canWrite) {
kind = undefined;
kind = undefined
}
if (kind == undefined && !canWrite) {
kind = 'read';
reason = '';
kind = 'read'
reason = ''
}
}
</script>

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