mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-24 00:00:46 +00:00
feat: Instance group management page (#2994)
This commit is contained in:
committed by
GitHub
parent
c6cfee5d89
commit
165d5cbfc8
@@ -1,6 +1,6 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
version: 1.240.0
|
||||
version: 1.241.0
|
||||
title: Windmill API
|
||||
contact:
|
||||
name: Windmill Team
|
||||
@@ -8695,7 +8695,7 @@ paths:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
/groups/udpate/{name}:
|
||||
/groups/update/{name}:
|
||||
post:
|
||||
summary: update instance group
|
||||
operationId: updateInstanceGroup
|
||||
@@ -8744,7 +8744,7 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
/groups/adduser/{name}:
|
||||
delete:
|
||||
post:
|
||||
summary: add user to instance group
|
||||
operationId: addUserToInstanceGroup
|
||||
tags:
|
||||
@@ -8774,7 +8774,7 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
/groups/removeuser/{name}:
|
||||
delete:
|
||||
post:
|
||||
summary: remove user from instance group
|
||||
operationId: removeUserFromInstanceGroup
|
||||
tags:
|
||||
|
||||
@@ -5656,7 +5656,7 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/groups/udpate/{name}:
|
||||
/groups/update/{name}:
|
||||
post:
|
||||
summary: update instance group
|
||||
operationId: updateInstanceGroup
|
||||
@@ -5701,7 +5701,7 @@ paths:
|
||||
type: string
|
||||
|
||||
/groups/adduser/{name}:
|
||||
delete:
|
||||
post:
|
||||
summary: add user to instance group
|
||||
operationId: addUserToInstanceGroup
|
||||
tags:
|
||||
@@ -5729,7 +5729,7 @@ paths:
|
||||
type: string
|
||||
|
||||
/groups/removeuser/{name}:
|
||||
delete:
|
||||
post:
|
||||
summary: remove user from instance group
|
||||
operationId: removeUserFromInstanceGroup
|
||||
tags:
|
||||
|
||||
@@ -316,7 +316,7 @@ async fn update_igroup(
|
||||
sqlx::query("UPDATE instance_group SET summary = $1 WHERE name = $2")
|
||||
.bind(igroup_update.new_summary)
|
||||
.bind(&name)
|
||||
.fetch_one(&mut *tx)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
audit_log(
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
<script lang="ts">
|
||||
import { GroupService, UserService, type InstanceGroup } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import AutoComplete from 'simple-svelte-autocomplete'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import autosize from 'svelte-autosize'
|
||||
import { Button } from './common'
|
||||
import Skeleton from './common/skeleton/Skeleton.svelte'
|
||||
import TableCustom from './TableCustom.svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
|
||||
export let name: string
|
||||
|
||||
let instance_group: InstanceGroup | undefined
|
||||
let members: { member_email: string }[] | undefined = undefined
|
||||
let usernames: string[] | undefined = []
|
||||
let username: string = ''
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
async function loadUsernames(): Promise<void> {
|
||||
usernames = await UserService.listUsernames({ workspace: $workspaceStore! })
|
||||
}
|
||||
|
||||
$: {
|
||||
load()
|
||||
}
|
||||
|
||||
async function load() {
|
||||
return Promise.all([loadInstanceGroup(), loadUsernames()])
|
||||
}
|
||||
|
||||
async function loadInstanceGroup(): Promise<void> {
|
||||
instance_group = await GroupService.getInstanceGroup({ name })
|
||||
members = instance_group?.emails
|
||||
? instance_group.emails.map((x) => {
|
||||
return { member_email: x }
|
||||
})
|
||||
: []
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<h1>{name}</h1>
|
||||
{#if instance_group !== undefined}
|
||||
<div class="flex flex-col gap-1">
|
||||
<textarea
|
||||
rows="2"
|
||||
type="text"
|
||||
use:autosize
|
||||
bind:value={instance_group.summary}
|
||||
placeholder="Summary of the group"
|
||||
/>
|
||||
<div class="flex justify-end">
|
||||
<Button
|
||||
size="xs"
|
||||
on:click={async () => {
|
||||
await GroupService.updateInstanceGroup({
|
||||
name,
|
||||
requestBody: { new_summary: instance_group?.summary ?? '' }
|
||||
})
|
||||
dispatch('update')
|
||||
sendUserToast('New summary saved')
|
||||
}}>Save Summary</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Members ({members?.length ?? 0})</h2>
|
||||
<div class="flex items-start">
|
||||
<AutoComplete noInputStyles items={usernames} bind:selectedItem={username} />
|
||||
<Button
|
||||
variant="contained"
|
||||
color="blue"
|
||||
size="sm"
|
||||
btnClasses="!ml-4"
|
||||
on:click={async () => {
|
||||
await GroupService.addUserToInstanceGroup({
|
||||
name,
|
||||
requestBody: { email: username }
|
||||
})
|
||||
dispatch('update')
|
||||
sendUserToast('User added')
|
||||
loadInstanceGroup()
|
||||
}}
|
||||
>
|
||||
Add member
|
||||
</Button>
|
||||
</div>
|
||||
{#if members}
|
||||
<TableCustom>
|
||||
<tr slot="header-row">
|
||||
<th>user</th>
|
||||
<th />
|
||||
</tr>
|
||||
<tbody slot="body">
|
||||
{#each members as { member_email }}<tr>
|
||||
<td>{member_email}</td>
|
||||
<td>
|
||||
<button
|
||||
class="ml-2 text-red-500"
|
||||
on:click={async () => {
|
||||
await GroupService.removeUserFromInstanceGroup({
|
||||
name,
|
||||
requestBody: { email: member_email }
|
||||
})
|
||||
dispatch('update')
|
||||
sendUserToast('User removed')
|
||||
loadInstanceGroup()
|
||||
}}>remove</button
|
||||
>
|
||||
</td>
|
||||
</tr>{/each}
|
||||
</tbody>
|
||||
</TableCustom>
|
||||
{:else}
|
||||
<div class="flex flex-col">
|
||||
{#each new Array(6) as _}
|
||||
<Skeleton layout={[[2], 0.7]} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,5 @@
|
||||
export function load() {
|
||||
return {
|
||||
stuff: { title: 'Instance groups' }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<script lang="ts">
|
||||
import type { InstanceGroup } from '$lib/gen'
|
||||
import { GroupService } from '$lib/gen'
|
||||
|
||||
import CenteredPage from '$lib/components/CenteredPage.svelte'
|
||||
import { Button, Drawer, DrawerContent, Popup } from '$lib/components/common'
|
||||
import InstanceGroupEditor from '$lib/components/InstanceGroupEditor.svelte'
|
||||
import PageHeader from '$lib/components/PageHeader.svelte'
|
||||
import TableCustom from '$lib/components/TableCustom.svelte'
|
||||
import { Plus } from 'lucide-svelte'
|
||||
|
||||
let newGroupName: string = ''
|
||||
let instanceGroups: InstanceGroup[] | undefined = undefined
|
||||
let groupDrawer: Drawer
|
||||
|
||||
async function loadInstanceGroups(): Promise<void> {
|
||||
try {
|
||||
instanceGroups = await GroupService.listInstanceGroups()
|
||||
} catch (e) {
|
||||
instanceGroups = undefined
|
||||
}
|
||||
}
|
||||
|
||||
async function addInstanceGroup() {
|
||||
await GroupService.createInstanceGroup({
|
||||
requestBody: { name: newGroupName }
|
||||
})
|
||||
loadInstanceGroups()
|
||||
editGroupName = newGroupName
|
||||
groupDrawer.openDrawer()
|
||||
}
|
||||
|
||||
$: {
|
||||
loadInstanceGroups()
|
||||
}
|
||||
|
||||
let editGroupName: string = ''
|
||||
</script>
|
||||
|
||||
<Drawer bind:this={groupDrawer}>
|
||||
<DrawerContent title="Instance Group {editGroupName}" on:close={groupDrawer.closeDrawer}>
|
||||
<InstanceGroupEditor on:update={loadInstanceGroups} name={editGroupName} />
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<CenteredPage>
|
||||
<PageHeader title="Instance Groups">
|
||||
<div class="flex flex-row">
|
||||
<div>
|
||||
<Popup
|
||||
let:close
|
||||
floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }}
|
||||
containerClasses="border rounded-lg shadow-lg p-4 bg-surface"
|
||||
>
|
||||
<svelte:fragment slot="button">
|
||||
<Button size="md" startIcon={{ icon: Plus }} nonCaptureEvent>New group</Button>
|
||||
</svelte:fragment>
|
||||
<div class="flex-col flex gap-2">
|
||||
<input class="mr-2" placeholder="New instance group name" bind:value={newGroupName} />
|
||||
<Button
|
||||
size="md"
|
||||
startIcon={{ icon: Plus }}
|
||||
disabled={!newGroupName}
|
||||
on:click={() => {
|
||||
addInstanceGroup()
|
||||
close(null)
|
||||
}}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</div>
|
||||
</Popup>
|
||||
</div>
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
{#if instanceGroups && instanceGroups.length > 0}
|
||||
<div class="relative mb-20 pt-8">
|
||||
<TableCustom>
|
||||
<tr slot="header-row">
|
||||
<th>Name</th>
|
||||
<th>Summary</th>
|
||||
<th>Members</th>
|
||||
</tr>
|
||||
<tbody slot="body">
|
||||
{#each instanceGroups as { name, summary, emails }}
|
||||
<tr>
|
||||
<td>
|
||||
<a
|
||||
href="#{name}"
|
||||
on:click={() => {
|
||||
editGroupName = name
|
||||
groupDrawer.openDrawer()
|
||||
}}
|
||||
>{name}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{summary ? summary.slice(0, 50) + (summary.length > 50 ? '...' : '') : '-'}
|
||||
</td>
|
||||
<td>{emails?.length ?? 0} members</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</TableCustom>
|
||||
</div>
|
||||
{/if}
|
||||
</CenteredPage>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
Reference in New Issue
Block a user