Files
windmill/frontend/src/lib/components/SuperadminSettings.svelte
centdix d24825c3ea feat: add navigator mode to AIChat and unify UI (#5859)
* feat: ai flow chat

* youpi

* feat: preprocessor and error handler support

* fix: reactivity

* Add GlobalChat component with drawer functionality

- Create GlobalChat.svelte with placeholder chat functionality
- Create GlobalChatDrawer.svelte as drawer wrapper
- Add global chat button to sidebar menu (both mobile and desktop)
- Integrate global chat state management in main layout
- Include message history, loading states, and error handling
- Implement responsive design and proper drawer behavior

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>

* draft

* use triggerable by ai compoennt

* make drawer triggerable

* implement logic

* add inkeep tool

* cleaner code

* make more things available

* more integrations + better system prompt

* fix docs fetching

* small fix

* cleaning

* add ask in search bar + right top icon on homepage + suggestions

* fix button

* disable chat if no ai providers

* add inkeep endpoint

* draft working stuff

* cleaner code

* better chat

* fix

* send license and uid

* better anim

* move logic

* parse links in chat

* add missing integration

* add reset button

* fix

* rm file

* integrate navigator mode

* integrate all changes

* add hide button

* adjust drawer size

* add script ai chat integration

* fix drawer

* small fixes

* small fixes

* draft

* merge script ai chat with global one

* cleaning

* fixes

* working draft

* add aichat service

* cleaning more

* remove left over from store

* more descriptive states

* better icon

* fix

* use pending prompt

* cleaning

* cleaning

* small fix

* add inkeep file

* clean

* add route

* Update backend/windmill-api/src/lib.rs

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>

* cleaning

* fix drawer

* save open state in local storage

* small fix

* fixes

* small fixes

* move chat request to manager

* renaming

* move flow effects in manager

* move chat effects in manager

* remove log

* Update frontend/src/lib/components/copilot/CronGen.svelte

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Update frontend/src/lib/components/copilot/chat/flow/core.ts

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* change askaibutton folder

* define button

* ifelse

* no any + no default size

* use tailwind

* use splitpanes

* move effects

* remove deprecated file

* wording

* add back disable ai

* add error message

* modify system prompt

* handle confirmation modal

* fix

* fix

* close script settings

* fix icon color

* fix

* fix history manager

* fix test panel

* save size

* remove floating button

* fix delete chat

* fix

* better fix

---------

Co-authored-by: HugoCasa <hugo@casademont.ch>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-06-04 19:17:25 +02:00

417 lines
13 KiB
Svelte

<script lang="ts">
import { UserService, type GlobalUserInfo, SettingService } from '$lib/gen'
import TableCustom from '$lib/components/TableCustom.svelte'
import InviteGlobalUser from '$lib/components/InviteGlobalUser.svelte'
import { Button, Drawer, DrawerContent, Tab, Tabs } from '$lib/components/common'
import { sendUserToast } from '$lib/toast'
import { base } from '$lib/base'
import SearchItems from './SearchItems.svelte'
import { page } from '$app/stores'
import { goto as gotoUrl } from '$app/navigation'
import Version from './Version.svelte'
import Uptodate from './Uptodate.svelte'
import TabContent from './common/tabs/TabContent.svelte'
import InstanceSettings from './InstanceSettings.svelte'
import { truncate } from '$lib/utils'
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
import { userStore } from '$lib/stores'
import { ExternalLink } from 'lucide-svelte'
import { settingsKeys } from './instanceSettings'
import ConfirmationModal from './common/confirmationModal/ConfirmationModal.svelte'
import ChangeInstanceUsername from './ChangeInstanceUsername.svelte'
import { isCloudHosted } from '$lib/cloud'
import InstanceNameEditor from './InstanceNameEditor.svelte'
import Toggle from './Toggle.svelte'
import { instanceSettingsSelectedTab } from '$lib/stores'
let drawer: Drawer
let filter = ''
export function openDrawer() {
listUsers(activeOnly)
drawer?.openDrawer?.()
}
export function closeDrawer() {
drawer?.closeDrawer()
removeHash()
}
function removeHash() {
const index = $page.url.href.lastIndexOf('#')
if (index === -1) return
const hashRemoved = $page.url.href.slice(0, index)
gotoUrl(hashRemoved)
}
let users: GlobalUserInfo[] = []
let filteredUsers: GlobalUserInfo[] = []
let deleteConfirmedCallback: (() => void) | undefined = undefined
let activeOnly = false
async function listUsers(activeOnly: boolean): Promise<void> {
users = await UserService.listUsersAsSuperAdmin({ perPage: 100000, activeOnly: activeOnly })
}
$: listUsers(activeOnly)
let tab: 'users' | string = 'users'
$: $instanceSettingsSelectedTab, (tab = $instanceSettingsSelectedTab)
$: tab, instanceSettingsSelectedTab.set(tab)
let nbDisplayed = 50
let instanceSettings
let automateUsernameCreation = false
async function getAutomateUsernameCreationSetting() {
automateUsernameCreation =
((await SettingService.getGlobal({ key: 'automate_username_creation' })) as any) ?? false
}
getAutomateUsernameCreationSetting()
let automateUsernameModalOpen = false
async function enableAutomateUsernameCreationSetting() {
await SettingService.setGlobal({
key: 'automate_username_creation',
requestBody: { value: true }
})
getAutomateUsernameCreationSetting()
sendUserToast('Automatic username creation enabled')
listUsers(activeOnly)
}
async function updateName(name: string | undefined, email: string) {
try {
await UserService.globalUserUpdate({
email,
requestBody: {
name
}
})
sendUserToast('User updated')
listUsers(activeOnly)
} catch (e) {
sendUserToast('Error updating user', true)
}
}
</script>
<SearchItems
{filter}
items={users}
bind:filteredItems={filteredUsers}
f={(x) => x.email + ' ' + x.name + ' ' + x.company}
/>
<Drawer
bind:this={drawer}
on:open={() => listUsers(activeOnly)}
size="1200px"
on:close={removeHash}
>
<DrawerContent overflow_y={true} title="Instance settings" on:close={closeDrawer}>
<div class="flex flex-col h-full w-full">
<div>
<div class="flex justify-between">
<div class="text-xs pt-1 text-tertiary flex flex-col">
<div>Windmill <Version /></div>
</div>
<div><Uptodate /></div></div
>
</div>
<div class="flex flex-row-reverse">
<Button
variant="border"
color="dark"
target="_blank"
href="{base}/?workspace=admins"
endIcon={{ icon: ExternalLink }}
>
Admins workspace
</Button>
</div>
<div class="pt-4 h-full">
<Tabs bind:selected={tab}>
<Tab value="users" aiId="instance-settings-users" aiDescription="Instance users settings"
>Users</Tab
>
{#each settingsKeys as category}
<Tab
value={category}
aiId={`instance-settings-${category}`}
aiDescription={`Instance ${category} settings`}
>
{category}
</Tab>
{/each}
<svelte:fragment slot="content">
<div class="pt-4"></div>
<TabContent value="users">
<div class="h-full">
{#if !automateUsernameCreation && !isCloudHosted()}
<div class="mb-4">
<h3 class="mb-2"> Automatic username creation </h3>
<div class="mb-2">
<span class="text-primary text-sm"
>Automatically create a username for new users based on their email, shared
across workspaces. <a
target="_blank"
href="https://www.windmill.dev/docs/advanced/instance_settings#automatic-username-creation"
>Learn more</a
></span
>
</div>
<Button
btnClasses="w-auto"
size="sm"
color="dark"
on:click={() => {
automateUsernameModalOpen = true
}}
>
Enable (recommended)
</Button>
<ConfirmationModal
open={automateUsernameModalOpen}
on:confirmed={() => {
automateUsernameModalOpen = false
enableAutomateUsernameCreationSetting()
}}
on:canceled={() => (automateUsernameModalOpen = false)}
title="Automatic username creation"
confirmationText="Enable"
>
Once activated, it will not be possible to disable this feature. In case
existing users have different usernames in different workspaces, you will have
to manually confirm the username for each user.
</ConfirmationModal>
</div>
{/if}
<div class="py-2 mb-4">
<InviteGlobalUser on:new={() => listUsers(activeOnly)} />
</div>
<div class="flex flex-row justify-between">
<h3>All instance users</h3>
<Toggle
bind:checked={activeOnly}
options={{
left: 'Show active users only',
leftTooltip:
'An active user is a user who has performed at least one action in the last 30 days'
}}
/>
</div>
<div class="pb-1"></div>
<div>
<input placeholder="Search users" bind:value={filter} class="input mt-1" />
</div>
<div class="mt-2 overflow-auto">
<TableCustom>
<tr slot="header-row" class="sticky top-0 bg-surface border-b">
<th>email</th>
<th>auth</th>
<th>name</th>
{#if automateUsernameCreation}
<th>username</th>
{/if}
{#if activeOnly}
<th>kind</th>
{/if}
<th></th>
<th></th>
</tr>
<tbody slot="body" class="overflow-y-auto w-full h-full max-h-full">
{#if filteredUsers && users}
{#each filteredUsers.slice(0, nbDisplayed) as { email, super_admin, devops, login_type, name, username, operator_only } (email)}
<tr class="border">
<td>{email}</td>
<td>{login_type}</td>
<td><span class="break-words">{truncate(name ?? '', 30)}</span></td>
{#if automateUsernameCreation}
<td>
{#if username}
{username}
{:else}
{#key filteredUsers.map((u) => u.username).join()}
<ChangeInstanceUsername
username=""
{email}
isConflict
on:renamed={() => {
listUsers(activeOnly)
}}
/>
{/key}
{/if}
</td>
{/if}
{#if activeOnly}
<td>
{#if operator_only}
Operator only
{:else}
Developer
{/if}
</td>
{/if}
<td>
<ToggleButtonGroup
selected={super_admin ? 'super_admin' : devops ? 'devops' : 'user'}
on:selected={async (e) => {
if (email == $userStore?.email) {
sendUserToast('You cannot demote yourself', true)
listUsers(activeOnly)
return
}
let role = e.detail
if (role === 'super_admin') {
await UserService.globalUserUpdate({
email,
requestBody: {
is_super_admin: true,
is_devops: false
}
})
}
if (role === 'devops') {
await UserService.globalUserUpdate({
email,
requestBody: {
is_super_admin: false,
is_devops: true
}
})
}
if (role === 'user') {
await UserService.globalUserUpdate({
email,
requestBody: {
is_super_admin: false,
is_devops: false
}
})
}
sendUserToast('User updated')
listUsers(activeOnly)
}}
let:item
>
<ToggleButton value={'user'} size="xs" label="User" {item} />
<ToggleButton
value={'devops'}
size="xs"
label="Devops"
tooltip="Devops is a role that grants visibilty similar to that of a super admin, but without giving all rights. For example devops users can see service logs and crtical alerts. You can think of it as a 'readonly' super admin"
{item}
/>
<ToggleButton
value={'super_admin'}
size="xs"
label="Superadmin"
{item}
/>
</ToggleButtonGroup>
</td>
<td>
<div class="flex flex-row gap-x-1 justify-end">
<InstanceNameEditor
{login_type}
value={name}
{username}
{email}
on:refresh={() => {
listUsers(activeOnly)
}}
on:save={(e) => {
updateName(e.detail, email)
}}
on:renamed={() => {
listUsers(activeOnly)
}}
{automateUsernameCreation}
/>
<Button
color="light"
variant="contained"
size="xs"
spacingSize="xs2"
btnClasses="text-red-500"
on:click={() => {
deleteConfirmedCallback = async () => {
await UserService.globalUserDelete({ email })
sendUserToast(`User ${email} removed`)
listUsers(activeOnly)
}
}}
>
Remove
</Button>
</div>
</td>
</tr>
{/each}
{/if}
</tbody>
</TableCustom>
</div>
{#if filteredUsers && filteredUsers?.length > 50 && nbDisplayed < filteredUsers.length}
<span class="text-xs"
>{nbDisplayed} users out of {filteredUsers.length}
<button class="ml-4" on:click={() => (nbDisplayed += 50)}>load 50 more</button
></span
>
{/if}
</div>
</TabContent>
<TabContent value="" values={settingsKeys}>
<InstanceSettings
bind:this={instanceSettings}
hideTabs
hideSave
{tab}
{closeDrawer}
/>
</TabContent>
</svelte:fragment>
</Tabs>
</div>
</div>
{#if tab != 'users'}
<div class="absolute bottom-2 w-[95%]">
<Button
color="dark"
on:click={() => {
instanceSettings?.saveSettings()
}}
>
Save
</Button>
</div>{/if}
</DrawerContent>
</Drawer>
<ConfirmationModal
open={Boolean(deleteConfirmedCallback)}
title="Remove user"
confirmationText="Remove"
on:canceled={() => {
deleteConfirmedCallback = undefined
}}
on:confirmed={() => {
if (deleteConfirmedCallback) {
deleteConfirmedCallback()
}
deleteConfirmedCallback = undefined
}}
>
<div class="flex flex-col w-full space-y-4">
<span>Are you sure you want to remove ?</span>
</div>
</ConfirmationModal>