Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Unverified
Commit
43591fba
authored
Jun 29, 2026
by
CaIon
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: improve advanced custom route editor
parent
25f99859
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
288 additions
and
65 deletions
+288
-65
web/default/src/features/channels/components/dialogs/advanced-custom-editor-dialog.tsx
+205
-56
web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx
+37
-0
web/default/src/features/channels/lib/advanced-custom.ts
+46
-9
No files found.
web/default/src/features/channels/components/dialogs/advanced-custom-editor-dialog.tsx
View file @
43591fba
...
@@ -16,12 +16,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
...
@@ -16,12 +16,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
For commercial licensing, please contact support@quantumnous.com
*/
*/
import
{
type
ReactNode
,
useMemo
,
useState
}
from
'react'
import
{
type
ReactNode
,
useMemo
,
use
Ref
,
use
State
}
from
'react'
import
{
Check
,
Plus
,
Trash2
}
from
'lucide-react'
import
{
ArrowRight
,
Check
,
Plus
,
Shuffle
,
Trash2
}
from
'lucide-react'
import
{
useTranslation
}
from
'react-i18next'
import
{
useTranslation
}
from
'react-i18next'
import
{
toast
}
from
'sonner'
import
{
toast
}
from
'sonner'
import
{
Alert
,
AlertDescription
}
from
'@/components/ui/alert'
import
{
Alert
,
AlertDescription
}
from
'@/components/ui/alert'
import
{
Badge
}
from
'@/components/ui/badge'
import
{
Button
}
from
'@/components/ui/button'
import
{
Button
}
from
'@/components/ui/button'
import
{
Input
}
from
'@/components/ui/input'
import
{
Input
}
from
'@/components/ui/input'
import
{
import
{
...
@@ -34,18 +33,26 @@ import {
...
@@ -34,18 +33,26 @@ import {
}
from
'@/components/ui/select'
}
from
'@/components/ui/select'
import
{
Separator
}
from
'@/components/ui/separator'
import
{
Separator
}
from
'@/components/ui/separator'
import
{
Textarea
}
from
'@/components/ui/textarea'
import
{
Textarea
}
from
'@/components/ui/textarea'
import
{
Tooltip
,
TooltipContent
,
TooltipProvider
,
TooltipTrigger
,
}
from
'@/components/ui/tooltip'
import
{
Dialog
}
from
'@/components/dialog'
import
{
Dialog
}
from
'@/components/dialog'
import
{
cn
}
from
'@/lib/utils'
import
{
import
{
ADVANCED_CUSTOM_AUTH_MODE_OPTIONS
,
ADVANCED_CUSTOM_AUTH_MODE_OPTIONS
,
ADVANCED_CUSTOM_CONVERTER_OPTIONS
,
ADVANCED_CUSTOM_CONVERTER_OPTIONS
,
ADVANCED_CUSTOM_INCOMING_PATH_OPTIONS
,
ADVANCED_CUSTOM_TEMPLATE_OPTIONS
,
ADVANCED_CUSTOM_TEMPLATE_OPTIONS
,
type
AdvancedCustomAuthMode
,
type
AdvancedCustomAuthMode
,
buildAdvancedCustomAuth
,
buildAdvancedCustomAuth
,
createAdvancedCustomConfig
,
createAdvancedCustomConfig
,
createAdvancedCustomRoute
,
createAdvancedCustomRoute
,
getAdvancedCustomAuthMode
,
getAdvancedCustomAuthMode
,
getAdvancedCustomConverterOptions
,
getAdvancedCustomIncomingPathLabel
,
getAdvancedCustomIncomingPathLabel
,
getAdvancedCustomIncomingPathOptions
,
getAdvancedCustomTemplateConfig
,
getAdvancedCustomTemplateConfig
,
getAdvancedCustomUpstreamPathPlaceholder
,
getAdvancedCustomUpstreamPathPlaceholder
,
getDefaultAdvancedCustomIncomingPath
,
getDefaultAdvancedCustomIncomingPath
,
...
@@ -74,6 +81,10 @@ type AdvancedCustomEditMode = 'visual' | 'json'
...
@@ -74,6 +81,10 @@ type AdvancedCustomEditMode = 'visual' | 'json'
const
longSelectContentClass
=
'w-[360px] max-w-[calc(100vw-2rem)]'
const
longSelectContentClass
=
'w-[360px] max-w-[calc(100vw-2rem)]'
const
longSelectItemClass
=
const
longSelectItemClass
=
'items-start py-2 [&_[data-slot=select-item-text]]:min-w-0 [&_[data-slot=select-item-text]]:shrink [&_[data-slot=select-item-text]]:whitespace-normal'
'items-start py-2 [&_[data-slot=select-item-text]]:min-w-0 [&_[data-slot=select-item-text]]:shrink [&_[data-slot=select-item-text]]:whitespace-normal'
const
routeEditorGridClassName
=
'lg:grid-cols-[7rem_minmax(0,1.45fr)_minmax(0,1.35fr)_minmax(0,1fr)_minmax(0,0.85fr)_2rem]'
const
upstreamPathDescriptionKey
=
'Use a path to append it to the channel Base URL, or enter a full URL to override the Base URL for this route.'
function
getOptionLabel
(
function
getOptionLabel
(
options
:
ReadonlyArray
<
{
value
:
string
;
label
:
string
}
>
,
options
:
ReadonlyArray
<
{
value
:
string
;
label
:
string
}
>
,
...
@@ -89,9 +100,18 @@ export function AdvancedCustomEditorDialog({
...
@@ -89,9 +100,18 @@ export function AdvancedCustomEditorDialog({
onSave
,
onSave
,
}:
AdvancedCustomEditorDialogProps
)
{
}:
AdvancedCustomEditorDialogProps
)
{
const
{
t
}
=
useTranslation
()
const
{
t
}
=
useTranslation
()
const
routeKeyCounterRef
=
useRef
(
0
)
const
[
config
,
setConfig
]
=
useState
<
AdvancedCustomConfig
>
(
const
[
config
,
setConfig
]
=
useState
<
AdvancedCustomConfig
>
(
()
=>
parseAdvancedCustomConfig
(
value
)
||
createAdvancedCustomConfig
()
()
=>
parseAdvancedCustomConfig
(
value
)
||
createAdvancedCustomConfig
()
)
)
const
[
routeKeys
,
setRouteKeys
]
=
useState
<
string
[]
>
(()
=>
{
const
initialConfig
=
parseAdvancedCustomConfig
(
value
)
||
createAdvancedCustomConfig
()
const
normalized
=
normalizeAdvancedCustomConfig
(
initialConfig
)
return
(
normalized
.
advanced_routes
||
[]).
map
(
(
_
,
routeIndex
)
=>
`advanced-custom-route-initial-
${
routeIndex
}
`
)
})
const
[
editMode
,
setEditMode
]
=
useState
<
AdvancedCustomEditMode
>
(
'visual'
)
const
[
editMode
,
setEditMode
]
=
useState
<
AdvancedCustomEditMode
>
(
'visual'
)
const
[
jsonText
,
setJsonText
]
=
useState
(()
=>
const
[
jsonText
,
setJsonText
]
=
useState
(()
=>
stringifyAdvancedCustomConfig
(
stringifyAdvancedCustomConfig
(
...
@@ -112,11 +132,28 @@ export function AdvancedCustomEditorDialog({
...
@@ -112,11 +132,28 @@ export function AdvancedCustomEditorDialog({
[
config
]
[
config
]
)
)
const
routes
=
normalizedConfig
.
advanced_routes
||
[]
const
routes
=
normalizedConfig
.
advanced_routes
||
[]
const
routeRows
=
routes
.
map
((
route
,
index
)
=>
({
route
,
routeKey
:
routeKeys
.
at
(
index
)
||
route
.
incoming_path
||
route
.
upstream_path
||
route
.
converter
||
'advanced-custom-route'
,
}))
const
validationError
=
useMemo
(
const
validationError
=
useMemo
(
()
=>
validateAdvancedCustomConfig
(
normalizedConfig
),
()
=>
validateAdvancedCustomConfig
(
normalizedConfig
),
[
normalizedConfig
]
[
normalizedConfig
]
)
)
const
createRouteKey
=
()
=>
{
routeKeyCounterRef
.
current
+=
1
return
`advanced-custom-route-
${
routeKeyCounterRef
.
current
}
`
}
const
createRouteKeys
=
(
count
:
number
)
=>
Array
.
from
({
length
:
count
},
()
=>
createRouteKey
())
const
updateRoute
=
(
index
:
number
,
patch
:
Partial
<
AdvancedCustomRoute
>
)
=>
{
const
updateRoute
=
(
index
:
number
,
patch
:
Partial
<
AdvancedCustomRoute
>
)
=>
{
setConfig
((
current
)
=>
{
setConfig
((
current
)
=>
{
const
next
=
normalizeAdvancedCustomConfig
(
current
)
const
next
=
normalizeAdvancedCustomConfig
(
current
)
...
@@ -137,6 +174,7 @@ export function AdvancedCustomEditorDialog({
...
@@ -137,6 +174,7 @@ export function AdvancedCustomEditorDialog({
],
],
}
}
})
})
setRouteKeys
((
current
)
=>
[...
current
,
createRouteKey
()])
}
}
const
removeRoute
=
(
index
:
number
)
=>
{
const
removeRoute
=
(
index
:
number
)
=>
{
...
@@ -149,6 +187,9 @@ export function AdvancedCustomEditorDialog({
...
@@ -149,6 +187,9 @@ export function AdvancedCustomEditorDialog({
),
),
}
}
})
})
setRouteKeys
((
current
)
=>
current
.
filter
((
_
,
routeIndex
)
=>
routeIndex
!==
index
)
)
}
}
const
parseJsonEditorConfig
=
():
AdvancedCustomConfig
|
null
=>
{
const
parseJsonEditorConfig
=
():
AdvancedCustomConfig
|
null
=>
{
...
@@ -171,7 +212,9 @@ export function AdvancedCustomEditorDialog({
...
@@ -171,7 +212,9 @@ export function AdvancedCustomEditorDialog({
const
switchToVisualMode
=
()
=>
{
const
switchToVisualMode
=
()
=>
{
const
parsed
=
parseJsonEditorConfig
()
const
parsed
=
parseJsonEditorConfig
()
if
(
!
parsed
)
return
if
(
!
parsed
)
return
setConfig
(
parsed
)
const
normalized
=
normalizeAdvancedCustomConfig
(
parsed
)
setConfig
(
normalized
)
setRouteKeys
(
createRouteKeys
(
normalized
.
advanced_routes
?.
length
||
0
))
setEditMode
(
'visual'
)
setEditMode
(
'visual'
)
}
}
...
@@ -212,6 +255,7 @@ export function AdvancedCustomEditorDialog({
...
@@ -212,6 +255,7 @@ export function AdvancedCustomEditorDialog({
const
normalized
=
normalizeAdvancedCustomConfig
(
nextConfig
)
const
normalized
=
normalizeAdvancedCustomConfig
(
nextConfig
)
setConfig
(
normalized
)
setConfig
(
normalized
)
setRouteKeys
(
createRouteKeys
(
normalized
.
advanced_routes
?.
length
||
0
))
setJsonText
(
stringifyAdvancedCustomConfig
(
normalized
))
setJsonText
(
stringifyAdvancedCustomConfig
(
normalized
))
setJsonError
(
''
)
setJsonError
(
''
)
}
}
...
@@ -341,8 +385,8 @@ export function AdvancedCustomEditorDialog({
...
@@ -341,8 +385,8 @@ export function AdvancedCustomEditorDialog({
</
div
>
</
div
>
{
editMode
===
'visual'
?
(
{
editMode
===
'visual'
?
(
<
div
className=
'flex flex-col gap-
5 p-4
'
>
<
div
className=
'flex flex-col gap-
4 p-4 lg:gap-3
'
>
<
div
className=
'flex justify-end border-y py-4'
>
<
div
className=
'flex justify-end border-y py-4
lg:py-2
'
>
<
Button
<
Button
type=
'button'
type=
'button'
variant=
'outline'
variant=
'outline'
...
@@ -365,11 +409,28 @@ export function AdvancedCustomEditorDialog({
...
@@ -365,11 +409,28 @@ export function AdvancedCustomEditorDialog({
</
Alert
>
</
Alert
>
)
:
null
}
)
:
null
}
<
div
className=
'flex flex-col gap-4'
>
<
p
className=
'text-muted-foreground bg-muted/30 hidden rounded-md border px-3 py-2 text-xs leading-relaxed lg:block'
>
{
routes
.
map
((
route
,
index
)
=>
(
{
t
(
upstreamPathDescriptionKey
)
}
</
p
>
<
div
className=
'flex flex-col gap-4 lg:gap-2'
>
<
div
className=
{
cn
(
'text-muted-foreground hidden items-center gap-2 px-3 text-xs font-medium lg:grid'
,
routeEditorGridClassName
)
}
>
<
span
>
{
t
(
'Route'
)
}
</
span
>
<
span
>
{
t
(
'Incoming path'
)
}
</
span
>
<
span
>
{
t
(
'Upstream path'
)
}
</
span
>
<
span
>
{
t
(
'Converter'
)
}
</
span
>
<
span
>
{
t
(
'Auth'
)
}
</
span
>
<
span
aria
-
hidden=
'true'
/>
</
div
>
{
routeRows
.
map
((
routeRow
,
index
)
=>
(
<
RouteEditor
<
RouteEditor
key=
{
index
}
key=
{
routeRow
.
routeKey
}
route=
{
route
}
route=
{
route
Row
.
route
}
index=
{
index
}
index=
{
index
}
onChange=
{
(
patch
)
=>
updateRoute
(
index
,
patch
)
}
onChange=
{
(
patch
)
=>
updateRoute
(
index
,
patch
)
}
onRemove=
{
()
=>
removeRoute
(
index
)
}
onRemove=
{
()
=>
removeRoute
(
index
)
}
...
@@ -429,14 +490,16 @@ function RouteEditor({
...
@@ -429,14 +490,16 @@ function RouteEditor({
const
authMode
=
getAdvancedCustomAuthMode
(
route
)
const
authMode
=
getAdvancedCustomAuthMode
(
route
)
const
incomingPath
=
const
incomingPath
=
route
.
incoming_path
||
getDefaultAdvancedCustomIncomingPath
(
converter
)
route
.
incoming_path
||
getDefaultAdvancedCustomIncomingPath
(
converter
)
const
incomingPath
Options
=
useMemo
(
const
converter
Options
=
useMemo
(
()
=>
getAdvancedCustom
IncomingPathOptions
(
converter
),
()
=>
getAdvancedCustom
ConverterOptions
(
incomingPath
),
[
converter
]
[
incomingPath
]
)
)
const
incomingPathLabel
=
getAdvancedCustomIncomingPathLabel
(
incomingPath
)
const
incomingPathLabel
=
getAdvancedCustomIncomingPathLabel
(
incomingPath
)
const
converterLabel
=
const
converterLabel
=
getOptionLabel
(
ADVANCED_CUSTOM_CONVERTER_OPTIONS
,
converter
)
getOptionLabel
(
ADVANCED_CUSTOM_CONVERTER_OPTIONS
,
converter
)
const
authLabel
=
getOptionLabel
(
ADVANCED_CUSTOM_AUTH_MODE_OPTIONS
,
authMode
)
const
authLabel
=
getOptionLabel
(
ADVANCED_CUSTOM_AUTH_MODE_OPTIONS
,
authMode
)
const
isNativeConverter
=
converter
===
'none'
const
ConverterVisualIcon
=
isNativeConverter
?
ArrowRight
:
Shuffle
const
setConverter
=
(
nextConverter
:
AdvancedCustomConverter
)
=>
{
const
setConverter
=
(
nextConverter
:
AdvancedCustomConverter
)
=>
{
const
patch
:
Partial
<
AdvancedCustomRoute
>
=
{
converter
:
nextConverter
}
const
patch
:
Partial
<
AdvancedCustomRoute
>
=
{
converter
:
nextConverter
}
...
@@ -446,6 +509,18 @@ function RouteEditor({
...
@@ -446,6 +509,18 @@ function RouteEditor({
onChange
(
patch
)
onChange
(
patch
)
}
}
const
setIncomingPath
=
(
nextIncomingPath
:
string
|
null
)
=>
{
const
resolvedIncomingPath
=
nextIncomingPath
||
getDefaultAdvancedCustomIncomingPath
(
converter
)
const
patch
:
Partial
<
AdvancedCustomRoute
>
=
{
incoming_path
:
resolvedIncomingPath
,
}
if
(
!
isAdvancedCustomIncomingPathAllowed
(
resolvedIncomingPath
,
converter
))
{
patch
.
converter
=
'none'
}
onChange
(
patch
)
}
const
setAuthMode
=
(
mode
:
AdvancedCustomAuthMode
)
=>
{
const
setAuthMode
=
(
mode
:
AdvancedCustomAuthMode
)
=>
{
onChange
({
auth
:
buildAdvancedCustomAuth
(
mode
,
route
.
auth
)
})
onChange
({
auth
:
buildAdvancedCustomAuth
(
mode
,
route
.
auth
)
})
}
}
...
@@ -467,36 +542,68 @@ function RouteEditor({
...
@@ -467,36 +542,68 @@ function RouteEditor({
}
}
return
(
return
(
<
div
className=
'border-border flex flex-col gap-4 rounded-md border p-4'
>
<
div
className=
'border-border flex flex-col gap-4 rounded-md border p-4 lg:gap-2 lg:p-3'
>
<
div
className=
'flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between'
>
<
div
<
div
className=
'min-w-0 space-y-2'
>
className=
{
cn
(
<
div
className=
'flex flex-wrap items-center gap-2'
>
'grid gap-4 md:grid-cols-2 lg:items-center lg:gap-2'
,
<
div
className=
'text-sm font-medium'
>
routeEditorGridClassName
{
t
(
'Route'
)
}
{
index
+
1
}
)
}
>
<
div
className=
'flex min-w-0 items-start justify-between gap-3 md:col-span-2 lg:col-span-1'
>
<
div
className=
'min-w-0 space-y-2 lg:space-y-1'
>
<
div
className=
'flex flex-wrap items-center gap-2'
>
<
div
className=
'text-sm font-medium'
>
{
t
(
'Route'
)
}
{
index
+
1
}
</
div
>
<
TooltipProvider
delay=
{
100
}
>
<
Tooltip
>
<
TooltipTrigger
render=
{
<
span
className=
{
cn
(
'border-border inline-flex size-7 shrink-0 items-center justify-center rounded-md border'
,
isNativeConverter
?
'bg-secondary text-secondary-foreground'
:
'bg-muted text-foreground'
)
}
/>
}
>
<
ConverterVisualIcon
className=
'size-3.5'
aria
-
hidden=
'true'
/>
<
span
className=
'sr-only'
>
{
t
(
converterLabel
)
}
</
span
>
</
TooltipTrigger
>
<
TooltipContent
side=
'top'
>
{
t
(
converterLabel
)
}
</
TooltipContent
>
</
Tooltip
>
</
TooltipProvider
>
</
div
>
</
div
>
<
Badge
variant=
'secondary'
>
{
t
(
converterLabel
)
}
</
Badge
>
</
div
>
</
div
>
<
Button
type=
'button'
variant=
'ghost'
size=
'icon'
className=
'lg:hidden'
onClick=
{
onRemove
}
>
<
Trash2
className=
'h-4 w-4'
/>
<
span
className=
'sr-only'
>
{
t
(
'Delete'
)
}
</
span
>
</
Button
>
</
div
>
</
div
>
<
Button
type=
'button'
variant=
'ghost'
size=
'icon'
onClick=
{
onRemove
}
>
<
Trash2
className=
'h-4 w-4'
/>
<
span
className=
'sr-only'
>
{
t
(
'Delete'
)
}
</
span
>
</
Button
>
</
div
>
<
div
className=
'grid gap-4 md:grid-cols-2'
>
<
FieldBlock
<
FieldBlock
label=
{
t
(
'Incoming path'
)
}
>
label=
{
t
(
'Incoming path'
)
}
className=
'lg:gap-1'
labelClassName=
'lg:sr-only'
>
<
Select
<
Select
value=
{
incomingPath
}
value=
{
incomingPath
}
onValueChange=
{
(
value
)
=>
onValueChange=
{
setIncomingPath
}
onChange
({
incoming_path
:
value
||
getDefaultAdvancedCustomIncomingPath
(
converter
),
})
}
>
>
<
SelectTrigger
className=
'w-full max-w-full'
>
<
SelectTrigger
className=
'w-full max-w-full
lg:h-8
'
>
<
SelectValue
className=
'min-w-0 truncate'
>
<
SelectValue
className=
'min-w-0 truncate'
>
{
`${
t(incomingPathLabel)} · ${incomingPath
}`
}
{
`${
incomingPathLabel
}`
}
</
SelectValue
>
</
SelectValue
>
</
SelectTrigger
>
</
SelectTrigger
>
<
SelectContent
<
SelectContent
...
@@ -504,14 +611,14 @@ function RouteEditor({
...
@@ -504,14 +611,14 @@ function RouteEditor({
className=
{
longSelectContentClass
}
className=
{
longSelectContentClass
}
>
>
<
SelectGroup
>
<
SelectGroup
>
{
incomingPathOptions
.
map
((
option
)
=>
(
{
ADVANCED_CUSTOM_INCOMING_PATH_OPTIONS
.
map
((
option
)
=>
(
<
SelectItem
<
SelectItem
key=
{
option
.
value
}
key=
{
option
.
value
}
value=
{
option
.
value
}
value=
{
option
.
value
}
className=
{
longSelectItemClass
}
className=
{
longSelectItemClass
}
>
>
<
div
className=
'flex min-w-0 flex-col gap-1 whitespace-normal leading-snug'
>
<
div
className=
'flex min-w-0 flex-col gap-1 whitespace-normal leading-snug'
>
<
span
>
{
t
(
option
.
label
)
}
</
span
>
<
span
>
{
option
.
label
}
</
span
>
<
span
className=
'text-muted-foreground break-all font-mono text-xs'
>
<
span
className=
'text-muted-foreground break-all font-mono text-xs'
>
{
option
.
value
}
{
option
.
value
}
</
span
>
</
span
>
...
@@ -523,7 +630,11 @@ function RouteEditor({
...
@@ -523,7 +630,11 @@ function RouteEditor({
</
Select
>
</
Select
>
</
FieldBlock
>
</
FieldBlock
>
<
FieldBlock
label=
{
t
(
'Upstream path'
)
}
>
<
FieldBlock
label=
{
t
(
'Upstream path'
)
}
className=
'lg:gap-1'
labelClassName=
'lg:sr-only'
>
<
Input
<
Input
value=
{
route
.
upstream_path
||
''
}
value=
{
route
.
upstream_path
||
''
}
onChange=
{
(
event
)
=>
onChange=
{
(
event
)
=>
...
@@ -533,23 +644,23 @@ function RouteEditor({
...
@@ -533,23 +644,23 @@ function RouteEditor({
}
}
placeholder=
{
getAdvancedCustomUpstreamPathPlaceholder
(
converter
)
}
placeholder=
{
getAdvancedCustomUpstreamPathPlaceholder
(
converter
)
}
/>
/>
<
p
className=
'text-muted-foreground text-xs leading-relaxed'
>
<
p
className=
'text-muted-foreground text-xs leading-relaxed lg:hidden'
>
{
t
(
{
t
(
upstreamPathDescriptionKey
)
}
'Use a path to append it to the channel Base URL, or enter a full URL to override the Base URL for this route.'
)
}
</
p
>
</
p
>
</
FieldBlock
>
</
FieldBlock
>
</
div
>
<
div
className=
'grid gap-4 md:grid-cols-2'
>
<
FieldBlock
<
FieldBlock
label=
{
t
(
'Converter'
)
}
>
label=
{
t
(
'Converter'
)
}
className=
'lg:gap-1'
labelClassName=
'lg:sr-only'
>
<
Select
<
Select
value=
{
converter
}
value=
{
converter
}
onValueChange=
{
(
value
)
=>
onValueChange=
{
(
value
)
=>
setConverter
(
value
as
AdvancedCustomConverter
)
setConverter
(
value
as
AdvancedCustomConverter
)
}
}
>
>
<
SelectTrigger
className=
'w-full max-w-full'
>
<
SelectTrigger
className=
'w-full max-w-full
lg:h-8
'
>
<
SelectValue
className=
'min-w-0 truncate'
>
<
SelectValue
className=
'min-w-0 truncate'
>
{
t
(
converterLabel
)
}
{
t
(
converterLabel
)
}
</
SelectValue
>
</
SelectValue
>
...
@@ -559,7 +670,7 @@ function RouteEditor({
...
@@ -559,7 +670,7 @@ function RouteEditor({
className=
{
longSelectContentClass
}
className=
{
longSelectContentClass
}
>
>
<
SelectGroup
>
<
SelectGroup
>
{
ADVANCED_CUSTOM_CONVERTER_OPTIONS
.
map
((
option
)
=>
(
{
converterOptions
.
map
((
option
)
=>
(
<
SelectItem
<
SelectItem
key=
{
option
.
value
}
key=
{
option
.
value
}
value=
{
option
.
value
}
value=
{
option
.
value
}
...
@@ -575,14 +686,18 @@ function RouteEditor({
...
@@ -575,14 +686,18 @@ function RouteEditor({
</
Select
>
</
Select
>
</
FieldBlock
>
</
FieldBlock
>
<
FieldBlock
label=
{
t
(
'Auth'
)
}
>
<
FieldBlock
label=
{
t
(
'Auth'
)
}
className=
'lg:gap-1'
labelClassName=
'lg:sr-only'
>
<
Select
<
Select
value=
{
authMode
}
value=
{
authMode
}
onValueChange=
{
(
value
)
=>
onValueChange=
{
(
value
)
=>
setAuthMode
(
value
as
AdvancedCustomAuthMode
)
setAuthMode
(
value
as
AdvancedCustomAuthMode
)
}
}
>
>
<
SelectTrigger
className=
'w-full max-w-full'
>
<
SelectTrigger
className=
'w-full max-w-full
lg:h-8
'
>
<
SelectValue
className=
'min-w-0 truncate'
>
<
SelectValue
className=
'min-w-0 truncate'
>
{
t
(
authLabel
)
}
{
t
(
authLabel
)
}
</
SelectValue
>
</
SelectValue
>
...
@@ -598,13 +713,34 @@ function RouteEditor({
...
@@ -598,13 +713,34 @@ function RouteEditor({
</
SelectContent
>
</
SelectContent
>
</
Select
>
</
Select
>
</
FieldBlock
>
</
FieldBlock
>
<
Button
type=
'button'
variant=
'ghost'
size=
'icon'
className=
'hidden lg:inline-flex'
onClick=
{
onRemove
}
>
<
Trash2
className=
'h-4 w-4'
/>
<
span
className=
'sr-only'
>
{
t
(
'Delete'
)
}
</
span
>
</
Button
>
</
div
>
</
div
>
{
authMode
===
'header'
||
authMode
===
'query'
?
(
{
authMode
===
'header'
||
authMode
===
'query'
?
(
<>
<>
<
Separator
/>
<
Separator
className=
'lg:hidden'
/>
<
div
className=
'grid gap-4 md:grid-cols-2'
>
<
div
<
FieldBlock
label=
{
t
(
'Auth name'
)
}
>
className=
{
cn
(
'grid gap-4 md:grid-cols-2 lg:items-end lg:gap-2 lg:border-t lg:pt-2'
,
routeEditorGridClassName
)
}
>
<
span
className=
'hidden lg:block'
aria
-
hidden=
'true'
/>
<
FieldBlock
label=
{
t
(
'Auth name'
)
}
className=
'lg:gap-1'
labelClassName=
'lg:text-xs'
>
<
Input
<
Input
value=
{
route
.
auth
?.
name
||
''
}
value=
{
route
.
auth
?.
name
||
''
}
onChange=
{
(
event
)
=>
updateAuth
(
'name'
,
event
.
target
.
value
)
}
onChange=
{
(
event
)
=>
updateAuth
(
'name'
,
event
.
target
.
value
)
}
...
@@ -613,7 +749,11 @@ function RouteEditor({
...
@@ -613,7 +749,11 @@ function RouteEditor({
}
}
/>
/>
</
FieldBlock
>
</
FieldBlock
>
<
FieldBlock
label=
{
t
(
'Auth value'
)
}
>
<
FieldBlock
label=
{
t
(
'Auth value'
)
}
className=
'lg:gap-1'
labelClassName=
'lg:text-xs'
>
<
Input
<
Input
value=
{
route
.
auth
?.
value
||
''
}
value=
{
route
.
auth
?.
value
||
''
}
onChange=
{
(
event
)
=>
updateAuth
(
'value'
,
event
.
target
.
value
)
}
onChange=
{
(
event
)
=>
updateAuth
(
'value'
,
event
.
target
.
value
)
}
...
@@ -622,6 +762,9 @@ function RouteEditor({
...
@@ -622,6 +762,9 @@ function RouteEditor({
}
}
/>
/>
</
FieldBlock
>
</
FieldBlock
>
<
span
className=
'hidden lg:block'
aria
-
hidden=
'true'
/>
<
span
className=
'hidden lg:block'
aria
-
hidden=
'true'
/>
<
span
className=
'hidden lg:block'
aria
-
hidden=
'true'
/>
</
div
>
</
div
>
</>
</>
)
:
null
}
)
:
null
}
...
@@ -631,14 +774,20 @@ function RouteEditor({
...
@@ -631,14 +774,20 @@ function RouteEditor({
function
FieldBlock
({
function
FieldBlock
({
label
,
label
,
className
,
labelClassName
,
children
,
children
,
}:
{
}:
{
label
:
string
label
:
string
className
?:
string
labelClassName
?:
string
children
:
ReactNode
children
:
ReactNode
})
{
})
{
return
(
return
(
<
div
className=
'flex min-w-0 flex-col gap-2'
>
<
div
className=
{
cn
(
'flex min-w-0 flex-col gap-2'
,
className
)
}
>
<
span
className=
'text-sm font-medium'
>
{
label
}
</
span
>
<
span
className=
{
cn
(
'text-sm font-medium'
,
labelClassName
)
}
>
{
label
}
</
span
>
{
children
}
{
children
}
</
div
>
</
div
>
)
)
...
...
web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx
View file @
43591fba
...
@@ -256,6 +256,7 @@ const ADVANCED_SETTINGS_SECTION_IDS = {
...
@@ -256,6 +256,7 @@ const ADVANCED_SETTINGS_SECTION_IDS = {
const
ADVANCED_SETTINGS_CHILD_SECTION_IDS
:
string
[]
=
Object
.
values
(
const
ADVANCED_SETTINGS_CHILD_SECTION_IDS
:
string
[]
=
Object
.
values
(
ADVANCED_SETTINGS_SECTION_IDS
ADVANCED_SETTINGS_SECTION_IDS
)
)
const
ADVANCED_CUSTOM_ROUTE_TYPE_PREVIEW_LIMIT
=
3
const
UPSTREAM_DETECTED_MODEL_PREVIEW_LIMIT
=
8
const
UPSTREAM_DETECTED_MODEL_PREVIEW_LIMIT
=
8
const
SENSITIVE_FORM_FIELDS
=
[
const
SENSITIVE_FORM_FIELDS
=
[
'type'
,
'type'
,
...
@@ -776,6 +777,18 @@ export function ChannelMutateDrawer({
...
@@ -776,6 +777,18 @@ export function ChannelMutateDrawer({
()
=>
getAdvancedCustomStats
(
currentAdvancedCustom
),
()
=>
getAdvancedCustomStats
(
currentAdvancedCustom
),
[
currentAdvancedCustom
]
[
currentAdvancedCustom
]
)
)
const
advancedCustomRouteTypeLabels
=
advancedCustomStats
.
routeTypeLabels
.
slice
(
0
,
ADVANCED_CUSTOM_ROUTE_TYPE_PREVIEW_LIMIT
)
const
hiddenAdvancedCustomRouteTypeCount
=
advancedCustomStats
.
routeTypeLabels
.
length
-
advancedCustomRouteTypeLabels
.
length
const
advancedCustomRouteTypeTitle
=
hiddenAdvancedCustomRouteTypeCount
>
0
?
advancedCustomStats
.
routeTypeLabels
.
join
(
', '
)
:
undefined
// Get all models list
// Get all models list
const
allModelsList
=
useMemo
(
const
allModelsList
=
useMemo
(
...
@@ -2647,6 +2660,30 @@ export function ChannelMutateDrawer({
...
@@ -2647,6 +2660,30 @@ export function ChannelMutateDrawer({
{
t
(
'Routes'
)
}
:
{
' '
}
{
t
(
'Routes'
)
}
:
{
' '
}
{
advancedCustomStats
.
routeCount
}
{
advancedCustomStats
.
routeCount
}
</
Badge
>
</
Badge
>
{
advancedCustomRouteTypeLabels
.
map
(
(
label
)
=>
(
<
Badge
key=
{
label
}
variant=
'outline'
className=
'max-w-[12rem]'
title=
{
label
}
>
<
span
className=
'truncate'
>
{
label
}
</
span
>
</
Badge
>
)
)
}
{
hiddenAdvancedCustomRouteTypeCount
>
0
&&
(
<
Badge
variant=
'outline'
title=
{
advancedCustomRouteTypeTitle
}
>
+
{
hiddenAdvancedCustomRouteTypeCount
}
</
Badge
>
)
}
{
!
advancedCustomStats
.
valid
&&
(
{
!
advancedCustomStats
.
valid
&&
(
<
Badge
variant=
'destructive'
>
<
Badge
variant=
'destructive'
>
{
t
(
'Incomplete'
)
}
{
t
(
'Incomplete'
)
}
...
...
web/default/src/features/channels/lib/advanced-custom.ts
View file @
43591fba
...
@@ -78,11 +78,7 @@ export const ADVANCED_CUSTOM_INCOMING_PATH_OPTIONS: AdvancedCustomIncomingPathOp
...
@@ -78,11 +78,7 @@ export const ADVANCED_CUSTOM_INCOMING_PATH_OPTIONS: AdvancedCustomIncomingPathOp
[
[
{
{
value
:
'/v1/chat/completions'
,
value
:
'/v1/chat/completions'
,
label
:
'OpenAI Chat Completions'
,
label
:
'OpenAI Chat'
,
},
{
value
:
'/v1/completions'
,
label
:
'OpenAI Completions'
,
},
},
{
{
value
:
'/v1/responses'
,
value
:
'/v1/responses'
,
...
@@ -105,6 +101,10 @@ export const ADVANCED_CUSTOM_INCOMING_PATH_OPTIONS: AdvancedCustomIncomingPathOp
...
@@ -105,6 +101,10 @@ export const ADVANCED_CUSTOM_INCOMING_PATH_OPTIONS: AdvancedCustomIncomingPathOp
label
:
'OpenAI Image Edits'
,
label
:
'OpenAI Image Edits'
,
},
},
{
{
value
:
'/v1/completions'
,
label
:
'OpenAI Completions'
,
},
{
value
:
'/v1/audio/speech'
,
value
:
'/v1/audio/speech'
,
label
:
'OpenAI Audio Speech'
,
label
:
'OpenAI Audio Speech'
,
},
},
...
@@ -142,6 +142,10 @@ export const ADVANCED_CUSTOM_INCOMING_PATH_OPTIONS: AdvancedCustomIncomingPathOp
...
@@ -142,6 +142,10 @@ export const ADVANCED_CUSTOM_INCOMING_PATH_OPTIONS: AdvancedCustomIncomingPathOp
},
},
]
]
const
ADVANCED_CUSTOM_ROUTE_SUMMARY_LABELS
:
Record
<
string
,
string
>
=
{
'/v1/chat/completions'
:
'OpenAI Chat'
,
}
export
type
AdvancedCustomValidationError
=
{
export
type
AdvancedCustomValidationError
=
{
message
:
string
message
:
string
routeIndex
?:
number
routeIndex
?:
number
...
@@ -357,8 +361,17 @@ export function isAdvancedCustomIncomingPathAllowed(
...
@@ -357,8 +361,17 @@ export function isAdvancedCustomIncomingPathAllowed(
incomingPath
:
string
,
incomingPath
:
string
,
converter
:
AdvancedCustomConverter
converter
:
AdvancedCustomConverter
):
boolean
{
):
boolean
{
return
getAdvancedCustomIncomingPathOptions
(
converter
).
some
(
return
isConverterPathAllowed
(
incomingPath
,
converter
)
(
option
)
=>
option
.
value
===
incomingPath
}
export
function
getAdvancedCustomConverterOptions
(
incomingPath
:
string
):
typeof
ADVANCED_CUSTOM_CONVERTER_OPTIONS
{
const
normalizedIncomingPath
=
incomingPath
.
trim
()
return
ADVANCED_CUSTOM_CONVERTER_OPTIONS
.
filter
(
(
option
)
=>
option
.
value
===
'none'
||
isConverterPathAllowed
(
normalizedIncomingPath
,
option
.
value
)
)
)
}
}
...
@@ -483,15 +496,28 @@ export function advancedCustomConfigUsesRelativeUpstreamPath(
...
@@ -483,15 +496,28 @@ export function advancedCustomConfigUsesRelativeUpstreamPath(
export
function
getAdvancedCustomStats
(
value
:
string
|
undefined
):
{
export
function
getAdvancedCustomStats
(
value
:
string
|
undefined
):
{
routeCount
:
number
routeCount
:
number
valid
:
boolean
valid
:
boolean
routeTypeLabels
:
string
[]
}
{
}
{
const
config
=
parseAdvancedCustomConfig
(
value
)
const
config
=
parseAdvancedCustomConfig
(
value
)
if
(
!
config
)
{
if
(
!
config
)
{
return
{
routeCount
:
0
,
valid
:
false
}
return
{
routeCount
:
0
,
valid
:
false
,
routeTypeLabels
:
[]
}
}
}
const
normalized
=
normalizeAdvancedCustomConfig
(
config
)
const
normalized
=
normalizeAdvancedCustomConfig
(
config
)
const
routes
=
normalized
.
advanced_routes
||
[]
const
routeTypeLabels
:
string
[]
=
[]
const
seenRouteTypeLabels
=
new
Set
<
string
>
()
for
(
const
route
of
routes
)
{
const
label
=
getAdvancedCustomRouteSummaryLabel
(
route
)
if
(
!
label
||
seenRouteTypeLabels
.
has
(
label
))
continue
routeTypeLabels
.
push
(
label
)
seenRouteTypeLabels
.
add
(
label
)
}
return
{
return
{
routeCount
:
normalized
.
advanced_routes
?.
length
||
0
,
routeCount
:
routes
.
length
,
valid
:
validateAdvancedCustomConfig
(
normalized
)
===
null
,
valid
:
validateAdvancedCustomConfig
(
normalized
)
===
null
,
routeTypeLabels
,
}
}
}
}
...
@@ -543,6 +569,17 @@ function getAdvancedCustomRouteUpstreamPath(route: AdvancedCustomRoute): string
...
@@ -543,6 +569,17 @@ function getAdvancedCustomRouteUpstreamPath(route: AdvancedCustomRoute): string
return
(
route
.
upstream_path
||
''
).
trim
()
return
(
route
.
upstream_path
||
''
).
trim
()
}
}
function
getAdvancedCustomRouteSummaryLabel
(
route
:
AdvancedCustomRoute
):
string
|
null
{
const
incomingPath
=
route
.
incoming_path
?.
trim
()
||
''
if
(
!
incomingPath
)
return
null
return
(
ADVANCED_CUSTOM_ROUTE_SUMMARY_LABELS
[
incomingPath
]
||
getAdvancedCustomIncomingPathLabel
(
incomingPath
)
)
}
function
isFullHttpURLOrAbsolutePath
(
value
:
string
):
boolean
{
function
isFullHttpURLOrAbsolutePath
(
value
:
string
):
boolean
{
if
(
value
.
startsWith
(
'/'
))
return
!
value
.
startsWith
(
'//'
)
if
(
value
.
startsWith
(
'/'
))
return
!
value
.
startsWith
(
'//'
)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment