Compare commits

...

41 Commits
main ... v1.8.0

Author SHA1 Message Date
Ruben Fiszel
e114d0f426 chore(main): release 1.8.0 (#52)
* chore(main): release 1.8.0

* Apply automatic changes

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2022-05-17 20:53:14 +02:00
Ruben Fiszel
03ec38e001 update cargo 2022-05-17 20:52:42 +02:00
Ruben Fiszel
2e1d43033f feat: Typescript support for scripts (alpha)
* typescript support

* frontend

* type inference

* type inference

* v0 works

* v0 typescript

* v0 typescript

* deno-client v0

* deno-client v0

* build_deno

* rm autogenerated files

* test workflow

* test workflow

* test workflow

* test workflow

* test workflow

* test workflow

* test workflow

* test workflow

* test workflow

* test workflow

* test workflow

* test workflow

* test workflow

* test workflow

* test workflow

* test workflow

* test workflow

* test workflow

* on tags

* createResource

* createResource

* createResource2

* typescript support

* templates

* include version
2022-05-17 20:42:05 +02:00
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
179 changed files with 5074 additions and 3579 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

49
.github/workflows/deno_on_release.yml vendored Normal file
View File

@@ -0,0 +1,49 @@
name: Publish deno-client
on:
push:
tags:
- "v*"
env:
repo: windmill-deno-client
jobs:
build_deno_and_push_to_repo:
runs-on: ubuntu-latest
container: openapitools/openapi-generator-cli:v6.0.0-beta
steps:
- uses: actions/checkout@v3
- name: generate_deno
run: |
cd deno-client
rm .gitignore
./generate.sh
- name: Pushes to another repository
id: push_directory
uses: cpina/github-action-push-to-another-repository@devel
env:
API_TOKEN_GITHUB: ${{ secrets.DENO_PAT }}
with:
source-directory: deno-client/
destination-github-username: ${{ github.repository_owner }}
destination-repository-name: ${{ env.repo }}
user-email: ruben@windmill.dev
commit-message: See ORIGIN_COMMIT from $GITHUB_REF
target-branch: main
tag_repo:
needs: [build_deno_and_push_to_repo]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
repository: ${{ github.repository_owner }}/${{ env.repo }}
token: ${{ secrets.DENO_PAT }}
path: ./client
- name: Push client
run: |
cd ./client
git config --global user.email "ruben@windmill.dev"
git config --global user.name "rubenfiszel[bot]"
git tag -a ${{ github.ref_name }} -m "${{ github.ref_name }}"
git push --tags

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,41 @@
# Changelog
## [1.8.0](https://github.com/windmill-labs/windmill/compare/v1.7.0...v1.8.0) (2022-05-17)
### Features
* Typescript support for scripts (alpha) ([2e1d430](https://github.com/windmill-labs/windmill/commit/2e1d43033f3ad6dbe86338b7a41da7b1120a5ffc))
## [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

884
backend/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.5.0"
version = "1.8.0"
authors = ["Ruben Fiszel <ruben@rubenfiszel.com>"]
edition = "2021"
@@ -55,6 +55,9 @@ regex = "^1"
deno_core = "^0"
indexmap = "~1.6.2"
async-recursion = "^1"
swc_common = "^0"
swc_ecma_parser = "^0"
swc_ecma_ast = "^0"
sqlx = { version = "^0", features = ["macros", "offline", "migrate", "uuid", "json", "chrono", "postgres", "runtime-tokio-rustls"]}
dotenv = "^0"

View File

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

View File

@@ -0,0 +1,12 @@
-- Add down migration script here
DROP TYPE SCRIPT_LANG;
ALTER TABLE script
DROP COLUMN language SCRIPT_LANG;
ALTER TABLE queue
DROP COLUMN language SCRIPT_LANG;
ALTER TABLE completed_job
DROP COLUMN language SCRIPT_LANG;

View File

@@ -0,0 +1,11 @@
-- Add up migration script here
CREATE TYPE SCRIPT_LANG AS ENUM ('python3', 'deno');
ALTER TABLE script
ADD COLUMN language SCRIPT_LANG NOT NULL DEFAULT 'python3';
ALTER TABLE queue
ADD COLUMN language SCRIPT_LANG NOT NULL DEFAULT 'python3';
ALTER TABLE completed_job
ADD COLUMN language SCRIPT_LANG NOT NULL DEFAULT 'python3';

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.8.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:
@@ -1251,11 +1317,16 @@ paths:
type: array
items:
type: string
language:
type: string
enum: [python3, deno]
required:
- path
- summary
- description
- content
- language
responses:
"201":
description: script created
@@ -1264,14 +1335,35 @@ paths:
schema:
type: string
/scripts/tojsonschema:
/scripts/python/tojsonschema:
post:
summary: inspect code to infer jsonschema of arguments
operationId: toJsonschema
summary: inspect python code to infer jsonschema of arguments
operationId: pythonToJsonschema
tags:
- script
requestBody:
description: code with the main function
description: python code with the main function
required: true
content:
application/json:
schema:
type: string
responses:
"200":
description: parsed args
content:
application/json:
schema:
$ref: "#/components/schemas/MainArgSignature"
/scripts/deno/tojsonschema:
post:
summary: inspect deno code to infer jsonschema of arguments
operationId: denoToJsonschema
tags:
- script
requestBody:
description: deno code with the main function
required: true
content:
application/json:
@@ -2588,6 +2680,9 @@ components:
type: string
lock_error_logs:
type: string
language:
type: string
enum: [python3, deno]
required:
- hash
- path
@@ -2599,14 +2694,17 @@ components:
- deleted
- is_template
- extra_perms
- language
ScriptArgs:
type: object
additionalProperties: true
additionalProperties: {}
QueuedJob:
type: object
properties:
workspace_id:
type: string
id:
type: string
format: uuid
@@ -2661,6 +2759,9 @@ components:
$ref: "#/components/schemas/FlowValue"
is_flow_step:
type: boolean
language:
type: string
enum: [python3, deno]
required:
- id
- running
@@ -2672,6 +2773,8 @@ components:
CompletedJob:
type: object
properties:
workspace_id:
type: string
id:
type: string
format: uuid
@@ -2683,6 +2786,9 @@ components:
created_at:
type: string
format: date-time
started_at:
type: string
format: date-time
duration:
type: integer
success:
@@ -2723,8 +2829,15 @@ components:
$ref: "#/components/schemas/FlowValue"
is_flow_step:
type: boolean
language:
type: string
enum: [python3, deno]
required:
- id
- created_by
- duration
- created_at
- started_at
- success
- canceled
- job_kind
@@ -2980,10 +3093,14 @@ components:
type: string
args:
$ref: "#/components/schemas/ScriptArgs"
language:
type: string
enum: [python3, deno]
required:
- content
- args
- language
CreateResource:
type: object
@@ -3243,7 +3360,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": {
@@ -419,6 +436,64 @@
]
}
},
"2420cb110a116dfbc6b8658a6d2d35db60c6aadd157488cb72eb9116ae7e9f54": {
"query": "INSERT INTO queue\n (workspace_id, id, parent_job, created_by, permissioned_as, scheduled_for, \n script_hash, script_path, raw_code, args, job_kind, schedule_path, raw_flow, flow_status, is_flow_step, language)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) RETURNING id",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Varchar",
"Uuid",
"Uuid",
"Varchar",
"Varchar",
"Timestamptz",
"Int8",
"Varchar",
"Text",
"Jsonb",
{
"Custom": {
"name": "job_kind",
"kind": {
"Enum": [
"script",
"preview",
"flow",
"dependencies",
"flowpreview"
]
}
}
},
"Varchar",
"Jsonb",
"Jsonb",
"Bool",
{
"Custom": {
"name": "script_lang",
"kind": {
"Enum": [
"python3",
"deno"
]
}
}
}
]
},
"nullable": [
false
]
}
},
"255aafff962738317f3227ae4eb871830d89b4c12c73d8dbabe6836da124e54d": {
"query": "select path from script where hash = $1 AND (workspace_id = $2 OR workspace_id = 'starter')",
"describe": {
@@ -1249,6 +1324,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": {
@@ -1834,53 +1960,6 @@
"nullable": []
}
},
"a151ceeddfd4a2825d4528542d3adc5c0a6947573558a2fd62429ba5da369617": {
"query": "INSERT INTO queue\n (workspace_id, id, parent_job, created_by, permissioned_as, scheduled_for, \n script_hash, script_path, raw_code, args, job_kind, schedule_path, raw_flow, flow_status, is_flow_step)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING id",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Varchar",
"Uuid",
"Uuid",
"Varchar",
"Varchar",
"Timestamptz",
"Int8",
"Varchar",
"Text",
"Jsonb",
{
"Custom": {
"name": "job_kind",
"kind": {
"Enum": [
"script",
"preview",
"flow",
"dependencies",
"flowpreview"
]
}
}
},
"Varchar",
"Jsonb",
"Jsonb",
"Bool"
]
},
"nullable": [
false
]
}
},
"a1d46b44718a63d6ce5a9054d493dadbffb205500dc8fb55e9816bcdb613e0d5": {
"query": "DELETE FROM queue WHERE schedule_path = $1",
"describe": {
@@ -2232,6 +2311,40 @@
]
}
},
"be33c6eb702c149044650d49b3c50493d7538d590be3f4ff6242fea85c57c667": {
"query": "INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, content, created_by, schema, is_template, extra_perms, lock, language) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Int8",
"Varchar",
"Int8Array",
"Text",
"Text",
"Text",
"Varchar",
"Text",
"Bool",
"Jsonb",
"Text",
{
"Custom": {
"name": "script_lang",
"kind": {
"Enum": [
"python3",
"deno"
]
}
}
}
]
},
"nullable": []
}
},
"bf1d8e043338867e1da1ed236ff6c85a566d5fd58d4b0d5c3a10454513811ba3": {
"query": "UPDATE workspace_settings\n SET slack_team_id = null, slack_name = null WHERE workspace_id = $1",
"describe": {
@@ -2713,69 +2826,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": {
@@ -2797,29 +2847,6 @@
]
}
},
"f12e710a0c2b2e13b98fd522028b6af8be74a9126a8207aa2974b47fd36e1845": {
"query": "INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, content, created_by, schema, is_template, extra_perms, lock) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Int8",
"Varchar",
"Int8Array",
"Text",
"Text",
"Text",
"Varchar",
"Text",
"Bool",
"Jsonb",
"Text"
]
},
"nullable": []
}
},
"f325a1262084bd3468e12dc8bcc289a96536f172b679af54dd0fbc82d4d7c987": {
"query": "DELETE FROM usr_to_group WHERE usr = $1 AND group_ = $2 AND workspace_id = $3",
"describe": {

View File

@@ -12,6 +12,7 @@ use sqlx::{query_scalar, Postgres, Transaction};
use std::collections::HashMap;
use crate::js_eval::eval_timeout;
use crate::scripts::ScriptLang;
use crate::users::create_token_for_owner;
use crate::{
audit::{audit_log, ActionKind},
@@ -83,6 +84,7 @@ pub struct QueuedJob {
pub flow_status: Option<serde_json::Value>,
pub raw_flow: Option<serde_json::Value>,
pub is_flow_step: bool,
pub language: ScriptLang,
}
#[derive(Debug, sqlx::FromRow, Serialize)]
@@ -92,6 +94,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>,
@@ -110,6 +113,7 @@ struct CompletedJob {
flow_status: Option<serde_json::Value>,
raw_flow: Option<serde_json::Value>,
is_flow_step: bool,
language: ScriptLang,
}
#[derive(Deserialize, Clone, Copy)]
@@ -263,6 +267,7 @@ async fn run_preview_job(
JobPayload::Code(RawCode {
content: preview.content,
path: preview.path,
language: preview.language,
}),
preview.args,
&authed.username,
@@ -416,6 +421,7 @@ async fn list_jobs(
"permissioned_as",
"flow_status",
"is_flow_step",
"language",
],
);
let sqlc = list_completed_jobs_query(
@@ -433,7 +439,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",
@@ -449,6 +455,7 @@ async fn list_jobs(
"permissioned_as",
"flow_status",
"is_flow_step",
"language",
],
);
let sql = format!(
@@ -544,6 +551,7 @@ async fn list_completed_jobs(
"parent_job",
"created_by",
"created_at",
"started_at",
"duration",
"success",
"script_hash",
@@ -562,6 +570,7 @@ async fn list_completed_jobs(
"null as flow_status",
"null as raw_flow",
"is_flow_step",
"language",
],
)
.sql()?;
@@ -855,6 +864,7 @@ struct UnifiedJob {
permissioned_as: String,
flow_status: Option<serde_json::Value>,
is_flow_step: bool,
language: ScriptLang,
}
impl From<UnifiedJob> for Job {
@@ -866,6 +876,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,
@@ -884,6 +895,7 @@ impl From<UnifiedJob> for Job {
flow_status: uj.flow_status,
raw_flow: None,
is_flow_step: uj.is_flow_step,
language: uj.language,
}),
"QueuedJob" => Job::QueuedJob(QueuedJob {
workspace_id: uj.workspace_id,
@@ -909,6 +921,7 @@ impl From<UnifiedJob> for Job {
flow_status: uj.flow_status,
raw_flow: None,
is_flow_step: uj.is_flow_step,
language: uj.language,
}),
t => panic!("job type {} not valid", t),
}
@@ -922,6 +935,7 @@ struct CancelJob {
pub struct RawCode {
content: String,
path: Option<String>,
language: ScriptLang,
}
#[derive(Deserialize)]
@@ -929,6 +943,7 @@ struct Preview {
content: String,
path: Option<String>,
args: Option<Map<String, Value>>,
language: ScriptLang,
}
#[derive(Deserialize)]
@@ -1001,22 +1016,32 @@ pub async fn push<'c>(
}
}
let (script_hash, script_path, raw_code, job_kind, raw_flow) = match job_payload {
let (script_hash, script_path, raw_code, job_kind, raw_flow, language) = match job_payload {
JobPayload::ScriptHash { hash, path } => {
(Some(hash.0), Some(path), None, JobKind::Script, None)
}
JobPayload::Code(RawCode { content, path }) => {
(None, path, Some(content), JobKind::Preview, None)
(Some(hash.0), Some(path), None, JobKind::Script, None, None)
}
JobPayload::Code(RawCode {
content,
path,
language,
}) => (
None,
path,
Some(content),
JobKind::Preview,
None,
Some(language),
),
JobPayload::Dependencies { hash, dependencies } => (
Some(hash.0),
None,
Some(dependencies.join("\n")),
JobKind::Dependencies,
None,
Some(ScriptLang::Python3),
),
JobPayload::RawFlow { value, path } => {
(None, path, None, JobKind::FlowPreview, Some(value))
(None, path, None, JobKind::FlowPreview, Some(value), None)
}
JobPayload::Flow(flow) => {
let value_json = sqlx::query_scalar!("SELECT value FROM flow WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter')",
@@ -1029,7 +1054,7 @@ pub async fn push<'c>(
"could not convert json to flow for {flow}: {err:?}"
))
})?;
(None, Some(flow), None, JobKind::Flow, Some(value))
(None, Some(flow), None, JobKind::Flow, Some(value), None)
}
};
@@ -1043,8 +1068,8 @@ pub async fn push<'c>(
let uuid = sqlx::query_scalar!(
"INSERT INTO queue
(workspace_id, id, parent_job, created_by, permissioned_as, scheduled_for,
script_hash, script_path, raw_code, args, job_kind, schedule_path, raw_flow, flow_status, is_flow_step)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING id",
script_hash, script_path, raw_code, args, job_kind, schedule_path, raw_flow, flow_status, is_flow_step, language)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) RETURNING id",
workspace_id,
job_id,
parent_job,
@@ -1059,7 +1084,8 @@ pub async fn push<'c>(
schedule_path,
raw_flow.map(|f| serde_json::json!(f)),
flow_status.map(|f| serde_json::json!(f)),
is_flow_step
is_flow_step,
language: ScriptLang
)
.fetch_one(&mut tx)
.await?;

View File

@@ -7,7 +7,7 @@
use ::oauth2::basic::BasicClient;
use argon2::Argon2;
use axum::{extract::extractor_middleware, handler::Handler, routing::get, Extension, Router};
use axum::{handler::Handler, middleware::from_extractor, routing::get, Extension, Router};
use db::DB;
use git_version::git_version;
use hyper::Response;
@@ -215,8 +215,8 @@ pub async fn run_server(
.nest("/workers", worker_ping::global_service())
.nest("/scripts", scripts::global_service())
.nest("/schedules", schedule::global_service())
.route_layer(extractor_middleware::<users::Authed>())
.route_layer(extractor_middleware::<users::Tokened>())
.route_layer(from_extractor::<users::Authed>())
.route_layer(from_extractor::<users::Tokened>())
.nest(
"/auth",
users::make_unauthed_service().layer(Extension(argon2)),

View File

@@ -43,7 +43,7 @@ pub fn global_service() -> Router {
.route("/login_callback/:client", get(login_callback))
.route(
"/slack_command",
post(slack_command).route_layer(axum::extract::extractor_middleware::<SlackSig>()),
post(slack_command).route_layer(axum::middleware::from_extractor::<SlackSig>()),
)
}

View File

@@ -25,7 +25,7 @@ pub struct MainArgSignature {
pub args: Vec<Arg>,
}
#[derive(Serialize)]
#[derive(Serialize, Clone)]
#[serde(rename_all(serialize = "lowercase"))]
pub enum Typ {
Str,
@@ -39,7 +39,7 @@ pub enum Typ {
Unknown,
}
#[derive(Serialize)]
#[derive(Serialize, Clone)]
pub struct Arg {
pub name: String,
pub typ: Typ,
@@ -47,7 +47,7 @@ pub struct Arg {
pub has_default: bool,
}
pub fn parse_signature(code: &str) -> error::Result<MainArgSignature> {
pub fn parse_python_signature(code: &str) -> error::Result<MainArgSignature> {
let ast = parser::parse_program(code)
.map_err(|e| error::Error::ExecutionErr(format!("Error parsing code: {}", e.to_string())))?
.statements;
@@ -115,6 +115,136 @@ pub fn parse_signature(code: &str) -> error::Result<MainArgSignature> {
}
}
use swc_common::sync::Lrc;
use swc_common::{FileName, SourceMap};
use swc_ecma_ast::{
AssignPat, BindingIdent, Decl, ExportDecl, FnDecl, Ident, ModuleDecl, ModuleItem, Pat,
TsKeywordTypeKind, TsType,
};
use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax, TsConfig};
pub fn parse_deno_signature(code: &str) -> error::Result<MainArgSignature> {
let cm: Lrc<SourceMap> = Default::default();
let fm = cm.new_source_file(FileName::Custom("test.ts".into()), code.into());
let lexer = Lexer::new(
// We want to parse ecmascript
Syntax::Typescript(TsConfig::default()),
// EsVersion defaults to es5
Default::default(),
StringInput::from(&*fm),
None,
);
let mut parser = Parser::new_from(lexer);
let mut err_s = "".to_string();
for e in parser.take_errors() {
err_s += &e.into_kind().msg().to_string();
}
let ast = parser
.parse_module()
.map_err(|e| {
error::Error::ExecutionErr(format!("impossible to parse module: {err_s}\n{e:?}"))
})?
.body;
// println!("{ast:?}");
let params = ast.into_iter().find_map(|x| match x {
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
decl:
Decl::Fn(FnDecl {
ident:
Ident {
span: _,
sym,
optional: _,
},
declare: _,
function,
}),
span: _,
})) if &sym.to_string() == "main" => Some(function.params),
_ => None,
});
if let Some(params) = params {
Ok(MainArgSignature {
star_args: false,
star_kwargs: false,
args: params
.into_iter()
.map(|x| match x.pat {
Pat::Ident(ident) => {
let (name, typ) = binding_ident_to_arg(&ident)?;
Ok(Arg {
name,
typ,
default: None,
has_default: false,
})
}
Pat::Assign(AssignPat {
span: _,
left,
right,
type_ann: _,
}) => {
let (name, typ) =
left.as_ident().map(binding_ident_to_arg).ok_or_else(|| {
error::Error::ExecutionErr(format!(
"Arg {left:?} has unexepected syntax"
))
})??;
Ok(Arg {
name,
typ,
default: serde_json::to_value(right)
.map_err(|e| error::Error::ExecutionErr(e.to_string()))?
.as_object()
.and_then(|x| x.get("value").to_owned())
.cloned(),
has_default: true,
})
}
_ => Err(error::Error::ExecutionErr(format!(
"Arg {x:?} has unexepected syntax"
))),
})
.collect::<Result<Vec<Arg>, error::Error>>()?,
})
} else {
Err(error::Error::ExecutionErr(
"main function was not findable (expected to find 'export main function(...)'"
.to_string(),
))
}
}
fn binding_ident_to_arg(
BindingIdent { id, type_ann }: &BindingIdent,
) -> anyhow::Result<(String, Typ)> {
Ok((
id.sym.to_string(),
type_ann
.as_ref()
.map(|x| match &*x.type_ann {
TsType::TsKeywordType(t) => match t.kind {
TsKeywordTypeKind::TsObjectKeyword => Typ::Dict,
TsKeywordTypeKind::TsBooleanKeyword => Typ::Bool,
TsKeywordTypeKind::TsBigIntKeyword => Typ::Int,
TsKeywordTypeKind::TsNumberKeyword => Typ::Float,
TsKeywordTypeKind::TsStringKeyword => Typ::Str,
_ => Typ::Unknown,
},
// TODO: we can do better here and extract the inner type of array
TsType::TsArrayType(_) => Typ::List,
_ => Typ::Unknown,
})
.unwrap_or(Typ::Unknown),
))
}
const STDIMPORTS: [&str; 301] = [
"__future__",
"_abc",
@@ -468,7 +598,7 @@ fn to_value(et: &ExpressionType) -> Option<serde_json::Value> {
}
}
pub fn parse_imports(code: &str) -> error::Result<Vec<String>> {
pub fn parse_python_imports(code: &str) -> error::Result<Vec<String>> {
let find_requirements = code
.lines()
.find_position(|x| x.starts_with("#requirements:"));
@@ -527,7 +657,7 @@ mod tests {
use super::*;
#[test]
fn test_parse_sig() -> anyhow::Result<()> {
fn test_parse_python_sig() -> anyhow::Result<()> {
//let code = "print(2 + 3, fd=sys.stderr)";
let code = "
@@ -540,13 +670,13 @@ def main(test1: str, name: datetime.datetime = datetime.now(), byte: bytes = byt
return {\"len\": len(name), \"splitted\": name.split() }
";
println!("{}", serde_json::to_string(&parse_signature(code)?)?);
println!("{}", serde_json::to_string(&parse_python_signature(code)?)?);
Ok(())
}
#[test]
fn test_parse_imports() -> anyhow::Result<()> {
fn test_parse_python_imports() -> anyhow::Result<()> {
//let code = "print(2 + 3, fd=sys.stderr)";
let code = "
@@ -559,14 +689,14 @@ def main():
pass
";
let r = parse_imports(code)?;
let r = parse_python_imports(code)?;
println!("{}", serde_json::to_string(&r)?);
assert_eq!(r, vec!["wmill", "zanzibar", "matplotlib"]);
Ok(())
}
#[test]
fn test_parse_imports2() -> anyhow::Result<()> {
fn test_parse_python_imports2() -> anyhow::Result<()> {
//let code = "print(2 + 3, fd=sys.stderr)";
let code = "
#requirements:
@@ -583,10 +713,24 @@ def main():
pass
";
let r = parse_imports(code)?;
let r = parse_python_imports(code)?;
println!("{}", serde_json::to_string(&r)?);
assert_eq!(r, vec!["burkina=0.4", "nigeria"]);
Ok(())
}
#[test]
fn test_parse_deno_sig() -> anyhow::Result<()> {
let code = "
export function main(test1: string, test2: string = \"burkina\") {
console.log(42)
}
";
println!("{}", serde_json::to_string(&parse_deno_signature(code)?)?);
Ok(())
}
}

View File

@@ -35,7 +35,12 @@ use std::{
const MAX_HASH_HISTORY_LENGTH_STORED: usize = 20;
pub fn global_service() -> Router {
Router::new().route("/tojsonschema", post(parse_code_to_jsonschema))
Router::new()
.route(
"/python/tojsonschema",
post(parse_python_code_to_jsonschema),
)
.route("/deno/tojsonschema", post(parse_deno_code_to_jsonschema))
}
pub fn workspaced_service() -> Router {
@@ -50,6 +55,13 @@ pub fn workspaced_service() -> Router {
.route("/deployment_status/h/:hash", get(get_deployment_status))
}
#[derive(sqlx::Type, Serialize, Deserialize, Debug, PartialEq, Clone, Hash)]
#[sqlx(type_name = "SCRIPT_LANG", rename_all = "lowercase")]
#[serde(rename_all(serialize = "lowercase", deserialize = "lowercase"))]
pub enum ScriptLang {
Deno,
Python3,
}
#[derive(sqlx::Type, PartialEq, Debug, Hash, Clone, Copy)]
#[sqlx(transparent)]
pub struct ScriptHash(pub i64);
@@ -113,6 +125,7 @@ pub struct Script {
pub extra_perms: serde_json::Value,
pub lock: Option<String>,
pub lock_error_logs: Option<String>,
pub language: ScriptLang,
}
#[derive(Serialize, Deserialize, sqlx::Type, Debug)]
@@ -138,6 +151,7 @@ pub struct NewScript {
pub schema: Option<Schema>,
pub is_template: Option<bool>,
pub lock: Option<Vec<String>>,
pub language: ScriptLang,
}
#[derive(Deserialize)]
@@ -181,6 +195,7 @@ async fn list_scripts(
"extra_perms",
"null as lock",
"CASE WHEN lock_error_logs IS NOT NULL THEN 'error' ELSE null END as lock_error_logs",
"language",
])
.order_by("created_at", lq.order_desc.unwrap_or(true))
.and_where("workspace_id = ? OR workspace_id = 'starter'".bind(&w_id))
@@ -344,10 +359,16 @@ async fn create_script(
.map(|v| v.1.clone())
.unwrap_or(json!({}));
let lock = if ns.language == ScriptLang::Deno {
Some("".to_string())
} else {
ns.lock.as_ref().map(|x| x.join("\n"))
};
//::text::json is to ensure we use serde_json with preserve order
sqlx::query!(
"INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, content, \
created_by, schema, is_template, extra_perms, lock) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12)",
created_by, schema, is_template, extra_perms, lock, language) VALUES \
($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13)",
&w_id,
&hash.0,
ns.path,
@@ -359,13 +380,14 @@ async fn create_script(
ns.schema.and_then(|x| serde_json::to_string(&x.0).ok()),
ns.is_template.unwrap_or(false),
extra_perms,
ns.lock.as_ref().map(|x| x.join("\n"))
lock,
ns.language: ScriptLang
)
.execute(&mut tx)
.await?;
let mut tx = if ns.lock.is_none() {
let dependencies = parser::parse_imports(&ns.content)?;
let mut tx = if ns.lock.is_none() && ns.language == ScriptLang::Python3 {
let dependencies = parser::parse_python_imports(&ns.content)?;
let (_, tx) = jobs::push(
tx,
&w_id,
@@ -593,10 +615,16 @@ async fn delete_script_by_hash(
Ok(Json(script))
}
async fn parse_code_to_jsonschema(
async fn parse_python_code_to_jsonschema(
Json(code): Json<String>,
) -> JsonResult<parser::MainArgSignature> {
parser::parse_signature(&code).map(Json)
parser::parse_python_signature(&code).map(Json)
}
async fn parse_deno_code_to_jsonschema(
Json(code): Json<String>,
) -> JsonResult<parser::MainArgSignature> {
parser::parse_deno_signature(&code).map(Json)
}
pub fn to_i64(s: &str) -> Result<i64> {

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

@@ -24,7 +24,7 @@ use crate::{
QueuedJob,
},
parser::{self, Typ},
scripts::ScriptHash,
scripts::{ScriptHash, ScriptLang},
users::{create_token_for_owner, get_email_from_username},
variables,
};
@@ -44,11 +44,14 @@ use tokio::sync::mpsc;
const TMP_DIR: &str = "/tmp/windmill";
const PIP_CACHE_DIR: &str = "/tmp/windmill/cache/pip";
const DENO_CACHE_DIR: &str = "/tmp/windmill/cache/deno";
const NUM_SECS_ENV_CHECK: u64 = 15;
const INCLUDE_DEPS_SH_CONTENT: &str = include_str!("../../nsjail/download_deps.sh");
const NSJAIL_CONFIG_DOWNLOAD_CONTENT: &str = include_str!("../../nsjail/download.config.proto");
const NSJAIL_CONFIG_RUN_CONTENT: &str = include_str!("../../nsjail/run.config.proto");
const NSJAIL_CONFIG_RUN_PYTHON3_CONTENT: &str =
include_str!("../../nsjail/run.python3.config.proto");
const NSJAIL_CONFIG_RUN_DENO_CONTENT: &str = include_str!("../../nsjail/run.deno.config.proto");
pub async fn run_worker(
db: &DB,
@@ -66,17 +69,13 @@ pub async fn run_worker(
let worker_dir = format!("{TMP_DIR}/{worker_name}");
tracing::debug!(worker_dir = %worker_dir, worker_name = %worker_name, "Creating worker dir");
DirBuilder::new()
.recursive(true)
.create(&worker_dir)
.await
.expect("could not create initial worker dir");
DirBuilder::new()
.recursive(true)
.create(&PIP_CACHE_DIR)
.await
.expect("could not create initial worker dir");
for x in [&worker_dir, PIP_CACHE_DIR, DENO_CACHE_DIR] {
DirBuilder::new()
.recursive(true)
.create(x)
.await
.expect("could not create initial worker dir");
}
let _ = write_file(&worker_dir, "download_deps.sh", INCLUDE_DEPS_SH_CONTENT).await;
@@ -289,6 +288,7 @@ async fn handle_job(
.expect("could not create initial job dir");
let mut status: Result<ExitStatus, Error>;
if matches!(job.job_kind, JobKind::Dependencies) {
let requirements = job
.raw_code
@@ -344,12 +344,17 @@ async fn handle_job(
.await?;
}
} else {
let (inner_content, requirements_o) = if matches!(job.job_kind, JobKind::Preview) {
let (inner_content, requirements_o, language) = if matches!(job.job_kind, JobKind::Preview)
{
let code = (job.raw_code.as_ref().unwrap_or(&"no raw code".to_owned())).to_owned();
let reqs = parser::parse_imports(&code)?.join("\n");
(code, Some(reqs))
let reqs = if job.language == ScriptLang::Python3 {
Some(parser::parse_python_imports(&code)?.join("\n"))
} else {
None
};
(code, reqs, job.language.to_owned())
} else {
sqlx::query_as::<_, (String, Option<String>)>("SELECT content, lock FROM script WHERE hash = $1 AND (workspace_id = $2 OR workspace_id = 'starter')")
sqlx::query_as::<_, (String, Option<String>, ScriptLang)>("SELECT content, lock, language FROM script WHERE hash = $1 AND (workspace_id = $2 OR workspace_id = 'starter')")
.bind(&job.script_hash.unwrap_or(ScriptHash(0)).0)
.bind(&job.workspace_id)
.fetch_optional(db)
@@ -357,69 +362,76 @@ async fn handle_job(
.ok_or_else(|| Error::InternalErr(format!("expected content and lock")))?
};
let requirements =
requirements_o.ok_or_else(|| Error::InternalErr(format!("lockfile missing")))?;
match language {
ScriptLang::Python3 => {
let requirements = requirements_o
.ok_or_else(|| Error::InternalErr(format!("lockfile missing")))?;
let _ = write_file(
&job_dir,
"download.config.proto",
&NSJAIL_CONFIG_DOWNLOAD_CONTENT
.replace("{JOB_DIR}", &job_dir)
.replace("{WORKER_DIR}", &worker_dir)
.replace("{CACHE_DIR}", PIP_CACHE_DIR),
)
.await?;
let _ = write_file(&job_dir, "requirements.txt", &requirements).await?;
let _ = write_file(
&job_dir,
"download.config.proto",
&NSJAIL_CONFIG_DOWNLOAD_CONTENT
.replace("{JOB_DIR}", &job_dir)
.replace("{WORKER_DIR}", &worker_dir)
.replace("{CACHE_DIR}", PIP_CACHE_DIR),
)
.await?;
let _ = write_file(&job_dir, "requirements.txt", &requirements).await?;
let child = Command::new("nsjail")
.current_dir(&job_dir)
.args(vec!["--config", "download.config.proto"])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let child = Command::new("nsjail")
.current_dir(&job_dir)
.args(vec!["--config", "download.config.proto"])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
logs.push_str("\n--- DEPENDENCIES INSTALL ---\n");
status = handle_child(job, db, &mut logs, &mut last_line, timeout, child).await;
if status.is_ok() {
logs.push_str("\n\n--- CODE EXECUTION ---\n");
logs.push_str("\n--- PIP DEPENDENCIES INSTALL ---\n");
status = handle_child(job, db, &mut logs, &mut last_line, timeout, child).await;
set_logs(logs, job.id, db).await;
if status.is_ok() {
logs.push_str("\n\n--- PTHON CODE EXECUTION ---\n");
let _ = write_file(&job_dir, "inner.py", &inner_content).await?;
set_logs(logs, job.id, db).await;
let sig = crate::parser::parse_signature(&inner_content)?;
let transforms = sig.args.into_iter().map(|x| match x.typ {
let _ = write_file(&job_dir, "inner.py", &inner_content).await?;
let sig = crate::parser::parse_python_signature(&inner_content)?;
let transforms = sig.args.into_iter().map(|x| match x.typ {
Typ::Bytes => format!("if \"{}\" in kwargs and kwargs[\"{}\"] is not None:\n kwargs[\"{}\"] = base64.b64decode(kwargs[\"{}\"])\n", x.name, x.name, x.name, x.name),
Typ::Datetime => format!("if \"{}\" in kwargs and kwargs[\"{}\"] is not None:\n kwargs[\"{}\"] = datetime.strptime(kwargs[\"{}\"], '%Y-%m-%dT%H:%M')\n", x.name, x.name, x.name, x.name),
_ => "".to_string()
}).collect::<Vec<String>>().join("");
let tx = db.begin().await?;
let tx = db.begin().await?;
let token = create_token_for_owner(
&db,
&job.workspace_id,
&job.permissioned_as,
crate::users::NewToken {
label: Some("ephemeral-script".to_string()),
expiration: Some(
chrono::Utc::now() + chrono::Duration::seconds((timeout * 2).into()),
),
},
&job.created_by,
)
.await?;
let token = create_token_for_owner(
&db,
&job.workspace_id,
&job.permissioned_as,
crate::users::NewToken {
label: Some("ephemeral-script".to_string()),
expiration: Some(
chrono::Utc::now()
+ chrono::Duration::seconds((timeout * 2).into()),
),
},
&job.created_by,
)
.await?;
let args = if let Some(args) = &job.args {
Some(transform_json_value(&token, &job.workspace_id, base_url, args.clone()).await)
} else {
None
};
let ser_args = serde_json::to_string(&args)
.map_err(|e| Error::ExecutionErr(e.to_string()))?
.replace("\\\"", "\\\\\"");
let wrapper_content: String = format!(
r#"
let args = if let Some(args) = &job.args {
Some(
transform_json_value(&token, &job.workspace_id, base_url, args.clone())
.await,
)
} else {
None
};
let ser_args = serde_json::to_string(&args)
.map_err(|e| Error::ExecutionErr(e.to_string()))?
.replace("\\\"", "\\\\\"");
let wrapper_content: String = format!(
r#"
import json
import base64
from datetime import datetime
@@ -443,44 +455,150 @@ print()
print("result:")
print(res_json)
"#,
);
write_file(&job_dir, "main.py", &wrapper_content).await?;
);
write_file(&job_dir, "main.py", &wrapper_content).await?;
tx.commit().await?;
let reserved_variables = variables::get_reserved_variables(
&job.workspace_id,
&token,
&get_email_from_username(&job.created_by, db)
.await?
.unwrap_or_else(|| "nosuitable@email.xyz".to_string()),
&job.created_by,
&job.id.to_string(),
)
.into_iter()
.map(|rv| (rv.name, rv.value));
tx.commit().await?;
let reserved_variables = variables::get_reserved_variables(
&job.workspace_id,
&token,
&get_email_from_username(&job.created_by, db)
.await?
.unwrap_or_else(|| "nosuitable@email.xyz".to_string()),
&job.created_by,
&job.id.to_string(),
)
.into_iter()
.map(|rv| (rv.name, rv.value));
let _ = write_file(
&job_dir,
"run.config.proto",
&NSJAIL_CONFIG_RUN_PYTHON3_CONTENT.replace("{JOB_DIR}", &job_dir),
)
.await?;
let _ = write_file(
&job_dir,
"run.config.proto",
&NSJAIL_CONFIG_RUN_CONTENT.replace("{JOB_DIR}", &job_dir),
)
.await?;
let child = Command::new("nsjail")
.current_dir(&job_dir)
.envs(reserved_variables)
.args(vec![
"--config",
"run.config.proto",
"--",
"/usr/local/bin/python3",
"-u",
"/tmp/main.py",
])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
status = handle_child(job, db, &mut logs, &mut last_line, timeout, child).await;
}
}
ScriptLang::Deno => {
logs.push_str("\n\n--- DENO CODE EXECUTION ---\n");
let child = Command::new("nsjail")
.current_dir(&job_dir)
.envs(reserved_variables)
.args(vec![
"--config",
set_logs(logs, job.id, db).await;
let _ = write_file(&job_dir, "inner.ts", &inner_content).await?;
let sig = crate::parser::parse_deno_signature(&inner_content)?;
// let transforms = sig.args.clone().into_iter().map(|x| match x.typ {
// Typ::Bytes => format!("if \"{}\" in kwargs and kwargs[\"{}\"] is not None:\n kwargs[\"{}\"] = base64.b64decode(kwargs[\"{}\"])\n", x.name, x.name, x.name, x.name),
// Typ::Datetime => format!("if \"{}\" in kwargs and kwargs[\"{}\"] is not None:\n kwargs[\"{}\"] = datetime.strptime(kwargs[\"{}\"], '%Y-%m-%dT%H:%M')\n", x.name, x.name, x.name, x.name),
// _ => "".to_string()
// }).collect::<Vec<String>>().join("");
let tx = db.begin().await?;
let token = create_token_for_owner(
&db,
&job.workspace_id,
&job.permissioned_as,
crate::users::NewToken {
label: Some("ephemeral-script".to_string()),
expiration: Some(
chrono::Utc::now() + chrono::Duration::seconds((timeout * 2).into()),
),
},
&job.created_by,
)
.await?;
let args = if let Some(args) = &job.args {
Some(
transform_json_value(&token, &job.workspace_id, base_url, args.clone())
.await,
)
} else {
None
};
let ser_args = serde_json::to_string(&args)
.map_err(|e| Error::ExecutionErr(e.to_string()))?
.replace("\\\"", "\\\\\"");
let spread = sig.args.into_iter().map(|x| x.name).join(",");
let wrapper_content: String = format!(
r#"
import {{ main }} from "./inner.ts";
const {{{spread}}}= JSON.parse(`{ser_args}`);
async function run() {{
let res: any = await main({spread});
if (res == undefined) {{
res = {{}}
}}
if (typeof res !== 'object') {{
res = {{ res1: res }}
}}
const res_json = JSON.stringify(res);
console.log();
console.log("result:");
console.log(res_json);
}}
run();
"#,
);
write_file(&job_dir, "main.ts", &wrapper_content).await?;
tx.commit().await?;
let reserved_variables = variables::get_reserved_variables(
&job.workspace_id,
&token,
&get_email_from_username(&job.created_by, db)
.await?
.unwrap_or_else(|| "nosuitable@email.xyz".to_string()),
&job.created_by,
&job.id.to_string(),
)
.into_iter()
.map(|rv| (rv.name, rv.value));
let _ = write_file(
&job_dir,
"run.config.proto",
"--",
"/usr/local/bin/python3",
"-u",
"/tmp/main.py",
])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
status = handle_child(job, db, &mut logs, &mut last_line, timeout, child).await;
&NSJAIL_CONFIG_RUN_DENO_CONTENT
.replace("{JOB_DIR}", &job_dir)
.replace("{CACHE_DIR}", DENO_CACHE_DIR),
)
.await?;
let child = Command::new("nsjail")
.current_dir(&job_dir)
.envs(reserved_variables)
.args(vec![
"--config",
"run.config.proto",
"--",
"/usr/bin/deno",
"run",
"--v8-flags=--max-heap-size=2048",
"-A",
"/tmp/main.ts",
])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
status = handle_child(job, db, &mut logs, &mut last_line, timeout, child).await;
}
}
}
tokio::fs::remove_dir_all(job_dir).await?;

Binary file not shown.

1
deno-client/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
windmill-api

5
deno-client/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"deno.enable": true,
"deno.lint": true,
"deno.unstable": true
}

5
deno-client/generate.sh Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/bash
set -e
/usr/local/bin/docker-entrypoint.sh generate -i ../backend/openapi.yaml -g typescript --additional-properties platform=deno -o windmill-api
sed -i 's/this\.type = "Job";//' windmill-api/models/Job.ts

46
deno-client/index.ts Normal file
View File

@@ -0,0 +1,46 @@
import { ResourceApi, VariableApi, ServerConfiguration } from './windmill-api/index.ts'
import { createConfiguration, type Configuration as Configuration } from './windmill-api/configuration.ts'
export * from './windmill-api/index.ts'
export function createConf(): Configuration & { workspace_id: string } {
const token = Deno.env.get("WM_TOKEN") ?? 'no_token'
return {
...createConfiguration({
baseServer: new ServerConfiguration(Deno.env.get("BASE_URL") ?? 'http://localhost:8000/api', {}),
authMethods: { bearerAuth: { tokenProvider: { getToken() { return token } } } },
}), workspace_id: Deno.env.get("WM_WORKSPACE") ?? 'no_workspace'
}
}
export async function getResource(path: string): Promise<any> {
const conf = createConf()
const resource = await new ResourceApi(conf).getResource(conf.workspace_id, path)
return await transformLeaves(resource.value)
}
export async function getVariable(path: string): Promise<string | undefined> {
const conf = createConf()
const variable = await new VariableApi(conf).getVariable(conf.workspace_id, path)
return variable.value
}
export async function transformLeaves(d: { [key: string]: any }): Promise<{ [key: string]: any }> {
for (const k in d) {
d[k] = await _transformLeaf(d[k])
}
return d
}
const VAR_RESOURCE_PREFIX = "$var:"
async function _transformLeaf(v: any): Promise<any> {
if (typeof v === 'object') {
return transformLeaves(v)
}
else if (typeof v === 'string' && v.startsWith(VAR_RESOURCE_PREFIX)) {
const varName = v.substring(VAR_RESOURCE_PREFIX.length)
return await getVariable(varName)
} else {
return v
}
}

View File

@@ -0,0 +1,7 @@
{
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
"version": "6.0.0-beta"
}
}

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

@@ -5,4 +5,8 @@
bind {$ADDRESS}
reverse_proxy /api/* http://localhost:8000
reverse_proxy /* http://localhost:3000
reverse_proxy /ws/* http://localhost:3001 {
lb_policy header "Authorization"
}
}

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.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "windmill",
"version": "1.4.0",
"version": "1.6.1",
"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.8.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,65 @@
import type { Schema, SchemaProperty } from "./common"
import { ScriptService } from "./gen"
import { sendUserToast } from "./utils"
import type { Schema, SchemaProperty } from './common'
import { ScriptService, type MainArgSignature } 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 = {}
export async function inferArgs(language: "python3" | "deno", code: string, schema: Schema): Promise<void> {
try {
let inferedSchema: MainArgSignature
if (language == "python3") {
inferedSchema = await ScriptService.pythonToJsonschema({
requestBody: code
})
} else if (language == "deno") {
inferedSchema = await ScriptService.denoToJsonschema({
requestBody: code
})
} else {
return
}
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
schema.required = []
const oldProperties = Object.assign({}, schema.properties)
schema.properties = {}
if (!arg.has_default) {
schema.required.push(arg.name)
}
}
} catch (err) {
console.error(err)
sendUserToast(`Could not infer schema: ${err.body ?? err}`, true)
}
}
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
function array_move<T>(arr: T[], fromIndex: number, toIndex: number) {
var element = arr[fromIndex]
arr.splice(fromIndex, 1)
arr.splice(toIndex, 0, element)
if (!arg.has_default) {
schema.required.push(arg.name)
}
}
} catch (err) {
console.error(err)
sendUserToast(`Could not infer schema: ${err.body ?? err}`, true)
}
}
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,92 +1,94 @@
<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
let disposeMethod: () => void | undefined;
export let deno = false
export let lang = deno ? 'typescript' : '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
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();
if (lang == 'python') {
closeWebsockets()
if (lang == 'python' || deno) {
// 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({
name: name,
clientOptions: {
documentSelector: ['python'],
documentSelector: deno ? ['typescript'] : ['python'],
errorHandler: {
error: () => ErrorAction.Shutdown,
closed: () => CloseAction.Restart
@@ -95,79 +97,100 @@
isTrusted: true
},
// workspaceFolder: { uri: Uri.parse(`/tmp/${name}`), name: 'tmp', index: 0 },
initializationOptions
initializationOptions,
middleware: {
workspace: {
configuration: (params, token, configuration) => {
return [
{
enable: true
}
]
}
}
}
},
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', {
executionEnvironments: [
{
root: '/tmp/pyright',
pythonVersion: '3.7',
pythonPlatform: 'platform',
extraPaths: []
}
]
});
connectToLanguageServer(`wss://${$page.url.host}/ws/black`, 'black', {
formatters: {
black: {
command: 'black',
args: ['--quiet', '-']
if (deno) {
connectToLanguageServer(`ws://${$page.url.host}/ws/deno`, 'deno', {
deno: {
enable: true,
lint: true
}
},
formatFiletypes: {
python: 'black'
}
});
})
} else {
connectToLanguageServer(`wss://${$page.url.host}/ws/pyright`, 'pyright', {
executionEnvironments: [
{
root: '/tmp/pyright',
pythonVersion: '3.7',
pythonPlatform: 'platform',
extraPaths: []
}
]
})
connectToLanguageServer(`wss://${$page.url.host}/ws/black`, 'black', {
formatters: {
black: {
command: 'black',
args: ['--quiet', '-']
}
},
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 +198,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 +229,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,19 +251,24 @@
allowComments: false,
schemas: [],
enableSchemaRequest: true
});
})
}
if (lang == 'typescript') {
// compiler options
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
target: monaco.languages.typescript.ScriptTarget.ES6,
allowNonTsExtensions: true,
noLib: true
});
if (deno) {
monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
diagnosticCodesToIgnore: [2691]
})
} else {
// compiler options
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
target: monaco.languages.typescript.ScriptTarget.ES6,
allowNonTsExtensions: true,
noLib: true
})
monaco.languages.typescript.typescriptDefaults.addExtraLib(
`
monaco.languages.typescript.typescriptDefaults.addExtraLib(
`
/**
* get variable (including secret) at path
* @param {string} path - path of the variable (e.g: g/all/pretty_secret)
@@ -276,40 +304,40 @@ export const previous_result: any;
*/
export const params: any;
`,
'file:///node_modules/@types/windmill/index.d.ts'
);
'file:///node_modules/@types/windmill/index.d.ts'
)
}
}
if (lang == 'python' || deno) {
const { MonacoServices } = await import('@codingame/monaco-languageclient')
MonacoServices.install(monaco)
}
if (lang == 'python') {
const { MonacoServices } = await import('@codingame/monaco-languageclient');
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,24 @@
<script lang="ts">
import { workspaceStore } from '../../stores';
import { workspaceStore } from '../../stores'
import { createEventDispatcher } from 'svelte';
import Modal from '../../routes/components/Modal.svelte'
import { type 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 +27,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 +43,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 +86,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">

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