feat: alpha hub integration + frontend user store fixes + script client base_url fix

This commit is contained in:
Ruben Fiszel
2022-06-12 01:55:05 +02:00
parent fbc5f3862a
commit 193a26cfad
31 changed files with 333 additions and 194 deletions

View File

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

View File

@@ -0,0 +1,2 @@
-- Add up migration script here
ALTER TYPE JOB_KIND ADD VALUE 'script_hub';

View File

@@ -1210,6 +1210,52 @@ paths:
items:
type: string
/scripts/hub/list:
get:
summary: list all available hub scripts
operationId: listHubScripts
tags:
- script
responses:
"200":
description: hub scripts list
content:
application/json:
schema:
type: array
items:
type: object
properties:
id:
type: number
summary:
type: string
app:
type: string
approved:
type: boolean
required:
- id
- summary
- app
- approved
/scripts/hub/get/{path}:
get:
summary: get hub script content by path
operationId: getHubScriptContentByPath
tags:
- script
parameters:
- $ref: "#/components/parameters/ScriptPath"
responses:
"200":
description: script details
content:
text/plain:
schema:
type: string
/w/{workspace}/scripts/list:
get:
summary: list all available scripts

View File

@@ -12,7 +12,7 @@ use sqlx::{query_scalar, Postgres, Transaction};
use std::collections::HashMap;
use crate::js_eval::eval_timeout;
use crate::scripts::ScriptLang;
use crate::scripts::{get_hub_script_by_path, ScriptLang};
use crate::users::create_token_for_owner;
use crate::{
audit::{audit_log, ActionKind},
@@ -167,14 +167,11 @@ pub async fn run_job_by_path(
) -> error::Result<(StatusCode, String)> {
let script_path = script_path.to_path();
let mut tx = user_db.begin(&authed).await?;
let script_hash = get_latest_hash_for_path(&mut tx, &w_id, script_path).await?;
let job_payload = script_path_to_payload(script_path, &mut tx, &w_id).await?;
let (uuid, tx) = push(
tx,
&w_id,
JobPayload::ScriptHash {
hash: script_hash,
path: script_path.to_owned(),
},
job_payload,
args,
&authed.username,
owner_to_token_owner(&authed.username, false),
@@ -188,6 +185,25 @@ pub async fn run_job_by_path(
Ok((StatusCode::CREATED, uuid.to_string()))
}
async fn script_path_to_payload<'c>(
script_path: &str,
db: &mut Transaction<'c, Postgres>,
w_id: &String,
) -> Result<JobPayload, Error> {
let job_payload = if script_path.starts_with("hub/") {
JobPayload::ScriptHub {
path: script_path.to_owned(),
}
} else {
let script_hash = get_latest_hash_for_path(db, w_id, script_path).await?;
JobPayload::ScriptHash {
hash: script_hash,
path: script_path.to_owned(),
}
};
Ok(job_payload)
}
pub async fn get_latest_hash_for_path<'c>(
db: &mut Transaction<'c, Postgres>,
w_id: &str,
@@ -816,6 +832,7 @@ enum Job {
#[serde(rename_all(serialize = "lowercase"))]
pub enum JobKind {
Script,
Script_Hub,
Preview,
Dependencies,
Flow,
@@ -954,6 +971,9 @@ struct PreviewFlow {
}
pub enum JobPayload {
ScriptHub {
path: String,
},
ScriptHash {
hash: ScriptHash,
path: String,
@@ -1030,6 +1050,25 @@ pub async fn push<'c>(
Some(language),
)
}
JobPayload::ScriptHub { path } => (
None,
Some(path.clone()),
Some(
get_hub_script_by_path(
Authed {
email: Some("".to_string()),
username: user.to_string(),
is_admin: false,
groups: vec![],
},
Path(StripPath(path)),
)
.await?,
),
JobKind::Script_Hub,
None,
Some(ScriptLang::Deno),
),
JobPayload::Code(RawCode {
content,
path,
@@ -1446,12 +1485,7 @@ async fn push_next_flow_job(
let mut tx = db.begin().await?;
let job_payload = match &module.value {
FlowModuleValue::Script { path: script_path } => {
let script_hash =
get_latest_hash_for_path(&mut tx, &job.workspace_id, script_path).await?;
JobPayload::ScriptHash {
hash: script_hash,
path: script_path.to_owned(),
}
script_path_to_payload(script_path, &mut tx, &job.workspace_id).await?
}
a @ _ => {
tracing::info!("Unrecognized module values {:?}", a);

View File

@@ -11,7 +11,7 @@ use sql_builder::prelude::*;
use crate::{
audit::{audit_log, ActionKind},
db::{UserDB, DB},
error::{Error, JsonResult, Result},
error::{to_anyhow, Error, JsonResult, Result},
jobs, parser,
users::{owner_to_token_owner, truncate_token, Authed, Tokened},
utils::{require_admin, Pagination, StripPath},
@@ -41,6 +41,8 @@ pub fn global_service() -> Router {
post(parse_python_code_to_jsonschema),
)
.route("/deno/tojsonschema", post(parse_deno_code_to_jsonschema))
.route("/hub/list", get(list_hub_scripts))
.route("/hub/get/*path", get(get_hub_script_by_path))
}
pub fn workspaced_service() -> Router {
@@ -241,6 +243,41 @@ async fn list_scripts(
Ok(Json(rows))
}
#[derive(Deserialize, Serialize)]
struct SearchData {
asks: Vec<ScriptSearch>,
}
#[derive(Deserialize, Serialize)]
struct ScriptSearch {
id: i32,
summary: String,
app: String,
approved: bool,
}
async fn list_hub_scripts(
Authed {
email, username, ..
}: Authed,
) -> JsonResult<Vec<ScriptSearch>> {
let http_client = reqwest::ClientBuilder::new()
.user_agent("windmill/beta")
.build()
.map_err(to_anyhow)?;
let rows = http_client
.get("https://hub.windmill.dev/searchData?approved=true")
.header("X-email", email.unwrap_or_else(|| "".to_string()))
.header("X-username", username)
.send()
.await
.map_err(to_anyhow)?
.json::<SearchData>()
.await
.map_err(to_anyhow)?
.asks;
Ok(Json(rows))
}
fn hash_script(ns: &NewScript) -> i64 {
let mut dh = DefaultHasher::new();
ns.hash(&mut dh);
@@ -448,6 +485,34 @@ async fn create_script(
Ok((StatusCode::CREATED, format!("{}", hash)))
}
pub async fn get_hub_script_by_path(
Authed {
email, username, ..
}: Authed,
Path(path): Path<StripPath>,
) -> Result<String> {
let path = path
.to_path()
.strip_prefix("hub/")
.ok_or_else(|| Error::BadRequest("Impossible to remove prefix hex".to_string()))?;
let http_client = reqwest::ClientBuilder::new()
.user_agent("windmill/beta")
.build()
.map_err(to_anyhow)?;
let content = http_client
.get(format!("https://hub.windmill.dev/raw/{path}.ts"))
.header("X-email", email.unwrap_or_else(|| "".to_string()))
.header("X-username", username)
.send()
.await
.map_err(to_anyhow)?
.text()
.await
.map_err(to_anyhow)?;
Ok(content)
}
async fn get_script_by_path(
authed: Authed,
Extension(user_db): Extension<UserDB>,

View File

@@ -20,11 +20,15 @@ pub struct Pagination {
pub per_page: Option<usize>,
}
#[derive(Deserialize)]
pub struct StripPath(String);
pub struct StripPath(pub String);
impl StripPath {
pub fn to_path(&self) -> &str {
self.0.strip_prefix('/').unwrap()
if self.0.starts_with('/') {
self.0.strip_prefix('/').unwrap()
} else {
&self.0
}
}
}

View File

@@ -345,6 +345,7 @@ async fn handle_job(
}
} else {
let (inner_content, requirements_o, language) = if matches!(job.job_kind, JobKind::Preview)
|| matches!(job.job_kind, JobKind::Script_Hub)
{
let code = (job.raw_code.as_ref().unwrap_or(&"no raw code".to_owned())).to_owned();
let reqs = if job
@@ -549,7 +550,7 @@ print(res_json)
let wrapper_content: String = format!(
r#"
import {{ main }} from "./inner.ts";
const {{{spread}}}= JSON.parse(`{ser_args}`);
const {{{spread}}} = JSON.parse(`{ser_args}`);
async function run() {{
let res: any = await main({spread});

View File

@@ -13,7 +13,7 @@ export {
*/
export function createConf(): Configuration & { workspace_id: string } {
const token = Deno.env.get("WM_TOKEN") ?? 'no_token'
const base_url = Deno.env.get("BASE_URL") ?? 'http://localhost:8000'
const base_url = Deno.env.get("BASE_INTERNAL_URL") ?? 'http://localhost:8000'
return {
...createConfiguration({
baseServer: new ServerConfiguration(`${base_url}/api`, {}),

View File

@@ -3,15 +3,15 @@
import { page } from '$app/stores'
import { SvelteToast } from '@zerodevx/svelte-toast'
import { onMount } from 'svelte'
import { UserService, WorkspaceService } from '../gen'
import { WorkspaceService } from '../gen'
import { superadmin, userStore, usersWorkspaceStore, workspaceStore } from '../stores'
import {
clearStores,
superadmin,
usernameStore,
usersWorkspaceStore,
workspaceStore
} from '../stores'
import { getUser, logout, logoutWithRedirect, refreshSuperadmin, sendUserToast } from '../utils'
getUserExt,
logout,
logoutWithRedirect,
refreshSuperadmin,
sendUserToast
} from '../utils'
// Default toast options
const toastOptions = {
@@ -31,15 +31,20 @@
'Connection got disposed.'
]
async function loadData() {
async function loadUser() {
try {
$usersWorkspaceStore = await WorkspaceService.listUserWorkspaces()
await refreshSuperadmin()
if ($workspaceStore && $usernameStore) {
await getUser($workspaceStore)
} else if ($superadmin) {
console.log('You are a superadmin, you can go wherever you please')
if ($workspaceStore) {
if ($userStore) {
console.log(`Welcome ${$userStore.email}`)
} else if ($superadmin) {
console.log('You are a superadmin, you can go wherever you please')
} else {
$userStore = await getUserExt($workspaceStore)
throw Error('Not logged in')
}
} else {
goto('/user/workspaces')
}
@@ -49,7 +54,7 @@
}
onMount(() => {
loadData()
loadUser()
window.onunhandledrejection = (event: PromiseRejectionEvent) => {
event.preventDefault()

View File

@@ -21,14 +21,8 @@
import { onMount } from 'svelte'
import Icon from 'svelte-awesome'
import '../app.css'
import { OpenAPI } from '../gen'
import {
superadmin,
usernameStore,
userStore,
usersWorkspaceStore,
workspaceStore
} from '../stores'
import { OpenAPI, ScriptService } from '../gen'
import { hubScripts, superadmin, userStore, usersWorkspaceStore, workspaceStore } from '../stores'
import { clickOutside, logout } from '../utils'
OpenAPI.WITH_CREDENTIALS = true
@@ -56,10 +50,20 @@
workspacePickerOpen = false
}
async function loadSearchData() {
const scripts = await ScriptService.listHubScripts()
$hubScripts = scripts.map((x) => ({
path: `hub/${x.id}/${x.summary.toLowerCase().replaceAll(/\s+/g, '_')}`,
summary: `${x.summary} (${x.app})`,
approved: x.approved
}))
}
onMount(() => {
isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent)
//Mobile
isCollapsed = isMobile
loadSearchData()
})
</script>
@@ -179,7 +183,7 @@
<div class="mx-auto">
<span class:hidden={isCollapsed} class="px-2 font-mono text-xs whitespace-nowrap">
<Icon class="text-white" data={faUser} scale={0.6} />
{$usernameStore ?? $superadmin ?? '___'}
{$userStore?.username ?? $superadmin ?? '___'}
{#if $userStore?.is_admin}
<Icon class="text-white" data={faCrown} scale={0.6} />
{/if}

View File

@@ -5,7 +5,7 @@
import { displayDate, sendUserToast } from '../utils'
import { goto } from '$app/navigation'
import PageHeader from './components/PageHeader.svelte'
import { usernameStore, userStore, workspaceStore } from '../stores'
import { userStore, workspaceStore } from '../stores'
import TableCustom from './components/TableCustom.svelte'
import CenteredPage from './components/CenteredPage.svelte'
import Icon from 'svelte-awesome'
@@ -81,9 +81,7 @@
loadUsers()
loadLogs(username, pageIndex)
}
if ($usernameStore) {
username = $usernameStore
}
username = $userStore?.username
}
</script>

View File

@@ -1,12 +1,12 @@
<script lang="ts">
import { workspaceStore } from '../../stores'
import { userStore, workspaceStore } from '../../stores'
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 { canWrite } from '../../utils'
let name = ''
let modal: Modal
@@ -22,7 +22,7 @@
}
$: {
if (group && $workspaceStore) {
if (group && $workspaceStore && userStore) {
members = (group.members ?? []).map((x) => {
return {
name: x,
@@ -49,8 +49,7 @@
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)
can_write = canWrite(group.name!, group.extra_perms ?? {}, $userStore)
}
</script>

View File

@@ -40,7 +40,7 @@
<div class="w-12/12 pb-4">
<input placeholder="Search {itemName}" bind:value={itemsFilter} class="search-item" />
</div>
<ul role="list" class="divide-y divide-gray-200">
<ul class="divide-y divide-gray-200">
{#each filteredItems as obj}
<li
class="py-4 px-1 gap-1 flex flex-col hover:bg-white hover:border text-black cursor-pointer"

View File

@@ -8,6 +8,7 @@
import ScriptPicker from './ScriptPicker.svelte'
import { emptySchema } from '../../utils'
import FlowPreview from './FlowPreview.svelte'
import { inferArgs } from '../../infer'
export let flow: Flow
export let i: number
@@ -19,16 +20,24 @@
export async function loadSchema() {
if (mod.value.path) {
const script = await ScriptService.getScriptByPath({
workspace: $workspaceStore!,
path: mod.value.path ?? ''
})
let schema
if (mod.value.path.startsWith('hub/')) {
const code = await ScriptService.getHubScriptContentByPath({ path: mod.value.path })
schema = emptySchema()
await inferArgs('deno', code, schema)
} else {
const script = await ScriptService.getScriptByPath({
workspace: $workspaceStore!,
path: mod.value.path ?? ''
})
schema = script.schema
}
if (
JSON.stringify(Object.keys(script.schema?.properties ?? {}).sort()) !=
JSON.stringify(Object.keys(schema?.properties ?? {}).sort()) !=
JSON.stringify(Object.keys(mod.input_transform).sort())
) {
let it = {}
Object.keys(script.schema?.properties ?? {}).map(
Object.keys(schema?.properties ?? {}).map(
(x) =>
(it[x] = {
type: 'static',
@@ -37,7 +46,7 @@
)
schemaForms[i]?.setArgs(it)
}
schemas[i] = script.schema ?? emptySchema()
schemas[i] = schema ?? emptySchema()
} else {
schemaForms[i]?.setArgs({})
schemas[i] = emptySchema()
@@ -66,7 +75,7 @@
</div>
<div class="p-10">
<h2 class="mb-4">Step script</h2>
<ScriptPicker bind:scriptPath={mod.value.path} on:select={loadSchema} />
<ScriptPicker allowHub={true} bind:scriptPath={mod.value.path} on:select={loadSchema} />
<div class="my-4" />
<h2 class="mb-4">Step inputs</h2>
<SchemaForm

View File

@@ -9,7 +9,7 @@
const dispatch = createEventDispatcher()
</script>
<fieldset class="mt-2 mr-4">
<fieldset>
<legend class="sr-only">{label}</legend>
<div class="flex flex-row gap-2">
{#each options as [label, val]}

View File

@@ -1,10 +1,10 @@
<script lang="ts">
import { sendUserToast } from '../../utils'
import { ScriptService, FlowService } from '../../gen'
import { ScriptService, FlowService, Script } from '../../gen'
import Icon from 'svelte-awesome'
import { faSearch } from '@fortawesome/free-solid-svg-icons'
import { workspaceStore } from '../../stores'
import { hubScripts, workspaceStore } from '../../stores'
import { createEventDispatcher } from 'svelte'
import ItemPicker from './ItemPicker.svelte'
import RadioButton from './RadioButton.svelte'
@@ -16,7 +16,8 @@
export let scriptPath: string | undefined = undefined
export let allowFlow = false
export let isFlow = false
export let allowHub = false
export let itemKind: 'hub' | 'script' | 'flow' = allowHub ? 'hub' : 'script'
let items: { summary: String; path: String; version?: String }[] = []
let itemPicker: ItemPicker
@@ -24,23 +25,33 @@
let code: string = ''
let lang: 'deno' | 'python3' | undefined
let options: [[string, any]] = [['Script', 'script']]
allowHub && options.unshift(['Hub', 'hub'])
allowFlow && options.push(['Flow', 'flow'])
const dispatch = createEventDispatcher()
async function getScript() {
const script = await ScriptService.getScriptByPath({
workspace: $workspaceStore!,
path: scriptPath!
})
code = script.content
lang = script.language
if (itemKind == 'hub') {
code = await ScriptService.getHubScriptContentByPath({ path: scriptPath! })
lang = Script.language.DENO
} else {
const script = await ScriptService.getScriptByPath({
workspace: $workspaceStore!,
path: scriptPath!
})
code = script.content
lang = script.language
}
}
async function loadItems(isFlow: boolean): Promise<void> {
async function loadItems(): Promise<void> {
try {
if (isFlow) {
if (itemKind == 'flow') {
items = await FlowService.listFlows({ workspace: $workspaceStore! })
} else {
} else if (itemKind == 'script') {
items = await ScriptService.listScripts({ workspace: $workspaceStore! })
} else {
items = $hubScripts ?? []
}
} catch (err) {
sendUserToast(`Could not load items: ${err}`, true)
@@ -49,7 +60,7 @@
$: {
if ($workspaceStore) {
loadItems(isFlow)
loadItems()
}
}
</script>
@@ -62,38 +73,23 @@
bind:this={itemPicker}
pickCallback={(path, _) => {
scriptPath = path
dispatch('select', { path: scriptPath })
}}
itemName={isFlow ? 'Flow' : 'Script'}
itemName={itemKind == 'flow' ? 'Flow' : 'Script'}
extraField="summary"
loadItems={async () => {
return items
}}
/>
<div class="flex flex-row items-center">
{#if allowFlow}
<RadioButton
bind:value={isFlow}
options={[
['Script', false],
['Flow', true]
]}
/>
<div class="flex flex-row items-center space-x-5">
{#if options.length > 1}
<RadioButton bind:value={itemKind} {options} />
{/if}
<select
bind:value={scriptPath}
on:change={() => {
dispatch('select', { path: scriptPath })
}}
class="max-w-lg"
>
<option value={undefined} />
{#each items as s}
<option value={s.path}>{s.path} {s.summary ? ' | ' + s.summary : ''}</option>
{/each}
</select>
<button on:click={() => itemPicker.openModal()}
><Icon class="mx-4 text-gray-700 text-opacity-70" data={faSearch} /></button
<input type="text" value={scriptPath ?? 'No path chosen yet'} disabled />
<button class="default-button text-gray-100" on:click={() => itemPicker.openModal()}
>Pick a {itemKind} path<Icon class="mx-4" data={faSearch} /></button
>
{#if scriptPath != undefined && scriptPath != ''}
<button

View File

@@ -21,7 +21,7 @@
import Tooltip from './components/Tooltip.svelte'
import ShareModal from './components/ShareModal.svelte'
import SharedBadge from './components/SharedBadge.svelte'
import { superadmin, usernameStore, userStore, workspaceStore } from '../stores'
import { superadmin, userStore, workspaceStore } from '../stores'
import CenteredPage from './components/CenteredPage.svelte'
import Tabs from './components/Tabs.svelte'
@@ -50,7 +50,7 @@
let defaults: string[] = []
if (tab == 'all' || tab == 'personal') {
defaults = defaults.concat(`u/${$usernameStore}`)
defaults = defaults.concat(`u/${$userStore?.username}`)
}
if (tab == 'all' || tab == 'groups') {
defaults = defaults.concat($userStore?.groups.map((x) => `g/${x}`) ?? [])
@@ -65,7 +65,7 @@
function tabFromPath(path: string) {
let t: Tab = 'shared'
let path_prefix = path.split('/').slice(0, 2)
if (path_prefix[0] == 'u' && path_prefix[1] == $usernameStore) {
if (path_prefix[0] == 'u' && path_prefix[1] == $userStore?.username) {
t = 'personal'
} else if (path_prefix[0] == 'g' && $userStore?.groups.includes(path_prefix[1])) {
t = 'groups'
@@ -118,7 +118,7 @@
<Tabs
tabs={[
['all', 'all'],
['personal', `personal space (${$usernameStore})`],
['personal', `personal space (${$userStore?.username})`],
['groups', 'groups'],
['shared', 'shared']
]}
@@ -131,7 +131,7 @@
<div class="shadow p-4 my-2">
{#if sectionTab == 'personal'}
<h2 class="">
My personal space ({`u/${$usernameStore}`})
My personal space ({`u/${$userStore?.username}`})
</h2>
<p class="italic text-xs text-gray-600 mb-4">
All flows owned by you (and visible only to you if you do not explicitely share them)

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { page } from '$app/stores'
import { FlowService, type Flow } from '../../../gen'
import { sendUserToast, displayDaysAgo, canWrite, getUser } from '../../../utils'
import { sendUserToast, displayDaysAgo, canWrite } from '../../../utils'
import Icon from 'svelte-awesome'
import {
faPlay,
@@ -17,7 +17,7 @@
import github from 'svelte-highlight/styles/github'
import Tooltip from '../../components/Tooltip.svelte'
import ShareModal from '../../components/ShareModal.svelte'
import { workspaceStore } from '../../../stores'
import { userStore, workspaceStore } from '../../../stores'
import SharedBadge from '../../components/SharedBadge.svelte'
import SvelteMarkdown from 'svelte-markdown'
import SchemaViewer from '../../components/SchemaViewer.svelte'
@@ -31,7 +31,7 @@
let shareModal: ShareModal
$: {
if ($workspaceStore) {
if ($workspaceStore && $userStore) {
loadFlow(path)
}
}
@@ -48,8 +48,7 @@
async function loadFlow(hash: string): Promise<void> {
flow = await FlowService.getFlowByPath({ workspace: $workspaceStore!, path })
const user = await getUser($workspaceStore!)
can_write = canWrite(flow.path, flow.extra_perms!, user)
can_write = canWrite(flow.path, flow.extra_perms!, $userStore)
}
</script>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { canWrite, getUser } from '../utils'
import { canWrite } from '../utils'
import { GroupService } from '../gen'
import type { Group } from '../gen'
@@ -9,7 +9,7 @@
import ShareModal from './components/ShareModal.svelte'
import SharedBadge from './components/SharedBadge.svelte'
import { faEdit, faPlus, faShare } from '@fortawesome/free-solid-svg-icons'
import { workspaceStore } from '../stores'
import { userStore, workspaceStore } from '../stores'
import CenteredPage from './components/CenteredPage.svelte'
import Icon from 'svelte-awesome'
import GroupModal from './components/GroupModal.svelte'
@@ -22,9 +22,8 @@
let groupModal: GroupModal
async function loadGroups(): Promise<void> {
const user = await getUser($workspaceStore!)
groups = (await GroupService.listGroups({ workspace: $workspaceStore! })).map((x) => {
return { canWrite: canWrite(x.name, x.extra_perms ?? {}, user), ...x }
return { canWrite: canWrite(x.name, x.extra_perms ?? {}, $userStore), ...x }
})
}
@@ -44,7 +43,7 @@
}
$: {
if ($workspaceStore) {
if ($workspaceStore && $userStore) {
loadGroups()
}
}

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { canWrite, emptySchema, getUser, sendUserToast } from '../utils'
import { canWrite, emptySchema, sendUserToast } from '../utils'
import { ResourceService } from '../gen'
import type { Resource, ResourceType } from '../gen'
import PageHeader from './components/PageHeader.svelte'
@@ -40,16 +40,13 @@
let resourceEditor: ResourceEditor | undefined
let user: UserExt | undefined
$: user = $userStore
let shareModal: ShareModal
async function loadResources(): Promise<void> {
const user = await getUser($workspaceStore!)
resources = (await ResourceService.listResource({ workspace: $workspaceStore! })).map((x) => {
return {
canWrite: canWrite(x.path, x.extra_perms!, user) && $workspaceStore! == x.workspace_id,
canWrite:
canWrite(x.path, x.extra_perms!, $userStore) && $workspaceStore! == x.workspace_id,
...x
}
})
@@ -105,7 +102,7 @@
}
$: {
if ($workspaceStore) {
if ($workspaceStore && $userStore) {
loadResources()
loadResourceTypes()
}
@@ -245,7 +242,7 @@
on:click={() => {
handleDeleteResourceType(name)
}}
disabled={!(user?.is_admin ?? false)}
disabled={!($userStore?.is_admin ?? false)}
/>
{/if}
</td>

View File

@@ -21,6 +21,9 @@
let script_path = $page.url.searchParams.get('path') || ''
let is_flow = $page.url.searchParams.get('isFlow') == 'true'
let itemKind: 'flow' | 'script' = is_flow ? 'flow' : 'script'
$: is_flow = itemKind == 'flow'
let runnable: Script | Flow | undefined
let args: Record<string, any> = {}
@@ -163,7 +166,7 @@
<p class="text-xs text-gray-600">
Pick a script or flow to be triggered by the schedule<Required required={true} />
</p>
<ScriptPicker allowFlow={true} bind:isFlow={is_flow} bind:scriptPath={script_path} />
<ScriptPicker allowFlow={true} bind:itemKind bind:scriptPath={script_path} />
<div class="max-w-5xl {edit ? '' : 'mt-2 md:mt-6'}">
<h2>Arguments</h2>
{#if runnable}

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { sendUserToast, truncateHash, displayDate, canWrite, getUser } from '../utils'
import { sendUserToast, displayDate, canWrite } from '../utils'
import { type Schedule, ScheduleService } from '../gen'
import PageHeader from './components/PageHeader.svelte'
@@ -15,7 +15,7 @@
faToggleOff,
faToggleOn
} from '@fortawesome/free-solid-svg-icons'
import { workspaceStore } from '../stores'
import { userStore, workspaceStore } from '../stores'
import CenteredPage from './components/CenteredPage.svelte'
import Icon from 'svelte-awesome'
@@ -26,9 +26,8 @@
let shareModal: ShareModal
async function loadSchedules(): Promise<void> {
const user = await getUser($workspaceStore!)
schedules = (await ScheduleService.listSchedules({ workspace: $workspaceStore! })).map((x) => {
return { canWrite: canWrite(x.path, x.extra_perms!, user), ...x }
return { canWrite: canWrite(x.path, x.extra_perms!, $userStore), ...x }
})
}
@@ -46,7 +45,7 @@
}
$: {
if ($workspaceStore) {
if ($workspaceStore && $userStore) {
loadSchedules()
}
}

View File

@@ -13,7 +13,7 @@
import Icon from 'svelte-awesome'
import type { Script } from '../gen'
import { ScriptService } from '../gen'
import { superadmin, usernameStore, userStore, workspaceStore } from '../stores'
import { superadmin, userStore, workspaceStore } from '../stores'
import { canWrite, groupBy, sendUserToast, truncateHash } from '../utils'
import Badge from './components/Badge.svelte'
import CenteredPage from './components/CenteredPage.svelte'
@@ -62,7 +62,7 @@
let defaults: string[] = []
if (tab == 'all' || tab == 'personal') {
defaults = defaults.concat(`u/${$usernameStore}`)
defaults = defaults.concat(`u/${$userStore?.username}`)
}
if (tab == 'all' || tab == 'groups') {
defaults = defaults.concat($userStore?.groups.map((x) => `g/${x}`) ?? [])
@@ -86,7 +86,7 @@
function tabFromPath(path: string) {
let t: Tab = 'shared'
let path_prefix = path.split('/').slice(0, 2)
if (path_prefix[0] == 'u' && path_prefix[1] == $usernameStore) {
if (path_prefix[0] == 'u' && path_prefix[1] == $userStore?.username) {
t = 'personal'
} else if (path_prefix[0] == 'g' && $userStore?.groups.includes(path_prefix[1])) {
t = 'groups'
@@ -154,7 +154,7 @@
<Tabs
tabs={[
['all', 'all'],
['personal', `personal space (${$usernameStore})`],
['personal', `personal space (${$userStore?.username})`],
['groups', 'groups'],
['shared', 'shared'],
['community', 'community']
@@ -168,7 +168,7 @@
<div class="shadow p-4 my-2">
{#if sectionTab == 'personal'}
<h2 class="">
My personal space ({`u/${$usernameStore}`})
My personal space ({`u/${$userStore?.username}`})
</h2>
<p class="italic text-xs text-gray-600 mb-4">
All scripts owned by you (and visible only to you if you do not explicitely share them)

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { page } from '$app/stores'
import { ScriptService, type Script } from '../../../gen'
import { truncateHash, sendUserToast, displayDaysAgo, canWrite, getUser } from '../../../utils'
import { truncateHash, sendUserToast, displayDaysAgo, canWrite } from '../../../utils'
import Icon from 'svelte-awesome'
import {
faPlay,
@@ -83,9 +83,9 @@
script = await ScriptService.getScriptByPath({ workspace: $workspaceStore!, path: hash })
hash = script.hash
}
const user = await getUser($workspaceStore!)
can_write =
script.workspace_id == $workspaceStore && canWrite(script.path, script.extra_perms!, user)
script.workspace_id == $workspaceStore &&
canWrite(script.path, script.extra_perms!, $userStore)
if (script.path && script.archived) {
const script_by_path = await ScriptService.getScriptByPath({
workspace: $workspaceStore!,

View File

@@ -7,7 +7,7 @@
import { slide } from 'svelte/transition'
import { UserService, WorkspaceService } from '../../gen'
import { clearStores, userStore, usersWorkspaceStore, workspaceStore } from '../../stores'
import { getUser, refreshSuperadmin, sendUserToast } from '../../utils'
import { refreshSuperadmin, sendUserToast } from '../../utils'
import CenteredModal from './CenteredModal.svelte'
let email = $page.url.searchParams.get('email') ?? ''
@@ -28,25 +28,32 @@
// Once logged in, we can fetch the workspaces
$usersWorkspaceStore = await WorkspaceService.listUserWorkspaces()
// And the actual user
$userStore = await getUser($workspaceStore!)
// trigger a reload of the user
$workspaceStore = $workspaceStore
// Finally, we check whether the user is a superadmin
refreshSuperadmin()
redirectUser()
} catch (err) {
sendUserToast(`Cannot login: ${err.body}`, true)
}
}
function redirectUser() {
if ($workspaceStore) {
if (rd) {
goto(decodeURI(rd))
} else {
goto('/user/workspaces')
goto('/')
}
} catch (err) {
sendUserToast(`Cannot login: ${err.body}`, true)
} else {
goto('/user/workspaces')
}
}
onMount(async () => {
try {
await UserService.getCurrentEmail()
goto('/')
redirectUser()
} catch {
clearStores()
}

View File

@@ -85,6 +85,7 @@
"
on:click={() => {
workspaceStore.set(workspace.id)
goto('/')
}}
><span class="font-mono">{workspace.id}</span> - {workspace.name} as

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { canWrite, getUser, sendUserToast } from '../utils'
import { canWrite, sendUserToast } from '../utils'
import { VariableService } from '../gen'
import type { ListableVariable, ContextualVariable } from '../gen'
import Dropdown from './components/Dropdown.svelte'
@@ -9,7 +9,7 @@
import ShareModal from './components/ShareModal.svelte'
import SharedBadge from './components/SharedBadge.svelte'
import VariableEditor from './components/VariableEditor.svelte'
import { workspaceStore } from './../stores'
import { userStore, workspaceStore } from './../stores'
import CenteredPage from './components/CenteredPage.svelte'
import Icon from 'svelte-awesome'
import { faPlus } from '@fortawesome/free-solid-svg-icons'
@@ -24,10 +24,9 @@
// If relative, the dropdown is positioned relative to its button
async function loadVariables(): Promise<void> {
const user = await getUser($workspaceStore!)
variables = (await VariableService.listVariable({ workspace: $workspaceStore! })).map((x) => {
return {
canWrite: canWrite(x.path, x.extra_perms!, user) && x.workspace_id == $workspaceStore,
canWrite: canWrite(x.path, x.extra_perms!, $userStore) && x.workspace_id == $workspaceStore,
...x
}
})
@@ -45,7 +44,7 @@
}
$: {
if ($workspaceStore) {
if ($workspaceStore && $userStore) {
loadVariables()
loadContextualVariables()
}

View File

@@ -1,7 +1,7 @@
import { browser } from '$app/env'
import type { Readable } from 'svelte/store'
import { derived, writable } from 'svelte/store'
import { writable } from 'svelte/store'
import type { UserWorkspaceList } from './gen'
import { getUserExt } from './utils'
export interface UserExt {
email: string
@@ -11,27 +11,31 @@ export interface UserExt {
groups: string[]
pgroups: string[]
}
let persistedWorkspace = browser && localStorage.getItem('workspace')
export const userStore = writable<UserExt | undefined>(undefined)
export let workspaceStore = writable<string | undefined>(
export const workspaceStore = writable<string | undefined>(
persistedWorkspace ? String(persistedWorkspace) : undefined
)
export const usersWorkspaceStore = writable<UserWorkspaceList | undefined>(undefined)
export const usernameStore: Readable<string | undefined> = derived(
[usersWorkspaceStore, workspaceStore],
($values, set) => {
set($values[0]?.workspaces.find((x) => x.id == $values[1])?.username)
}
)
export const superadmin = writable<String | false | undefined>(undefined)
export const hubScripts = writable<Array<{
path: string
summary: string
approved: boolean
}> | undefined>(undefined)
if (browser) {
workspaceStore.subscribe((workspace) => {
workspaceStore.subscribe(async (workspace) => {
if (workspace) {
localStorage.setItem('workspace', String(workspace))
userStore.set(await getUserExt(workspace))
} else {
userStore.set(undefined)
}
})
}
export function clearStores(): void {

View File

@@ -69,53 +69,20 @@ export function truncateHash(hash: string): string {
}
}
async function loadStore(workspace: string): Promise<UserExt | undefined> {
export async function getUserExt(workspace: string): Promise<UserExt | undefined> {
try {
const user = await UserService.whoami({ workspace })
const nuser = mapUserToUserExt(user)
userStore.set(nuser)
return nuser
return mapUserToUserExt(user)
} catch (error) {
userStore.set(undefined)
return undefined
}
}
export async function getUser(workspace: string): Promise<UserExt | undefined> {
const user = get(userStore)
if (user === undefined) {
return loadStore(workspace)
} else {
return user
}
}
export function logoutWithRedirect(rd?: string): void {
const error = encodeURIComponent('You have been logged out because your session has expired.')
goto(`/user/login?error=${error}${rd ? '&rd=' + encodeURIComponent(rd) : ''}`)
}
export async function handle401<T>(
promise: CancelablePromise<T> | Promise<T>,
rd?: string
): Promise<T> {
// Redirects to login if the `promise` returns a 401 due to lack of authentication
// Optionnally provide `rd`, to which the user will be redirected after logging back in
return promise.catch(async (error) => {
if (error.status === 401) {
if (getUser(get(workspaceStore)!) === undefined) {
logoutWithRedirect(rd)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return null as any
} else {
throw Error('You do not have enough privilege to access this')
}
} else {
throw error
}
})
}
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms))
@@ -141,8 +108,8 @@ export async function refreshSuperadmin(): Promise<void> {
export async function logout(logoutMessage?: string): Promise<void> {
try {
clearStores()
goto(`/user/login${logoutMessage ? '?error=' + encodeURIComponent(logoutMessage) : ''}`)
await UserService.logout()
goto(`/user/login${logoutMessage ? '?error=' + encodeURIComponent(logoutMessage) : ''}`)
sendUserToast('you have been logged out')
} catch (error) {
goto(

View File

@@ -2,8 +2,8 @@
"compilerOptions": {
"moduleResolution": "node",
"module": "es2020",
"lib": ["es2020", "DOM"],
"target": "es2020",
"lib": ["es2021", "DOM"],
"target": "es2021",
/**
svelte-preprocess cannot figure out whether you have a value or a type, so tell TypeScript
to enforce using \`import type\` instead of \`import\` for Types.

View File

@@ -30,7 +30,7 @@ class JobStatus(Enum):
_client: AuthenticatedClient | None = None
def create_client(base_url: str | None = None, token: str | None = None) -> AuthenticatedClient:
env_base_url = os.environ.get("BASE_URL")
env_base_url = os.environ.get("BASE_INTERNAL_URL")
if env_base_url is not None:
env_base_url = env_base_url + "/api"