fix: consistent exists/{resource} addition + usage in frontend

This commit is contained in:
Ruben Fiszel
2022-07-18 17:21:34 +02:00
parent 2b712e6ce5
commit 10e9549ded
20 changed files with 523 additions and 103 deletions
+110 -8
View File
@@ -379,10 +379,10 @@ paths:
schema:
type: string
/workspaces/validate_id:
/workspaces/exists:
post:
summary: validate id
operationId: validateId
summary: exists workspace
operationId: existsWorkspace
tags:
- workspace
requestBody:
@@ -403,12 +403,12 @@ paths:
content:
text/plain:
schema:
type: string
type: boolean
/workspaces/validate_username:
/workspaces/exists_username:
post:
summary: validate username
operationId: validateUsername
summary: exists username
operationId: existsUsername
tags:
- workspace
requestBody:
@@ -431,7 +431,7 @@ paths:
content:
text/plain:
schema:
type: string
type: boolean
/users/email:
get:
@@ -934,6 +934,23 @@ paths:
schema:
$ref: "#/components/schemas/ListableVariable"
/w/{workspace}/variables/exists/{path}:
get:
summary: does variable exists at path
operationId: existsVariable
tags:
- variable
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
responses:
"200":
description: variable
content:
application/json:
schema:
type: boolean
/w/{workspace}/variables/list:
get:
summary: list variables
@@ -1304,6 +1321,23 @@ paths:
schema:
$ref: "#/components/schemas/Resource"
/w/{workspace}/resources/exists/{path}:
get:
summary: does resource exists
operationId: existsResource
tags:
- resource
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
responses:
"200":
description: does resource exists
content:
application/json:
schema:
type: boolean
/w/{workspace}/resources/list:
get:
summary: list resources
@@ -1410,6 +1444,23 @@ paths:
schema:
$ref: "#/components/schemas/ResourceType"
/w/{workspace}/resources/type/exists/{path}:
get:
summary: does resource_type exists
operationId: existsResourceType
tags:
- resource
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
responses:
"200":
description: does resource_type exist
content:
application/json:
schema:
type: boolean
/w/{workspace}/resources/type/list:
get:
summary: list resource_types
@@ -1731,6 +1782,23 @@ paths:
schema:
$ref: "#/components/schemas/Script"
/w/{workspace}/scripts/exists/p/{path}:
get:
summary: exists script by path
operationId: existsScriptByPath
tags:
- script
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
responses:
"200":
description: does it exists
content:
application/json:
schema:
type: boolean
/w/{workspace}/scripts/get/h/{hash}:
get:
summary: get script by hash
@@ -1906,6 +1974,23 @@ paths:
schema:
$ref: "#/components/schemas/Flow"
/w/{workspace}/flows/exists/{path}:
get:
summary: exists flow by path
operationId: existsFlowByPath
tags:
- flow
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
responses:
"200":
description: flow details
content:
application/json:
schema:
type: boolean
/w/{workspace}/flows/create:
post:
summary: create flow
@@ -2462,6 +2547,23 @@ paths:
schema:
$ref: "#/components/schemas/Schedule"
/w/{workspace}/schedules/exists/{path}:
get:
summary: does schedule exists
operationId: existsSchedule
tags:
- schedule
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
responses:
"200":
description: schedule deleted
content:
application/json:
schema:
type: boolean
/w/{workspace}/schedules/list:
get:
summary: list schedules
+126
View File
@@ -62,6 +62,27 @@
]
}
},
"0784bb86a503f02b9ef247a2b83a82ddfa49632552b223a9f4536a449b0a1eb8": {
"query": "SELECT EXISTS(SELECT 1 FROM resource WHERE path = $1 AND workspace_id = $2)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
null
]
}
},
"09246e9ff5b2beb61ab51a5f73d980f7638904d5a18a415e52d5e1c94dffd0aa": {
"query": "SELECT SUM(duration) FROM completed_job WHERE created_by = $1 AND created_at > NOW() - INTERVAL '1200 seconds' AND workspace_id = $2",
"describe": {
@@ -681,6 +702,27 @@
"nullable": []
}
},
"355dcb2cbebd13f0e3bdd4929b9e431b0e6d72716d1c4f9ab6af6adce5b5e4b3": {
"query": "SELECT EXISTS(SELECT 1 FROM resource_type WHERE name = $1 AND workspace_id = $2)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
null
]
}
},
"37d3ee8009055e869941e548a6d5a352053a5d7782f662c34b94706488abccb6": {
"query": "UPDATE queue SET running = false WHERE last_ping < $1 RETURNING id",
"describe": {
@@ -1691,6 +1733,27 @@
]
}
},
"88a3f58a1a315200fdd2e4bb8638246ee21818f8aaaf56f6e9d7ddce1490d886": {
"query": "SELECT EXISTS(SELECT 1 FROM variable WHERE path = $1 AND workspace_id = $2)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
null
]
}
},
"88b7589a6416da8be4b26af3bf30fcfcd6aeae7bc5a37e9a735cabbe2691c570": {
"query": "SELECT login_type::TEXT FROM password WHERE email = $1",
"describe": {
@@ -2095,6 +2158,27 @@
]
}
},
"a38059dc3574da498ce986c916b6d385b1f18d5bd659ef13c43fafa9daff6bda": {
"query": "SELECT EXISTS(SELECT 1 FROM flow WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter'))",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
null
]
}
},
"a98b2d68f023f46ab91167d3147416df672c2aed2ba5ab70e98a9da5fa47255a": {
"query": "INSERT INTO workspace_settings\n (workspace_id)\n VALUES ($1)",
"describe": {
@@ -2931,6 +3015,27 @@
]
}
},
"e587299612077e6de39b5a501aa8a736cae4a21f11853f8ae66a610b4e65f49e": {
"query": "SELECT EXISTS(SELECT 1 FROM script WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter') AND\n created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter')))",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
null
]
}
},
"e94abd39ec51b7e0c48c190d47ed766fd4f401187c3b60b3e599426c95232f7f": {
"query": "UPDATE queue SET last_ping = $1 WHERE id = $2",
"describe": {
@@ -2944,6 +3049,27 @@
"nullable": []
}
},
"ee99cb974fb35d2d92fa96cecb53b8ad3a729ac710f362dc55a576613b66f9be": {
"query": "SELECT EXISTS(SELECT 1 FROM schedule WHERE path = $1 AND workspace_id = $2)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
null
]
}
},
"f056b5f3e66a764748925f1bfd3180923fde8c7fdf69088d0e4a5555cc049545": {
"query": "SELECT result FROM completed_job WHERE id = $1 AND workspace_id = $2",
"describe": {
+20 -1
View File
@@ -20,7 +20,7 @@ use sqlx::FromRow;
use crate::{
audit::{audit_log, ActionKind},
db::UserDB,
db::{UserDB, DB},
error::{Error, JsonResult, Result},
jobs::RawCode,
scripts::Schema,
@@ -35,6 +35,7 @@ pub fn workspaced_service() -> Router {
.route("/update/*path", post(update_flow))
.route("/archive/*path", post(archive_flow_by_path))
.route("/get/*path", get(get_flow_by_path))
.route("/exists/*path", get(exists_flow_by_path))
}
#[derive(FromRow, Serialize)]
@@ -271,6 +272,24 @@ async fn get_flow_by_path(
Ok(Json(flow))
}
async fn exists_flow_by_path(
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<bool> {
let path = path.to_path();
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM flow WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter'))",
path,
w_id
)
.fetch_one(&db)
.await?
.unwrap_or(false);
Ok(Json(exists))
}
async fn archive_flow_by_path(
authed: Authed,
Extension(user_db): Extension<UserDB>,
+6 -2
View File
@@ -79,9 +79,10 @@ pub async fn initialize_tracing() -> anyhow::Result<()> {
tracing_init::initialize_tracing().await
}
#[derive(Clone)]
struct BaseUrl(String);
struct CloudHosted(bool);
pub async fn run_server(
db: DB,
addr: SocketAddr,
@@ -115,7 +116,10 @@ pub async fn run_server(
.layer(Extension(user_db))
.layer(Extension(auth_cache.clone()))
.layer(Extension(basic_clients))
.layer(Extension(BaseUrl(base_url.to_string())))
.layer(Extension(Arc::new(BaseUrl(base_url.to_string()))))
.layer(Extension(Arc::new(CloudHosted(
std::env::var("CLOUD_HOSTED").is_ok(),
))))
.layer(Extension(http_client))
.layer(CookieManagerLayer::new());
// build our application with a route
+2 -2
View File
@@ -597,7 +597,7 @@ async fn slack_command(
SlackSig { sig, ts }: SlackSig,
Extension(slack_verifier): Extension<Arc<Option<SlackVerifier>>>,
Extension(db): Extension<DB>,
Extension(base_url): Extension<BaseUrl>,
Extension(base_url): Extension<Arc<BaseUrl>>,
body: Bytes,
) -> error::Result<String> {
let form: SlackCommand = serde_urlencoded::from_bytes(&body)
@@ -651,7 +651,7 @@ async fn slack_command(
)
.await?;
tx.commit().await?;
let url = base_url.0;
let url = base_url.0.to_owned();
return Ok(format!("Job launched. See details at {url}/run/{uuid}"));
}
}
+36
View File
@@ -26,6 +26,7 @@ pub fn workspaced_service() -> Router {
Router::new()
.route("/list", get(list_resources))
.route("/get/*path", get(get_resource))
.route("/exists/*path", get(exists_resource))
.route("/get_value/*path", get(get_resource_value))
.route("/update/*path", post(update_resource))
.route("/delete/*path", delete(delete_resource))
@@ -33,6 +34,7 @@ pub fn workspaced_service() -> Router {
.route("/type/list", get(list_resource_types))
.route("/type/listnames", get(list_resource_types_names))
.route("/type/get/:name", get(get_resource_type))
.route("/type/exists/:name", get(exists_resource_type))
.route("/type/update/:name", post(update_resource_type))
.route("/type/delete/:name", delete(delete_resource_type))
.route("/type/create", post(create_resource_type))
@@ -150,6 +152,24 @@ async fn get_resource(
Ok(Json(resource))
}
async fn exists_resource(
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<bool> {
let path = path.to_path();
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM resource WHERE path = $1 AND workspace_id = $2)",
path,
w_id
)
.fetch_one(&db)
.await?
.unwrap_or(false);
Ok(Json(exists))
}
async fn get_resource_value(
authed: Authed,
Extension(user_db): Extension<UserDB>,
@@ -325,6 +345,22 @@ async fn get_resource_type(
Ok(Json(resource_type))
}
async fn exists_resource_type(
Extension(db): Extension<DB>,
Path((w_id, name)): Path<(String, String)>,
) -> JsonResult<bool> {
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM resource_type WHERE name = $1 AND workspace_id = $2)",
name,
w_id
)
.fetch_one(&db)
.await?
.unwrap_or(false);
Ok(Json(exists))
}
async fn create_resource_type(
authed: Authed,
Extension(user_db): Extension<UserDB>,
+20 -1
View File
@@ -9,7 +9,7 @@ use std::str::FromStr;
use crate::{
audit::{audit_log, ActionKind},
db::UserDB,
db::{UserDB, DB},
error::{self, JsonResult, Result},
jobs::{self, push, JobPayload},
users::Authed,
@@ -30,6 +30,7 @@ pub fn workspaced_service() -> Router {
Router::new()
.route("/list", get(list_schedule))
.route("/get/*path", get(get_schedule))
.route("/exists/*path", get(exists_schedule))
.route("/create", post(create_schedule))
.route("/update/*path", post(edit_schedule))
.route("/setenabled/*path", post(set_enabled))
@@ -284,6 +285,24 @@ async fn get_schedule(
Ok(Json(schedule))
}
async fn exists_schedule(
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<bool> {
let path = path.to_path();
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM schedule WHERE path = $1 AND workspace_id = $2)",
path,
w_id
)
.fetch_one(&db)
.await?
.unwrap_or(false);
Ok(Json(exists))
}
#[derive(Deserialize)]
pub struct PreviewPayload {
pub schedule: String,
+22 -1
View File
@@ -17,7 +17,7 @@ use crate::{
utils::{require_admin, Pagination, StripPath},
};
use axum::{
extract::{Extension, Path, Query},
extract::{Extension, Host, Path, Query},
routing::{get, post},
Json, Router,
};
@@ -51,6 +51,7 @@ pub fn workspaced_service() -> Router {
.route("/create", post(create_script))
.route("/archive/p/*path", post(archive_script_by_path))
.route("/get/p/*path", get(get_script_by_path))
.route("/exists/p/*path", get(exists_script_by_path))
.route("/archive/h/:hash", post(archive_script_by_hash))
.route("/delete/h/:hash", post(delete_script_by_hash))
.route("/get/h/:hash", get(get_script_by_hash))
@@ -263,6 +264,7 @@ async fn list_hub_scripts(
Authed {
email, username, ..
}: Authed,
Host(host): Host,
) -> JsonResult<Vec<ScriptSearch>> {
let http_client = reqwest::ClientBuilder::new()
.user_agent("windmill/beta")
@@ -272,6 +274,7 @@ async fn list_hub_scripts(
.get("https://hub.windmill.dev/searchData?approved=true")
.header("X-email", email.unwrap_or_else(|| "".to_string()))
.header("X-username", username)
.header("X-hostname", host)
.send()
.await
.map_err(to_anyhow)?
@@ -540,6 +543,24 @@ async fn get_script_by_path(
Ok(Json(script))
}
async fn exists_script_by_path(
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<bool> {
let path = path.to_path();
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM script WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter') AND
created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter')))",
path, w_id
)
.fetch_one(&db)
.await?
.unwrap_or(false);
Ok(Json(exists))
}
async fn get_script_by_hash_internal<'c>(
db: &mut Transaction<'c, Postgres>,
workspace_id: &str,
+19
View File
@@ -32,6 +32,7 @@ pub fn workspaced_service() -> Router {
.route("/list", get(list_variables))
.route("/list_contextual", get(list_contextual_variables))
.route("/get/*path", get(get_variable))
.route("/exists/*path", get(exists_variable))
.route("/update/*path", post(update_variable))
.route("/delete/*path", delete(delete_variable))
.route("/create", post(create_variable))
@@ -254,6 +255,24 @@ async fn get_variable(
Ok(Json(r))
}
async fn exists_variable(
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<bool> {
let path = path.to_path();
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM variable WHERE path = $1 AND workspace_id = $2)",
path,
w_id
)
.fetch_one(&db)
.await?
.unwrap_or(false);
Ok(Json(exists))
}
async fn create_variable(
authed: Authed,
Extension(user_db): Extension<UserDB>,
+2 -26
View File
@@ -41,10 +41,7 @@ pub fn global_service() -> Router {
.route("/users", get(user_workspaces))
.route("/create", post(create_workspace))
.route("/exists", post(exists_workspace))
.route("/validate_username", post(validate_username))
.route("/validate_id", post(validate_id))
.route("/exists_username", post(exists_username))
}
#[derive(FromRow, Serialize)]
@@ -485,7 +482,7 @@ async fn delete_invite(
))
}
async fn validate_username(
async fn exists_username(
Extension(db): Extension<DB>,
Json(vu): Json<ValidateUsername>,
) -> Result<String> {
@@ -506,27 +503,6 @@ async fn validate_username(
Ok("valid username".to_string())
}
async fn validate_id(
Extension(db): Extension<DB>,
Json(wi): Json<WorkspaceId>,
) -> Result<String> {
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM workspace WHERE id = $1)",
wi.id
)
.fetch_one(&db)
.await?
.unwrap_or(true);
if exists {
return Err(Error::BadRequest("id already taken".to_string()))
}
Ok("valid workspace".to_string())
}
#[derive(Serialize)]
struct ScriptMetadata {
summary: String,
+20 -20
View File
@@ -36,6 +36,9 @@
let step = 1
let no_back = false
let pathError = ''
export function open() {
step = 1
value = ''
@@ -72,27 +75,18 @@
} else if (step == 1 && !manual) {
window.location.href = `/api/oauth/connect/${resource_type}?scopes=${scopes.join('+')}`
} else {
let exists = true
try {
await VariableService.getVariable({
workspace: $workspaceStore!,
path
})
} catch (e) {
exists = false
}
let exists = await VariableService.existsVariable({
workspace: $workspaceStore!,
path
})
if (exists) {
throw Error(`Variable at path ${path} already exists. Delete it or pick another path`)
}
exists = true
try {
await ResourceService.getResource({
workspace: $workspaceStore!,
path
})
} catch (e) {
exists = false
}
exists = await ResourceService.existsResource({
workspace: $workspaceStore!,
path
})
if (exists) {
throw Error(`Resource at path ${path} already exists. Delete it or pick another path`)
}
@@ -235,7 +229,12 @@
</div>
{/if}
{:else}
<Path bind:path initialPath={`u/${$userStore?.username ?? ''}/my_${resource_type}`} />
<Path
bind:error={pathError}
bind:path
initialPath={`u/${$userStore?.username ?? ''}/my_${resource_type}`}
kind="resource"
/>
<ul class="mt-10 bg-white">
<li>
1. A secret variable containing the token <span class="font-bold"
@@ -266,7 +265,8 @@
<button
class="default-button px-4 py-2 font-semibold"
class:default-button-disabled={(step == 1 && resource_type == '') ||
(step == 2 && value == '')}
(step == 2 && value == '') ||
(step == 3 && pathError != '')}
on:click={next}
>
{step == 3 ? 'Connect' : 'Next'}
+13 -4
View File
@@ -3,7 +3,7 @@
import { page } from '$app/stores'
import { FlowService, ScriptService, type Flow } from '$lib/gen'
import { clearPreviewResults, hubScripts, workspaceStore } from '$lib/stores'
import { sendUserToast } from '$lib/utils'
import { pathIsEmpty, sendUserToast } from '$lib/utils'
import { onMount } from 'svelte'
import SvelteMarkdown from 'svelte-markdown'
import FlowEditor from './FlowEditor.svelte'
@@ -18,6 +18,8 @@
let mode: FlowMode
let pathError = ''
$: step = Number($page.url.searchParams.get('step')) || 1
async function loadSearchData() {
@@ -92,6 +94,7 @@
}}>Step 1: Metadata</button
>
<button
disabled={pathError != ''}
class="{step === 2
? 'default-button-disabled text-gray-700'
: 'default-button-secondary'} min-w-max ml-2"
@@ -100,6 +103,7 @@
}}>Step 2: Flow</button
>
<button
disabled={pathError != ''}
class="{step === 3
? 'default-button-disabled text-gray-700'
: 'default-button-secondary'} min-w-max ml-2"
@@ -111,8 +115,7 @@
<div class="flex flex-row-reverse ml-2">
{#if step != 3}
<button
disabled={step == 1 &&
(flow.path == undefined || flow.path == '' || flow.path.split('/')[2] == '')}
disabled={step == 1 && pathError != ''}
class="default-button px-6 max-h-8"
on:click={() => {
changeStep(step + 1)
@@ -141,7 +144,13 @@
<!-- metadata -->
{#if step === 1}
<div class="grid grid-cols-1 gap-6 max-w-7xl">
<Path bind:path={flow.path} {initialPath} namePlaceholder="example/my/flow">
<Path
bind:error={pathError}
bind:path={flow.path}
{initialPath}
namePlaceholder="example/my/flow"
kind="flow"
>
<div slot="ownerToolkit" class="text-gray-700 text-2xs">
Flow permissions depend on their path. Select the group <span class="font-mono">all</span>
to share your flow, and <span class="font-mono">user</span> to keep it private.
+65 -10
View File
@@ -1,12 +1,21 @@
<script lang="ts">
import { type Meta, pathToMeta } from '$lib/common'
import type { Group } from '$lib/gen'
import {
FlowService,
ResourceService,
ScheduleService,
ScriptService,
VariableService,
type Group
} from '$lib/gen'
import { GroupService } from '$lib/gen'
import Tooltip from './Tooltip.svelte'
import { userStore, workspaceStore } from '$lib/stores'
import { sleep } from '$lib/utils'
import { workspace } from 'vscode'
type PathKind = 'resource' | 'script' | 'variable' | 'flow' | 'schedule'
export let meta: Meta = {
ownerKind: 'user',
owner: '',
@@ -15,9 +24,11 @@
export let namePlaceholder = ''
export let initialPath: string
export let path = ''
export let error = ''
export let kind: PathKind
let groups: Group[] = []
let error = ''
$: {
path = [meta.ownerKind === 'group' ? 'g' : 'u', meta.owner, meta.name].join('/')
@@ -26,6 +37,7 @@
export function getPath() {
return path
}
export async function reset() {
if (path == '' || path == 'u//') {
meta.ownerKind = 'user'
@@ -40,23 +52,66 @@
}
}
$: validateName(meta)
$: validate(meta, path, kind)
async function loadGroups(): Promise<void> {
groups = await GroupService.listGroups({ workspace: $workspaceStore! })
meta.owner = meta.owner
}
function validateName(meta: Meta): void {
async function validate(meta: Meta, path: string, kind: PathKind) {
validateName(meta) && (await validatePath(path, kind))
}
let validateTimeout: NodeJS.Timeout | undefined = undefined
async function validatePath(path: string, kind: PathKind): Promise<void> {
if (initialPath == '' && initialPath != path) {
if (validateTimeout) {
clearTimeout(validateTimeout)
}
validateTimeout = setTimeout(async () => {
if (
(kind == 'flow' &&
(await FlowService.existsFlowByPath({ workspace: $workspaceStore!, path: path }))) ||
(kind == 'script' &&
(await ScriptService.existsScriptByPath({
workspace: $workspaceStore!,
path: path
}))) ||
(kind == 'resource' &&
(await ResourceService.existsResource({
workspace: $workspaceStore!,
path: path
}))) ||
(kind == 'variable' &&
(await VariableService.existsVariable({
workspace: $workspaceStore!,
path: path
}))) ||
(kind == 'schedule' &&
(await ScheduleService.existsSchedule({ workspace: $workspaceStore!, path: path })))
) {
error = 'path already used'
} else if (validateName(meta)) {
error = ''
}
validateTimeout = undefined
}, 500)
} else {
error = ''
}
}
function validateName(meta: Meta): boolean {
if (meta.name == undefined || meta.name == '') {
error = 'choose a name'
return
}
const regex = new RegExp(/^[\w-]+(\/[\w-]+)*$/)
if (regex.test(meta.name)) {
error = ''
return false
} else if (!/^[\w-]+(\/[\w-]+)*$/.test(meta.name)) {
error = 'This name is not valid.'
return false
} else {
error = 'This name is not valid. '
return true
}
}
@@ -16,6 +16,7 @@
let path = ''
let initialPath = ''
let pathError = ''
let step = 1
@@ -126,7 +127,13 @@ A good way to make resources user friendly is to link to a default script for yo
<div>
<span class="text-purple-500 text-2xs grow">{error ?? ''}</span>
<span class="mb-1 font-semibold text-gray-700">Path</span>
<Path bind:path {initialPath} namePlaceholder="my/resource">
<Path
bind:error={pathError}
bind:path
{initialPath}
namePlaceholder="my/resource"
kind="resource"
>
<div slot="ownerToolkit" class="text-gray-700 text-2xs">
Resource permissions depend on their path. Select the group <span class="font-mono"
>all</span
@@ -195,19 +202,16 @@ A good way to make resources user friendly is to link to a default script for yo
</div>
<span slot="submission">
{#if step === 1}
{#if selectedResourceType && path != undefined && path != '' && path.split('/')[2] != ''}
<button
class="default-button px-4 py-2 font-semibold"
on:click={async () => {
await loadResourceType()
step = 2
}}
>
Next
</button>
{:else}
<button class="default-button-disabled px-4 py-2 font-semibold">Next</button>
{/if}
<button
class="default-button px-4 py-2 font-semibold"
on:click={async () => {
await loadResourceType()
step = 2
}}
disabled={selectedResourceType == undefined || pathError != ''}
>
Next
</button>
{:else}
<button
class="default-button-secondary px-4 py-2 font-semibold"
@@ -1,7 +1,7 @@
<script lang="ts">
import { ScriptService, type Script } from '$lib/gen'
import { emptySchema, sendUserToast } from '$lib/utils'
import { emptySchema, pathIsEmpty, sendUserToast } from '$lib/utils'
import { onDestroy } from 'svelte'
import ScriptEditor from './ScriptEditor.svelte'
import { page } from '$app/stores'
@@ -22,6 +22,8 @@
export let script: Script
export let initialPath: string = ''
let pathError = ''
$: {
$page.url.searchParams.set('state', btoa(JSON.stringify(script)))
history.replaceState({}, '', $page.url)
@@ -100,6 +102,7 @@
}}>Step 1: Metadata</button
>
<button
disabled={pathError != ''}
class="{step === 2
? 'default-button-disabled text-gray-700'
: 'default-button-secondary'} min-w-max ml-2"
@@ -108,6 +111,7 @@
}}>Step 2: Code</button
>
<button
disabled={pathError != ''}
class="{step === 3
? 'default-button-disabled text-gray-700'
: 'default-button-secondary'} min-w-max ml-2"
@@ -119,8 +123,7 @@
<div class="flex flex-row-reverse ml-2">
{#if step != 3}
<button
disabled={step == 1 &&
(script.path == undefined || script.path == '' || script.path.split('/')[2] == '')}
disabled={step == 1 && pathError != ''}
class="default-button px-6 max-h-8"
on:click={() => {
changeStep(step + 1)
@@ -152,7 +155,13 @@
<!-- metadata -->
{#if step === 1}
<div class="grid grid-cols-1 gap-6 max-w-7xl">
<Path bind:path={script.path} {initialPath} namePlaceholder="example/my/script">
<Path
bind:error={pathError}
bind:path={script.path}
{initialPath}
namePlaceholder="example/my/script"
kind="script"
>
<div slot="ownerToolkit" class="text-gray-700 text-2xs">
Script permissions depend on their path. Select the group <span class="font-mono"
>all</span
@@ -27,6 +27,7 @@
let modal: Modal
let edit = false
let initialPath: string
let pathError = ''
export function initNew(): void {
variable = {
@@ -122,7 +123,13 @@
<div class="flex flex-col gap-2 ">
<div>
<div class="text-gray-700 mb-0 pb-0">path</div>
<Path bind:path {initialPath} namePlaceholder="my/variable" />
<Path
bind:error={pathError}
bind:path
{initialPath}
namePlaceholder="my/variable"
kind="variable"
/>
</div>
<label class="block pb-6">
@@ -160,7 +167,7 @@
createVariable()
}
}}
disabled={!valid}
disabled={!valid || pathError != ''}
>
{#if edit}
Save
+4
View File
@@ -190,6 +190,10 @@ export function elapsedSinceSecs(date: string): number {
return Math.round((new Date().getTime() - new Date(date).getTime()) / 1000)
}
export function pathIsEmpty(path: string): boolean {
return path == undefined || path.split('/')[2] == ''
}
export function groupBy<T>(
scripts: T[],
toGroup: (t: T) => string,
+10 -3
View File
@@ -34,6 +34,7 @@
let preview: string[] = []
let path: string = ''
let pathError = ''
const offset = new Date().getTimezoneOffset()
@@ -143,7 +144,13 @@
{#if !edit}
<h2>Save schedule as</h2>
<Path bind:path {initialPath} namePlaceholder={'my/schedule'}>
<Path
bind:error={pathError}
bind:path
{initialPath}
namePlaceholder={'my/schedule'}
kind="schedule"
>
<div slot="ownerToolkit" class="text-gray-700 text-2xs">
Schedule permissions depend on their path. Select the group <span class="font-mono"
>all</span
@@ -213,8 +220,8 @@
<div>
<button
type="submit"
disabled={!allowSchedule}
class="{allowSchedule ? 'default-button' : 'default-button-disabled'} w-min px-6"
disabled={!allowSchedule || pathError != ''}
class="default-button w-min px-6"
on:click={scheduleScript}
>
{edit ? 'Save' : 'Schedule'}
@@ -28,7 +28,7 @@
async function validateName(username: string): Promise<void> {
try {
await WorkspaceService.validateUsername({ requestBody: { id: workspace_id, username } })
await WorkspaceService.existsUsername({ requestBody: { id: workspace_id, username } })
errorUsername = ''
} catch {
errorUsername = 'username already exists'
@@ -21,13 +21,16 @@
$: validateName(id)
async function validateName(id: string): Promise<void> {
try {
await WorkspaceService.validateId({ requestBody: { id } })
errorId = ''
} catch {
let exists = await WorkspaceService.existsWorkspace({ requestBody: { id } })
if (exists) {
errorId = 'id already exists'
} else if (id != '' && !/^\w+(-\w+)*$/.test(id)) {
errorId = 'id can only contain letters, numbers and dashes and must not finish by a dash'
} else {
errorId = ''
}
}
async function createWorkspace(): Promise<void> {
await WorkspaceService.createWorkspace({
requestBody: {