mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
feat: Superadmins workspace (#1003)
* Add auto-invite superadmins workspace
* Revert "Add auto-invite superadmins workspace"
This reverts commit 768ae31857.
* Add Admin Workspace
* Show unable to delete info in UI
* Add Hub sync
* Update Migration
* Add Starter workspace
* Add workspace_key for admins workspace
* Remove schedule
* Update CI
This commit is contained in:
Generated
+11
-11
@@ -4094,7 +4094,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api"
|
||||
version = "1.54.0"
|
||||
version = "1.55.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -4145,7 +4145,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-client"
|
||||
version = "1.54.0"
|
||||
version = "1.55.0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"chrono",
|
||||
@@ -4160,7 +4160,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-audit"
|
||||
version = "1.54.0"
|
||||
version = "1.55.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"serde",
|
||||
@@ -4173,7 +4173,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.54.0"
|
||||
version = "1.55.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -4197,7 +4197,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.54.0"
|
||||
version = "1.55.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4205,7 +4205,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.54.0"
|
||||
version = "1.55.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools",
|
||||
@@ -4219,7 +4219,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.54.0"
|
||||
version = "1.55.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools",
|
||||
@@ -4231,7 +4231,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.54.0"
|
||||
version = "1.55.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools",
|
||||
@@ -4246,7 +4246,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.54.0"
|
||||
version = "1.55.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deno_core",
|
||||
@@ -4260,7 +4260,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-queue"
|
||||
version = "1.54.0"
|
||||
version = "1.55.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -4283,7 +4283,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker"
|
||||
version = "1.54.0"
|
||||
version = "1.55.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
@@ -0,0 +1,9 @@
|
||||
INSERT INTO workspace(id, name, owner) VALUES
|
||||
('admins', 'Admins', 'admin@windmill.dev');
|
||||
|
||||
INSERT INTO workspace_settings (workspace_id) VALUES
|
||||
('admins');
|
||||
|
||||
INSERT INTO workspace_key
|
||||
(workspace_id, kind, key)
|
||||
VALUES ('admins', 'cloud', md5(random()::text) || md5(random()::text))
|
||||
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
@@ -0,0 +1,20 @@
|
||||
INSERT INTO script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
|
||||
'admins',
|
||||
'system',
|
||||
'import wmill from "https://deno.land/x/wmill@v1.55.0/main.ts";
|
||||
|
||||
export async function main() {
|
||||
await run(
|
||||
"workspace", "add", "__automation", "starter", Deno.env.get("WM_BASE_URL") + "/", "--token", Deno.env.get("WM_TOKEN"));
|
||||
|
||||
await run("hub", "pull");
|
||||
}
|
||||
|
||||
async function run(...cmd: string[]) {
|
||||
console.log("Running \"" + cmd.join('' '') + "\"");
|
||||
await wmill.parse(cmd);
|
||||
}',
|
||||
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
|
||||
'Syncronize Hub Resource types with starter workspace',
|
||||
'Basic administrative script to sync latest resource types from hub. Recommended to run at least once. On a schedule by default.',
|
||||
'u/admin/hub_sync', -28028598712388162, 'deno', '');
|
||||
+87
-81
@@ -1980,87 +1980,6 @@
|
||||
},
|
||||
"query": "SELECT EXISTS(SELECT 1 FROM usr WHERE workspace_id = $1 AND username = $2)"
|
||||
},
|
||||
"71b953191b3175a32a22f803205a5fd3dedef1baa0993afad08109ab5bf62fe9": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "workspace_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "path",
|
||||
"ordinal": 1,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "value",
|
||||
"ordinal": 2,
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "resource_type",
|
||||
"ordinal": 4,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "extra_perms",
|
||||
"ordinal": 5,
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"name": "is_expired",
|
||||
"ordinal": 6,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "refresh_error",
|
||||
"ordinal": 7,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "is_linked",
|
||||
"ordinal": 8,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "is_oauth?",
|
||||
"ordinal": 9,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "account",
|
||||
"ordinal": 10,
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
true,
|
||||
null,
|
||||
false,
|
||||
true
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "SELECT resource.*, (now() > account.expires_at) as is_expired, account.refresh_error,\n variable.path IS NOT NULL as is_linked,\n variable.is_oauth as \"is_oauth?\",\n variable.account\n FROM resource\n LEFT JOIN variable ON variable.path = resource.path AND variable.workspace_id = resource.workspace_id\n LEFT JOIN account ON variable.account = account.id AND account.workspace_id = resource.workspace_id\n WHERE resource.path = $1 AND (resource.workspace_id = $2 OR resource.workspace_id = 'starter')"
|
||||
},
|
||||
"765c18d77412cbb4474f4074d583b9b44681f3b9f58754662ac07a3a3470a3c5": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -3373,6 +3292,93 @@
|
||||
},
|
||||
"query": "\n UPDATE capture\n SET payload = $3\n WHERE workspace_id = $1\n AND path = $2\n "
|
||||
},
|
||||
"b9e968d522558b8ca2bd24d345768ba864a766e10dd79864b9d601254bb2d612": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "workspace_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "path",
|
||||
"ordinal": 1,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "value",
|
||||
"ordinal": 2,
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "resource_type",
|
||||
"ordinal": 4,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "extra_perms",
|
||||
"ordinal": 5,
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"name": "is_expired",
|
||||
"ordinal": 6,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "is_refreshed",
|
||||
"ordinal": 7,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "refresh_error",
|
||||
"ordinal": 8,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "is_linked",
|
||||
"ordinal": 9,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "is_oauth?",
|
||||
"ordinal": 10,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "account",
|
||||
"ordinal": 11,
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
true,
|
||||
null,
|
||||
false,
|
||||
true
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "SELECT resource.*, (now() > account.expires_at) as is_expired, account.refresh_token != '' as is_refreshed,\n account.refresh_error,\n variable.path IS NOT NULL as is_linked,\n variable.is_oauth as \"is_oauth?\",\n variable.account\n FROM resource\n LEFT JOIN variable ON variable.path = resource.path AND variable.workspace_id = resource.workspace_id\n LEFT JOIN account ON variable.account = account.id AND account.workspace_id = resource.workspace_id\n WHERE resource.path = $1 AND (resource.workspace_id = $2 OR resource.workspace_id = 'starter')"
|
||||
},
|
||||
"bb56e61c7cfb09c0a28fb3226dfe91704c70d9fe15eda18e6889adfd7496f80b": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
|
||||
@@ -426,6 +426,15 @@ async fn delete_workspace(
|
||||
Path(w_id): Path<String>,
|
||||
Authed { is_admin, username, email, .. }: Authed,
|
||||
) -> Result<String> {
|
||||
let w_id = match w_id.as_str() {
|
||||
"starter" => Err(Error::BadRequest(
|
||||
"starter workspace cannot be deleted".to_string(),
|
||||
)),
|
||||
"admins" => Err(Error::BadRequest(
|
||||
"admins workspace cannot be deleted".to_string(),
|
||||
)),
|
||||
_ => Ok(w_id),
|
||||
}?;
|
||||
require_admin(is_admin, &username)?;
|
||||
let mut tx = db.begin().await?;
|
||||
sqlx::query!("UPDATE workspace SET deleted = true WHERE id = $1", &w_id)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { workspaceStore, usersWorkspaceStore } from '$lib/stores'
|
||||
import { workspaceStore, userWorkspaces } from '$lib/stores'
|
||||
import { classNames } from '$lib/utils'
|
||||
import Icon from 'svelte-awesome'
|
||||
import { Building } from 'lucide-svelte'
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
<div class="divide-y divide-gray-100" role="none">
|
||||
<table class="w-full">
|
||||
{#each $usersWorkspaceStore?.workspaces ?? [] as workspace}
|
||||
{#each $userWorkspaces as workspace}
|
||||
<tr
|
||||
class="text-xs cursor-pointer"
|
||||
on:click={() => {
|
||||
|
||||
+69
-42
@@ -1,55 +1,82 @@
|
||||
import { browser } from '$app/env'
|
||||
import { writable } from 'svelte/store'
|
||||
import type { UserWorkspaceList } from '$lib/gen/models/UserWorkspaceList.js'
|
||||
import { getUserExt } from './user'
|
||||
import type { TokenResponse } from './gen'
|
||||
import { browser } from "$app/env";
|
||||
import { derived, type Readable, writable } from "svelte/store";
|
||||
import type { UserWorkspaceList } from "$lib/gen/models/UserWorkspaceList.js";
|
||||
import { getUserExt } from "./user";
|
||||
import type { TokenResponse } from "./gen";
|
||||
|
||||
export interface UserExt {
|
||||
email: string
|
||||
username: string
|
||||
is_admin: boolean
|
||||
created_at: string
|
||||
groups: string[]
|
||||
pgroups: string[]
|
||||
email: string;
|
||||
username: string;
|
||||
is_admin: boolean;
|
||||
created_at: string;
|
||||
groups: string[];
|
||||
pgroups: string[];
|
||||
}
|
||||
|
||||
let persistedWorkspace = browser && localStorage.getItem('workspace')
|
||||
let persistedWorkspace = browser && localStorage.getItem("workspace");
|
||||
|
||||
export const oauthStore = writable<TokenResponse | undefined>(undefined)
|
||||
export const userStore = writable<UserExt | undefined>(undefined)
|
||||
export const oauthStore = writable<TokenResponse | undefined>(undefined);
|
||||
export const userStore = writable<UserExt | undefined>(undefined);
|
||||
export const workspaceStore = writable<string | undefined>(
|
||||
persistedWorkspace ? String(persistedWorkspace) : undefined
|
||||
)
|
||||
export const starStore = writable(1)
|
||||
export const usersWorkspaceStore = writable<UserWorkspaceList | undefined>(undefined)
|
||||
export const superadmin = writable<String | false | undefined>(undefined)
|
||||
persistedWorkspace ? String(persistedWorkspace) : undefined,
|
||||
);
|
||||
export const starStore = writable(1);
|
||||
export const usersWorkspaceStore = writable<UserWorkspaceList | undefined>(
|
||||
undefined,
|
||||
);
|
||||
export const superadmin = writable<String | false | undefined>(undefined);
|
||||
export const userWorkspaces: Readable<
|
||||
Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
username: string;
|
||||
}>
|
||||
> = derived(
|
||||
[usersWorkspaceStore, superadmin],
|
||||
([store, superadmin]) => {
|
||||
const originalWorkspaces = (store?.workspaces ?? []);
|
||||
if (superadmin) {
|
||||
return [...originalWorkspaces, {
|
||||
id: "admins",
|
||||
name: "Admins",
|
||||
username: "superadmin",
|
||||
}, {
|
||||
id: "starter",
|
||||
name: "Starter",
|
||||
username: "superadmin",
|
||||
}];
|
||||
} else {
|
||||
return originalWorkspaces;
|
||||
}
|
||||
},
|
||||
);
|
||||
export const hubScripts = writable<
|
||||
| Array<{
|
||||
path: string
|
||||
summary: string
|
||||
approved: boolean
|
||||
kind: string
|
||||
app: string
|
||||
ask_id: number
|
||||
}>
|
||||
| undefined
|
||||
>(undefined)
|
||||
| Array<{
|
||||
path: string;
|
||||
summary: string;
|
||||
approved: boolean;
|
||||
kind: string;
|
||||
app: string;
|
||||
ask_id: number;
|
||||
}>
|
||||
| undefined
|
||||
>(undefined);
|
||||
|
||||
if (browser) {
|
||||
workspaceStore.subscribe(async (workspace) => {
|
||||
if (workspace) {
|
||||
localStorage.setItem('workspace', String(workspace))
|
||||
userStore.set(await getUserExt(workspace))
|
||||
} else {
|
||||
userStore.set(undefined)
|
||||
}
|
||||
})
|
||||
workspaceStore.subscribe(async (workspace) => {
|
||||
if (workspace) {
|
||||
localStorage.setItem("workspace", String(workspace));
|
||||
userStore.set(await getUserExt(workspace));
|
||||
} else {
|
||||
userStore.set(undefined);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function clearStores(): void {
|
||||
localStorage.removeItem('workspace')
|
||||
userStore.set(undefined)
|
||||
workspaceStore.set(undefined)
|
||||
usersWorkspaceStore.set(undefined)
|
||||
superadmin.set(undefined)
|
||||
localStorage.removeItem("workspace");
|
||||
userStore.set(undefined);
|
||||
workspaceStore.set(undefined);
|
||||
usersWorkspaceStore.set(undefined);
|
||||
superadmin.set(undefined);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
faFastForward
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import DisplayResult from '$lib/components/DisplayResult.svelte'
|
||||
import { superadmin, userStore, usersWorkspaceStore, workspaceStore } from '$lib/stores'
|
||||
import { superadmin, userStore, userWorkspaces, workspaceStore } from '$lib/stores'
|
||||
import CenteredPage from '$lib/components/CenteredPage.svelte'
|
||||
import FlowStatusViewer from '$lib/components/FlowStatusViewer.svelte'
|
||||
import HighlightCode from '$lib/components/HighlightCode.svelte'
|
||||
@@ -105,7 +105,7 @@
|
||||
<h1 class="text-red-400 mt-6">Job {$page.params.run} not found in {workspace_id}</h1>
|
||||
<h2>Are you in the right workspace?</h2>
|
||||
<div class="flex flex-col gap-2">
|
||||
{#each $usersWorkspaceStore?.workspaces ?? [] as workspace}
|
||||
{#each $userWorkspaces as workspace}
|
||||
<div>
|
||||
<Button variant="border" href="/run/{$page.params.run}?workspace={workspace.id}">
|
||||
See in {workspace.name}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
import { sendUserToast } from '$lib/utils'
|
||||
import { logout, logoutWithRedirect } from '$lib/logout'
|
||||
import { UserService, type WorkspaceInvite, WorkspaceService } from '$lib/gen'
|
||||
import { superadmin, usersWorkspaceStore, workspaceStore } from '$lib/stores'
|
||||
import { superadmin, usersWorkspaceStore, userWorkspaces, workspaceStore } from '$lib/stores'
|
||||
import { faCrown, faUserCog } from '@fortawesome/free-solid-svg-icons'
|
||||
import Icon from 'svelte-awesome'
|
||||
import { Button } from '$lib/components/common'
|
||||
@@ -60,7 +60,7 @@
|
||||
if (list_all_as_super_admin) {
|
||||
loadWorkspacesAsAdmin()
|
||||
} else {
|
||||
workspaces = $usersWorkspaceStore?.workspaces ?? []
|
||||
workspaces = $userWorkspaces
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -277,8 +277,15 @@
|
||||
<p class="italic text-xs">
|
||||
The workspace will be archived for a short period of time and then permanently deleted
|
||||
</p>
|
||||
{#if $workspaceStore === 'admins' || $workspaceStore === 'starter'}
|
||||
<p class="italic text-xs">
|
||||
This workspace cannot be deleted as it has a special function. Consult the documentation for
|
||||
more information.
|
||||
</p>
|
||||
{/if}
|
||||
<Button
|
||||
color="red"
|
||||
disabled={$workspaceStore === 'admins' || $workspaceStore === 'starter'}
|
||||
size="sm"
|
||||
btnClasses="mt-2"
|
||||
on:click={async () => {
|
||||
|
||||
Reference in New Issue
Block a user