AddServerModal.svelte 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. <script lang="ts">
  2. import { toast } from 'svelte-sonner';
  3. import { getContext, onMount } from 'svelte';
  4. const i18n = getContext('i18n');
  5. import { settings } from '$lib/stores';
  6. import Modal from '$lib/components/common/Modal.svelte';
  7. import Plus from '$lib/components/icons/Plus.svelte';
  8. import Minus from '$lib/components/icons/Minus.svelte';
  9. import PencilSolid from '$lib/components/icons/PencilSolid.svelte';
  10. import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
  11. import Tooltip from '$lib/components/common/Tooltip.svelte';
  12. import Switch from '$lib/components/common/Switch.svelte';
  13. import Tags from './common/Tags.svelte';
  14. import { getToolServerData } from '$lib/apis';
  15. import { verifyToolServerConnection } from '$lib/apis/configs';
  16. import AccessControl from './workspace/common/AccessControl.svelte';
  17. import Spinner from '$lib/components/common/Spinner.svelte';
  18. import XMark from '$lib/components/icons/XMark.svelte';
  19. export let onSubmit: Function = () => {};
  20. export let onDelete: Function = () => {};
  21. export let show = false;
  22. export let edit = false;
  23. export let direct = false;
  24. export let connection = null;
  25. let url = '';
  26. let path = 'openapi.json';
  27. let auth_type = 'bearer';
  28. let key = '';
  29. let accessControl = {};
  30. let id = '';
  31. let name = '';
  32. let description = '';
  33. let enable = true;
  34. let loading = false;
  35. const verifyHandler = async () => {
  36. if (url === '') {
  37. toast.error($i18n.t('Please enter a valid URL'));
  38. return;
  39. }
  40. if (path === '') {
  41. toast.error($i18n.t('Please enter a valid path'));
  42. return;
  43. }
  44. if (direct) {
  45. const res = await getToolServerData(
  46. auth_type === 'bearer' ? key : localStorage.token,
  47. path.includes('://') ? path : `${url}${path.startsWith('/') ? '' : '/'}${path}`
  48. ).catch((err) => {
  49. toast.error($i18n.t('Connection failed'));
  50. });
  51. if (res) {
  52. toast.success($i18n.t('Connection successful'));
  53. console.debug('Connection successful', res);
  54. }
  55. } else {
  56. const res = await verifyToolServerConnection(localStorage.token, {
  57. url,
  58. path,
  59. auth_type,
  60. key,
  61. config: {
  62. enable: enable,
  63. access_control: accessControl
  64. },
  65. info: {
  66. id,
  67. name,
  68. description
  69. }
  70. }).catch((err) => {
  71. toast.error($i18n.t('Connection failed'));
  72. });
  73. if (res) {
  74. toast.success($i18n.t('Connection successful'));
  75. console.debug('Connection successful', res);
  76. }
  77. }
  78. };
  79. const submitHandler = async () => {
  80. loading = true;
  81. // remove trailing slash from url
  82. url = url.replace(/\/$/, '');
  83. const connection = {
  84. url,
  85. path,
  86. auth_type,
  87. key,
  88. config: {
  89. enable: enable,
  90. access_control: accessControl
  91. },
  92. info: {
  93. id: id,
  94. name: name,
  95. description: description
  96. }
  97. };
  98. await onSubmit(connection);
  99. loading = false;
  100. show = false;
  101. url = '';
  102. path = 'openapi.json';
  103. key = '';
  104. auth_type = 'bearer';
  105. id = '';
  106. name = '';
  107. description = '';
  108. enable = true;
  109. accessControl = null;
  110. };
  111. const init = () => {
  112. if (connection) {
  113. url = connection.url;
  114. path = connection?.path ?? 'openapi.json';
  115. auth_type = connection?.auth_type ?? 'bearer';
  116. key = connection?.key ?? '';
  117. id = connection.info?.id ?? '';
  118. name = connection.info?.name ?? '';
  119. description = connection.info?.description ?? '';
  120. enable = connection.config?.enable ?? true;
  121. accessControl = connection.config?.access_control ?? null;
  122. }
  123. };
  124. $: if (show) {
  125. init();
  126. }
  127. onMount(() => {
  128. init();
  129. });
  130. </script>
  131. <Modal size="sm" bind:show>
  132. <div>
  133. <div class=" flex justify-between dark:text-gray-100 px-5 pt-4 pb-2">
  134. <h1 class=" text-lg font-medium self-center font-primary">
  135. {#if edit}
  136. {$i18n.t('Edit Connection')}
  137. {:else}
  138. {$i18n.t('Add Connection')}
  139. {/if}
  140. </h1>
  141. <button
  142. class="self-center"
  143. aria-label={$i18n.t('Close Configure Connection Modal')}
  144. on:click={() => {
  145. show = false;
  146. }}
  147. >
  148. <XMark className={'size-5'} />
  149. </button>
  150. </div>
  151. <div class="flex flex-col md:flex-row w-full px-4 pb-4 md:space-x-4 dark:text-gray-200">
  152. <div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
  153. <form
  154. class="flex flex-col w-full"
  155. on:submit={(e) => {
  156. e.preventDefault();
  157. submitHandler();
  158. }}
  159. >
  160. <div class="px-1">
  161. <div class="flex gap-2">
  162. <div class="flex flex-col w-full">
  163. <div class="flex justify-between mb-0.5">
  164. <label
  165. for="api-base-url"
  166. class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
  167. >{$i18n.t('URL')}</label
  168. >
  169. </div>
  170. <div class="flex flex-1 items-center">
  171. <input
  172. id="api-base-url"
  173. class={`w-full flex-1 text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
  174. type="text"
  175. bind:value={url}
  176. placeholder={$i18n.t('API Base URL')}
  177. autocomplete="off"
  178. required
  179. />
  180. <Tooltip
  181. content={$i18n.t('Verify Connection')}
  182. className="shrink-0 flex items-center mr-1"
  183. >
  184. <button
  185. class="self-center p-1 bg-transparent hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 rounded-lg transition"
  186. on:click={() => {
  187. verifyHandler();
  188. }}
  189. aria-label={$i18n.t('Verify Connection')}
  190. type="button"
  191. >
  192. <svg
  193. xmlns="http://www.w3.org/2000/svg"
  194. viewBox="0 0 20 20"
  195. fill="currentColor"
  196. class="w-4 h-4"
  197. aria-hidden="true"
  198. >
  199. <path
  200. fill-rule="evenodd"
  201. d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
  202. clip-rule="evenodd"
  203. />
  204. </svg>
  205. </button>
  206. </Tooltip>
  207. <Tooltip content={enable ? $i18n.t('Enabled') : $i18n.t('Disabled')}>
  208. <Switch bind:state={enable} />
  209. </Tooltip>
  210. </div>
  211. <div class="flex-1 flex items-center">
  212. <label for="url-or-path" class="sr-only"
  213. >{$i18n.t('openapi.json URL or Path')}</label
  214. >
  215. <input
  216. class={`w-full text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
  217. type="text"
  218. id="url-or-path"
  219. bind:value={path}
  220. placeholder={$i18n.t('openapi.json URL or Path')}
  221. autocomplete="off"
  222. required
  223. />
  224. </div>
  225. </div>
  226. </div>
  227. <div
  228. class={`text-xs mt-1 ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
  229. >
  230. {$i18n.t(`WebUI will make requests to "{{url}}"`, {
  231. url: path.includes('://') ? path : `${url}${path.startsWith('/') ? '' : '/'}${path}`
  232. })}
  233. </div>
  234. <div class="flex gap-2 mt-2">
  235. <div class="flex flex-col w-full">
  236. <label
  237. for="select-bearer-or-session"
  238. class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
  239. >{$i18n.t('Auth')}</label
  240. >
  241. <div class="flex gap-2">
  242. <div class="flex-shrink-0 self-start">
  243. <select
  244. id="select-bearer-or-session"
  245. class={`w-full text-sm bg-transparent pr-5 ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
  246. bind:value={auth_type}
  247. >
  248. <option value="none">{$i18n.t('None')}</option>
  249. <option value="bearer">{$i18n.t('Bearer')}</option>
  250. <option value="session">{$i18n.t('Session')}</option>
  251. {#if !direct}
  252. <option value="oauth">{$i18n.t('OAuth')}</option>
  253. {/if}
  254. </select>
  255. </div>
  256. <div class="flex flex-1 items-center">
  257. {#if auth_type === 'bearer'}
  258. <SensitiveInput
  259. bind:value={key}
  260. placeholder={$i18n.t('API Key')}
  261. required={false}
  262. />
  263. {:else if auth_type === 'none'}
  264. <div
  265. class={`text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
  266. >
  267. {$i18n.t('No authentication')}
  268. </div>
  269. {:else if auth_type === 'session'}
  270. <div
  271. class={`text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
  272. >
  273. {$i18n.t('Forwards system user session credentials to authenticate')}
  274. </div>
  275. {:else if auth_type === 'oauth'}
  276. <div
  277. class={`text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
  278. >
  279. {$i18n.t('Forwards system user OAuth access token to authenticate')}
  280. </div>
  281. {/if}
  282. </div>
  283. </div>
  284. </div>
  285. </div>
  286. {#if !direct}
  287. <hr class=" border-gray-100 dark:border-gray-700/10 my-2.5 w-full" />
  288. <div class="flex gap-2">
  289. <div class="flex flex-col w-full">
  290. <label
  291. for="enter-id"
  292. class={`mb-0.5 text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
  293. >{$i18n.t('ID')}
  294. <span class="text-xs text-gray-200 dark:text-gray-800 ml-0.5"
  295. >{$i18n.t('Optional')}</span
  296. >
  297. </label>
  298. <div class="flex-1">
  299. <input
  300. id="enter-id"
  301. class={`w-full text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
  302. type="text"
  303. bind:value={id}
  304. placeholder={$i18n.t('Enter ID')}
  305. autocomplete="off"
  306. />
  307. </div>
  308. </div>
  309. </div>
  310. <div class="flex gap-2 mt-2">
  311. <div class="flex flex-col w-full">
  312. <label
  313. for="enter-name"
  314. class={`mb-0.5 text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
  315. >{$i18n.t('Name')}
  316. </label>
  317. <div class="flex-1">
  318. <input
  319. id="enter-name"
  320. class={`w-full text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
  321. type="text"
  322. bind:value={name}
  323. placeholder={$i18n.t('Enter name')}
  324. autocomplete="off"
  325. required
  326. />
  327. </div>
  328. </div>
  329. </div>
  330. <div class="flex flex-col w-full mt-2">
  331. <label
  332. for="description"
  333. class={`mb-1 text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100 placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700 text-gray-500'}`}
  334. >{$i18n.t('Description')}</label
  335. >
  336. <div class="flex-1">
  337. <input
  338. id="description"
  339. class={`w-full text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
  340. type="text"
  341. bind:value={description}
  342. placeholder={$i18n.t('Enter description')}
  343. autocomplete="off"
  344. />
  345. </div>
  346. </div>
  347. <hr class=" border-gray-100 dark:border-gray-700/10 my-2.5 w-full" />
  348. <div class="my-2 -mx-2">
  349. <div class="px-3 py-2 bg-gray-50 dark:bg-gray-950 rounded-lg">
  350. <AccessControl bind:accessControl />
  351. </div>
  352. </div>
  353. {/if}
  354. </div>
  355. <div class="flex justify-end pt-3 text-sm font-medium gap-1.5">
  356. {#if edit}
  357. <button
  358. class="px-3.5 py-1.5 text-sm font-medium dark:bg-black dark:hover:bg-gray-900 dark:text-white bg-white text-black hover:bg-gray-100 transition rounded-full flex flex-row space-x-1 items-center"
  359. type="button"
  360. on:click={() => {
  361. onDelete();
  362. show = false;
  363. }}
  364. >
  365. {$i18n.t('Delete')}
  366. </button>
  367. {/if}
  368. <button
  369. class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full flex flex-row space-x-1 items-center {loading
  370. ? ' cursor-not-allowed'
  371. : ''}"
  372. type="submit"
  373. disabled={loading}
  374. >
  375. {$i18n.t('Save')}
  376. {#if loading}
  377. <div class="ml-2 self-center">
  378. <Spinner />
  379. </div>
  380. {/if}
  381. </button>
  382. </div>
  383. </form>
  384. </div>
  385. </div>
  386. </div>
  387. </Modal>