gestion des providers

This commit is contained in:
fabien committed 2026-08-31 21:23:35 +02:00
1 parent 357bf9b41c
commit 4d72952b2c
46 files changed
+995 -150

No files matched your search

@@ -1,7 +1,8 @@
<div class="container">
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur?
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur?
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur?
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur?
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur?
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur?
</div>
@@ -3,6 +3,7 @@
import { page } from '$app/state';
import {uiState} from "$lib/state/ui.svelte"
import { auth } from '$lib/state/auth.svelte'
import { space } from '$lib/state/space.svelte'
import Sidebar from "$lib/components/Sidebar.svelte"
import { goto } from '$app/navigation'
import { onMount } from "svelte";
@@ -39,7 +40,9 @@
onMount(async () => {
handleResize()
if (data.me) {
auth.user = data.me
auth.user = data.me.user
space.spaces = data.me.teams
space.id = data.me.teams[0].id
themeToLocalStorage(auth.user.theme)
applyTheme()
auth.status = 'authenticated'
@@ -64,7 +67,7 @@
</div>
<div id="main">
<div class="flex flex-col h-screen ">
<div class={["header", page.data.theme]}>
<div class={["header", "container" , page.data.theme]}>
<h1 class="flex">
<span class="flex-1">{page.data.title}</span>
{#if page.data.back}
@@ -2,7 +2,7 @@
import { SvelteMap } from 'svelte/reactivity';
import type { PageProps } from './$types';
import type { User, Address } from '$lib/types/api'
import type { User, Address, Item } from '$lib/types/api'
import { userApi } from '$lib/api/endpoints/user'
import { miscApi } from '$lib/api/endpoints/misc'
import FormError from '$lib/components/FormError.svelte';
@@ -13,15 +13,15 @@
let { data }: PageProps = $props();
let form : User = $state(data.me)
let form : User = $state(data.me.user)
let addresses : Address[] = $state([] as Address[])
let newAddress : Address = $state({} as Address)
let timezones = $state([] as string[])
let timezones = $state([] as Item[])
let errors = new SvelteMap<string,string>()
onMount(async ()=> {
addresses = await userApi.listAddresses(form.id)
timezones = await miscApi.listTimezones()
timezones = (await miscApi.listTimezones()).map((it)=>{return {id:it, text:it}})
})
$effect(()=>{
@@ -64,24 +64,26 @@
</script>
<div class="mb-4 formgroup">
<div class="mb-4 formgroup container">
<h2>Mon compte</h2>
<form onsubmit={submitUser} onchange={()=>{errors.clear()}}>
<label for="email" class="block my-1 flex-1">Email</label>
<input
id="email"
type="text"
bind:value={form.email}
class="textinput"
autofocus
/>
<FormError key="email" errors={errors} />
<div class="formelement">
<label for="email" class="my-1 flex-1">Email</label>
<input
id="email"
type="text"
bind:value={form.email}
class="textinput"
autofocus
/>
<FormError key="email" errors={errors} />
</div>
<div class="flex">
<div class="flex-1 mr-2">
<label for="firstname" class="block my-1 flex-1">Prénom</label>
<div class="grid grid-cols-1 gap-x-4 md:grid-cols-2">
<div class="formelement">
<label for="firstname" class=" my-1 flex-1">Prénom</label>
<input
id="firstname"
type="text"
@@ -91,8 +93,8 @@
<FormError key="firstname" errors={errors} />
</div>
<div class="flex-1 ml-2">
<label for="lastname" class="block my-1 flex-1">Nom</label>
<div class="formelement">
<label for="lastname" class="my-1 flex-1">Nom</label>
<input
id="lastname"
type="text"
@@ -103,9 +105,9 @@
</div>
</div>
<div class="flex">
<div class="grid grid-cols-1 gap-x-4 md:grid-cols-2">
<div class="flex-1 mr-2">
<div class="formelement">
<label for="lang" class="block my-1 flex-1">Langue</label>
<select bind:value={form.lang} class="textinput">
<option value="en">English</option>
@@ -114,14 +116,14 @@
<FormError key="lang" errors={errors} />
</div>
<div class="flex-1 ml-2">
<label for="lang" class="block my-1 flex-1">Timezone</label>
<div class="formelement">
<label for="timezone" class="block my-1 flex-1">Timezone</label>
<Select items={timezones} bind:value={form.timezone}/>
<FormError key="lang" errors={errors} />
<FormError key="timezone" errors={errors} />
</div>
</div>
<div class="flex-1 mr-2">
<div class="formelement">
<label for="theme" class="block my-1 flex-1">Thème</label>
<select bind:value={form.theme} class="textinput">
<option value="auto">Automatique</option>
@@ -137,14 +139,14 @@
</div>
<div class="mb-4 formgroup">
<div class="mb-4 formgroup container">
<h2>Canaux de communication</h2>
{#each addresses as address (address.id) }
<div class="m-2">
<div class="dark:bg-stone-700 bg-stone-300 rounded flex inline px-2 py-1">
<div class="dark:bg-stone-700 bg-stone-300 rounded inline px-2 py-1 text-sm">
<span class="inline-block w-20">{address.type} : </span>
<span class="font-bold">{address.address}</span>
<span class="">{address.address}</span>
<button class="ml-4 pointer" onclick={()=>{handleDeleteAddress(address.id)}}>
<i class="icon-cancel"></i>
</button>
@@ -1,4 +1,4 @@
<div class="grid grid-cols-[repeat(auto-fit,minmax(250px,1fr))] gap-4">
<div class="container grid grid-cols-[repeat(auto-fit,minmax(250px,1fr))] gap-4">
<a class="bt p-5 text-center"
title="Modèles"
@@ -1 +1,157 @@
<div>Modèles</div>
<script lang="ts">
import { SvelteMap } from 'svelte/reactivity';
import type { Provider} from '$lib/types/api'
import type { PageProps } from './$types';
import { providerApi } from "$lib/api/endpoints/provider"
import { onMount } from 'svelte';
import { validateUrl } from '$lib/utilities/validators'
import ProviderForm from './ProviderForm.svelte'
let { data }: PageProps = $props();
let providers = $state([] as Provider[])
let addingProvider = $state(false)
let editingProvider = $state(null as string|null)
let providerForm = $state({} as Provider)
let errors = new SvelteMap<string,string>()
onMount(async ()=>{
providers = await providerApi.getProviders()
})
const enterAddProviderHandler = () => {
if (editingProvider) return
errors.clear()
providerForm = {} as Provider
providerForm.models = ""
addingProvider= true
}
const cancelAddProviderHandler = () => {
addingProvider= false
}
const submitAddProvider = (p: Provider) => {
if (!p.name) {
errors.set("name","Le nom est obligatoire !")
}
if (!p.type) {
errors.set("type","Le type est obligatoire !")
}
if (p.baseUrl && !validateUrl(p.baseUrl)) {
errors.set("baseUrl","Mauvaise URL !")
}
if (errors.size === 0) {
providerApi.createProvider(p)
.then((p)=>{
providers.push(p)
addingProvider= false
providerForm = {} as Provider
})
.catch((err)=>{
console.log(err)
})
}
}
const cancelEditProviderHandler = () => {
editingProvider = null
providerForm = {} as Provider
}
const submitEditProvider = (p: Provider) => {
if (!p.name) {
errors.set("name","Le nom est obligatoire !")
}
if (!p.type) {
errors.set("type","Le type est obligatoire !")
}
if (p.baseUrl && !validateUrl(p.baseUrl)) {
errors.set("baseUrl","Mauvaise URL !")
}
if (errors.size === 0) {
providerApi.updateProvider(p)
.then((p)=>{
providers = providers.map((it)=>{return p.id===it.id ? p : it})
editingProvider = null
providerForm = {} as Provider
})
.catch((err)=>{
console.log(err)
})
}
}
const handleEditProvider = async (id: string) => {
errors.clear()
if (addingProvider) return
providerApi.getProvider(id)
.then((data)=>{
providers = providers.map((it)=>it.id===id ? data : it)
editingProvider = id
})
}
const handleDeleteProvider = (id: string) => {
providerApi.deleteProvider(id)
.then(() => {
providers = providers.filter((it)=>it.id!==id)
})
.catch((err) => {console.log(err)})
}
</script>
<div class="container">
{#if addingProvider}
{#key "new"}
<ProviderForm
item={providerForm}
errors={errors}
providerTypes={data.providerTypes}
onSave={submitAddProvider}
onCancel={cancelAddProviderHandler}
/>
{/key}
{:else}
<div class="rounded border border-dashed p-2 border-stone-500 cursor-pointer" onclick={enterAddProviderHandler} tabindex="0">
<i class="icon-plus"></i>
Ajouter un fournisseur
</div>
{/if}
{#each providers as p (p.id) }
{#if editingProvider===p.id}
{#key p.id}
<ProviderForm
item={p}
errors={errors}
providerTypes={data.providerTypes}
onSave={submitEditProvider}
onCancel={cancelEditProviderHandler}
/>
{/key}
{:else}
<div class="rounded border p-2 my-2 border-stone-500 flex">
<div class="flex-1">
<span>{ p.name }</span>
<span class="text-stone-500">({ p.type })</span>
</div>
<button class="bt small mx-1" onclick={()=>handleEditProvider(p.id)}>
<i class="icon-edit"></i>
</button>
<button class="bt small mx-1" onclick={()=>handleDeleteProvider(p.id)}>
<i class="icon-trash-empty"></i>
</button>
</div>
{/if}
{/each}
</div>
@@ -1,7 +1,11 @@
export function load() {
import { providerApi } from "$lib/api/endpoints/provider"
import type { PageLoad } from './$types';
export const load: PageLoad = async () => {
return {
title: 'Paramétrage : Modèles',
back: "/settings",
theme: "settings"
theme: "settings",
providerTypes: await providerApi.providerTypes(),
};
}
@@ -0,0 +1,142 @@
<script lang="ts">
import { SvelteMap } from 'svelte/reactivity';
import FormError from '$lib/components/FormError.svelte';
import Select from 'svelte-select/no-styles';
import type { Provider, Item } from "$lib/types/api"
import { providerApi } from "$lib/api/endpoints/provider"
type Props = {
item: Provider
errors: Map<string,string>
providerTypes: Item[]
onSave: (item: Provider) => void
onCancel: () => void
}
let {
item,
errors,
providerTypes,
onSave,
onCancel,
}: Props = $props();
type Selectable = {
value: string
selected: boolean
}
let selectedModels = $state(item.models.split(";"))
let availableModels : Selectable[]= $state(item.models.split(";").map((it)=> {return {value:it, selected:true}}))
let modelsErrors = new SvelteMap<string,string>()
const autofocus = (node: HTMLElement) => {
node.focus();
}
const handleSave = (e: Event) => {
e.preventDefault()
item.models = selectedModels.join(";")
onSave(item)
}
const handleCancel = (e: Event) => {
e.preventDefault()
onCancel()
}
const fetchModels = (e: Event) => {
e.preventDefault()
modelsErrors.clear()
providerApi.getAvailableModels(item)
.then((data) => {
availableModels = data.map((it)=>{
return { value: it, selected: selectedModels.includes(it)}
});
})
.catch((err) => { modelsErrors.set("models",err) })
}
const handleModelChange = () => {
selectedModels = availableModels.filter((it)=>it.selected).map((it)=>it.value)
}
</script>
<div>
<div class="formgroup my-2">
<form onsubmit={handleSave} onchange={()=>{errors.clear()}}>
<div class="formelement">
<label for="name">Nom</label>
<input
id="name"
type="text"
bind:value={item.name}
class="textinput"
placeholder="nom affiché"
use:autofocus
/>
<FormError key="name" errors={errors} />
</div>
<div class="formelement">
<label for="type" class="block my-1 flex-1">Type</label>
<Select items={providerTypes}
valueMode="id" itemId="id" label="text"
bind:value={item.type}
/>
<FormError key="type" errors={errors} />
</div>
<div class="formelement">
<label for="baseUrl">URL</label>
<input
id="baseUrl"
type="text"
bind:value={item.baseUrl}
class="textinput"
placeholder="http://127.0.0.1:8080/v1"
autocomplete="off"
/>
<FormError key="baseUrl" errors={errors} />
</div>
<div class="formelement">
<label for="apiKey">Clé API</label>
<input
id="apiKey"
type="password"
bind:value={item.apiKey}
class="textinput"
placeholder="clé API (optionnel)"
autocomplete="new-password"
/>
<FormError key="apiKey" errors={errors} />
</div>
<div class="formelement">
<label for="apiKey">Models</label>
<button class="bt small" onclick={fetchModels}>
Afficher les modèles disponibles
</button>
<ol class="ml-2">
{#each availableModels as model (model.value)}
{#if model.value}
<li>
<label>
<input type="checkbox" bind:checked={model.selected} onchange={handleModelChange}/>
{model.value}
</label>
</li>
{/if}
{/each}
</ol>
<div><FormError key="models" errors={modelsErrors} /></div>
</div>
<div class="text-right">
<button type="button" class="bt" onclick={handleCancel}>Annuler</button>
<button type="submit" class="bt mx-4">Enregistrer</button>
</div>
</form>
</div>
</div>
+34 -18
View File
@@ -8,35 +8,45 @@ html {
@apply h-full;
}
body {
@apply h-full bg-stone-100 text-stone-900 dark:bg-stone-900 dark:text-stone-200;
@apply h-full bg-stone-100 text-stone-950 dark:bg-stone-950 dark:text-stone-100;
}
label {
.formelement > label {
@apply text-xs text-stone-600 uppercase dark:text-stone-400;
}
hr {
@apply border-stone-200 dark:border-stone-800;
}
h2 {
@apply mb-4 font-bold;
@apply mb-4 text-base font-bold;
}
.container {
@apply mx-auto max-w-3xl;
}
.pointer {
@apply cursor-pointer;
}
.bt {
appearance: button;
@apply cursor-pointer rounded bg-stone-300 px-4 py-1 font-bold hover:bg-stone-200 dark:bg-stone-800 dark:hover:bg-stone-700;
&.small {
@apply px-2 py-1;
}
}
.menuitem {
@apply text-stone-800 text-stone-950 dark:text-stone-300 hover:dark:text-stone-50;
@apply text-stone-950 dark:text-stone-300 hover:dark:text-stone-50;
}
.header {
@apply p-2 text-lg;
@apply py-2 text-lg font-bold;
/*
h1 {
@apply rounded bg-stone-200 px-2 font-bold text-sky-600 dark:bg-stone-800 dark:text-sky-200;
@apply rounded bg-stone-200 px-2 font-bold uppercase dark:bg-stone-800;
}
*/
&.settings {
h1 {
@@ -53,10 +63,6 @@ h2 {
@apply text-sky-500;
}
.formgroup {
@apply rounded border border-stone-300 bg-stone-100 p-2 dark:border-stone-700 dark:bg-stone-900;
}
#outer {
@apply flex h-full flex-row;
}
@@ -94,7 +100,7 @@ h2 {
@apply ml-10;
}
#sidebar {
@apply fixed left-0 h-full w-full bg-stone-100 dark:bg-stone-900;
@apply fixed left-0 h-full w-full bg-stone-100 dark:bg-stone-950;
}
.mobileMenuClosed {
#sidebar {
@@ -103,14 +109,24 @@ h2 {
}
}
/* svelte select */
/* form */
.textinput {
@apply h-8 w-full rounded border-stone-700 bg-white px-2 py-1 text-stone-900 dark:bg-stone-800 dark:text-stone-100;
.formgroup {
@apply rounded border border-stone-300 bg-stone-100 p-2 text-sm dark:border-stone-700 dark:bg-stone-900;
}
.formelement {
@apply mb-3;
}
.textinput {
@apply h-8 w-full rounded border-stone-700 bg-stone-50 px-2 py-1 text-sm text-stone-900 dark:bg-stone-950 dark:text-stone-100;
}
/* svelte select */
.svelte-select {
@apply relative flex h-8 w-full border-none text-stone-900 dark:bg-stone-800 dark:text-stone-100;
@apply relative flex h-8 w-full border-none text-stone-900 dark:bg-stone-950 dark:text-stone-100;
}
.svelte-select .value-container {
@@ -187,7 +203,7 @@ h2 {
}
.svelte-select-list .list-group-title {
@apply h-8 cursor-default px-5 text-sm leading-10 font-medium overflow-ellipsis whitespace-nowrap text-gray-400 uppercase;
@apply h-8 cursor-default px-5 text-sm leading-10 font-medium text-ellipsis whitespace-nowrap text-gray-400 uppercase;
}
.svelte-select-list .empty {
@@ -195,7 +211,7 @@ h2 {
}
.svelte-select-item {
@apply h-8 cursor-default overflow-hidden px-5 leading-10 overflow-ellipsis whitespace-nowrap text-gray-800;
@apply h-8 cursor-default overflow-hidden px-5 leading-10 text-ellipsis whitespace-nowrap text-gray-800;
}
.item.group-item {
@@ -223,7 +239,7 @@ h2 {
}
.multi-item {
@apply mt-1 mr-1 flex h-8 max-w-full cursor-default items-center overflow-hidden rounded-sm border border-gray-200 bg-gray-100 pr-1 pl-1 leading-8 overflow-ellipsis whitespace-nowrap;
@apply mt-1 mr-1 flex h-8 max-w-full cursor-default items-center overflow-hidden rounded-sm border border-gray-200 bg-gray-100 pr-1 pl-1 leading-8 text-ellipsis whitespace-nowrap;
}
.multi-item.disabled {