Add advanced options to servers

This commit is contained in:
Alejandro Celaya 2025-04-20 11:12:43 +02:00
parent 4947e0490a
commit e997d11c2c
6 changed files with 52 additions and 22 deletions

14
package-lock.json generated
View File

@ -16,7 +16,7 @@
"@json2csv/plainjs": "^7.0.6", "@json2csv/plainjs": "^7.0.6",
"@reduxjs/toolkit": "^2.7.0", "@reduxjs/toolkit": "^2.7.0",
"@shlinkio/data-manipulation": "^1.0.3", "@shlinkio/data-manipulation": "^1.0.3",
"@shlinkio/shlink-frontend-kit": "^0.8.10", "@shlinkio/shlink-frontend-kit": "^0.8.12",
"@shlinkio/shlink-js-sdk": "^2.1.0", "@shlinkio/shlink-js-sdk": "^2.1.0",
"@shlinkio/shlink-web-component": "^0.13.3", "@shlinkio/shlink-web-component": "^0.13.3",
"bootstrap": "5.2.3", "bootstrap": "5.2.3",
@ -3438,9 +3438,9 @@
} }
}, },
"node_modules/@shlinkio/shlink-frontend-kit": { "node_modules/@shlinkio/shlink-frontend-kit": {
"version": "0.8.10", "version": "0.8.12",
"resolved": "https://registry.npmjs.org/@shlinkio/shlink-frontend-kit/-/shlink-frontend-kit-0.8.10.tgz", "resolved": "https://registry.npmjs.org/@shlinkio/shlink-frontend-kit/-/shlink-frontend-kit-0.8.12.tgz",
"integrity": "sha512-cB5qyZBCWEwLzEf3XK6ih/32x8i4ER4Tn6WNqIROhcr6Myjot0gvAfNStoXbEeYjJSw2+5wRFSccbAh3w5RxJA==", "integrity": "sha512-J3t0HnvOaZDLSZ1zjbAn9l025GNTy7XvcKEV5+t8iYirf6THyGCK7JDoY1CfgRWfjiWBCFA+WmzrK92a2PqcAA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"clsx": "^2.1.1" "clsx": "^2.1.1"
@ -13699,9 +13699,9 @@
"requires": {} "requires": {}
}, },
"@shlinkio/shlink-frontend-kit": { "@shlinkio/shlink-frontend-kit": {
"version": "0.8.10", "version": "0.8.12",
"resolved": "https://registry.npmjs.org/@shlinkio/shlink-frontend-kit/-/shlink-frontend-kit-0.8.10.tgz", "resolved": "https://registry.npmjs.org/@shlinkio/shlink-frontend-kit/-/shlink-frontend-kit-0.8.12.tgz",
"integrity": "sha512-cB5qyZBCWEwLzEf3XK6ih/32x8i4ER4Tn6WNqIROhcr6Myjot0gvAfNStoXbEeYjJSw2+5wRFSccbAh3w5RxJA==", "integrity": "sha512-J3t0HnvOaZDLSZ1zjbAn9l025GNTy7XvcKEV5+t8iYirf6THyGCK7JDoY1CfgRWfjiWBCFA+WmzrK92a2PqcAA==",
"requires": { "requires": {
"clsx": "^2.1.1" "clsx": "^2.1.1"
} }

View File

@ -29,7 +29,7 @@
"@json2csv/plainjs": "^7.0.6", "@json2csv/plainjs": "^7.0.6",
"@reduxjs/toolkit": "^2.7.0", "@reduxjs/toolkit": "^2.7.0",
"@shlinkio/data-manipulation": "^1.0.3", "@shlinkio/data-manipulation": "^1.0.3",
"@shlinkio/shlink-frontend-kit": "^0.8.10", "@shlinkio/shlink-frontend-kit": "^0.8.12",
"@shlinkio/shlink-js-sdk": "^2.1.0", "@shlinkio/shlink-js-sdk": "^2.1.0",
"@shlinkio/shlink-web-component": "^0.13.3", "@shlinkio/shlink-web-component": "^0.13.3",
"bootstrap": "5.2.3", "bootstrap": "5.2.3",

View File

@ -4,7 +4,7 @@ import type { GetState } from '../../container/types';
import type { ServerWithId } from '../../servers/data'; import type { ServerWithId } from '../../servers/data';
import { hasServerData } from '../../servers/data'; import { hasServerData } from '../../servers/data';
const apiClients: Record<string, ShlinkApiClient> = {}; const apiClients: Map<string, ShlinkApiClient> = new Map();
const isGetState = (getStateOrSelectedServer: GetState | ServerWithId): getStateOrSelectedServer is GetState => const isGetState = (getStateOrSelectedServer: GetState | ServerWithId): getStateOrSelectedServer is GetState =>
typeof getStateOrSelectedServer === 'function'; typeof getStateOrSelectedServer === 'function';
@ -18,19 +18,22 @@ const getSelectedServerFromState = (getState: GetState): ServerWithId => {
}; };
export const buildShlinkApiClient = (httpClient: HttpClient) => (getStateOrSelectedServer: GetState | ServerWithId) => { export const buildShlinkApiClient = (httpClient: HttpClient) => (getStateOrSelectedServer: GetState | ServerWithId) => {
const { url: baseUrl, apiKey } = isGetState(getStateOrSelectedServer) const { url: baseUrl, apiKey, forwardCredentials } = isGetState(getStateOrSelectedServer)
? getSelectedServerFromState(getStateOrSelectedServer) ? getSelectedServerFromState(getStateOrSelectedServer)
: getStateOrSelectedServer; : getStateOrSelectedServer;
const serverKey = `${apiKey}_${baseUrl}`; const serverKey = `${apiKey}_${baseUrl}_${forwardCredentials ? 'forward' : 'no-forward'}`;
const existingApiClient = apiClients.get(serverKey);
const apiClient = apiClients[serverKey] ?? new ShlinkApiClient( if (existingApiClient) {
return existingApiClient;
}
const apiClient = new ShlinkApiClient(
httpClient, httpClient,
{ apiKey, baseUrl }, { apiKey, baseUrl },
// FIXME Disabling this as it's breaking existing Shlink servers as configured out of the box { requestCredentials: forwardCredentials ? 'include' : undefined },
// { requestCredentials: 'include' },
); );
apiClients[serverKey] = apiClient; apiClients.set(serverKey, apiClient);
return apiClient; return apiClient;
}; };

View File

@ -4,6 +4,7 @@ export interface ServerData {
name: string; name: string;
url: string; url: string;
apiKey: string; apiKey: string;
forwardCredentials?: boolean;
} }
export interface ServerWithId extends ServerData { export interface ServerWithId extends ServerData {

View File

@ -1,11 +1,15 @@
import { useToggle } from '@shlinkio/shlink-frontend-kit';
import { import {
Checkbox,
Details,
Label,
LabelledInput, LabelledInput,
LabelledRevealablePasswordInput, LabelledRevealablePasswordInput,
SimpleCard, SimpleCard,
} from '@shlinkio/shlink-frontend-kit/tailwind'; } from '@shlinkio/shlink-frontend-kit/tailwind';
import type { FC, PropsWithChildren, ReactNode } from 'react'; import type { FC, PropsWithChildren, ReactNode } from 'react';
import { useState } from 'react'; import { useState } from 'react';
import { handleEventPreventingDefault } from '../../utils/utils'; import { usePreventDefault } from '../../utils/utils';
import type { ServerData } from '../data'; import type { ServerData } from '../data';
type ServerFormProps = PropsWithChildren<{ type ServerFormProps = PropsWithChildren<{
@ -18,7 +22,11 @@ export const ServerForm: FC<ServerFormProps> = ({ onSubmit, initialValues, child
const [name, setName] = useState(initialValues?.name ?? ''); const [name, setName] = useState(initialValues?.name ?? '');
const [url, setUrl] = useState(initialValues?.url ?? ''); const [url, setUrl] = useState(initialValues?.url ?? '');
const [apiKey, setApiKey] = useState(initialValues?.apiKey ?? ''); const [apiKey, setApiKey] = useState(initialValues?.apiKey ?? '');
const handleSubmit = handleEventPreventingDefault(() => onSubmit({ name, url, apiKey })); const { flag: forwardCredentials, toggle: toggleForwardCredentials } = useToggle(
initialValues?.forwardCredentials ?? false,
true,
);
const handleSubmit = usePreventDefault(() => onSubmit({ name, url, apiKey, forwardCredentials }));
return ( return (
<form name="serverForm" onSubmit={handleSubmit}> <form name="serverForm" onSubmit={handleSubmit}>
@ -31,6 +39,19 @@ export const ServerForm: FC<ServerFormProps> = ({ onSubmit, initialValues, child
onChange={(e) => setApiKey(e.target.value)} onChange={(e) => setApiKey(e.target.value)}
required required
/> />
<Details summary="Advanced options">
<div className="tw:flex tw:flex-col tw:gap-1">
<Label className="tw:flex tw:items-center tw:gap-x-1.5 tw:cursor-pointer">
<Checkbox onChange={toggleForwardCredentials} checked={forwardCredentials} />
Forward credentials (like cookies) to this server on every request.
</Label>
<small className="tw:pl-5.5 tw:text-gray-600 tw:dark:text-gray-400">
<b>Important!</b> If you are not sure what this means, leave it unchecked. Enabling this option will
make all requests fail for Shlink older than v4.5.0, as it requires the server to set a more strict
value for <code className="tw:whitespace-nowrap">Access-Control-Allow-Origin</code> than <code>*</code>.
</small>
</div>
</Details>
</SimpleCard> </SimpleCard>
<div className="tw:flex tw:items-center tw:justify-end tw:gap-x-2">{children}</div> <div className="tw:flex tw:items-center tw:justify-end tw:gap-x-2">{children}</div>

View File

@ -1,6 +1,11 @@
import type { SyntheticEvent } from 'react'; import type { SyntheticEvent } from 'react';
import { useCallback } from 'react';
export const handleEventPreventingDefault = <T>(handler: () => T) => (e: SyntheticEvent) => { /**
* Wraps an event handler so that it calls e.preventDefault() before invoking the event handler
*/
export const usePreventDefault = <Event extends SyntheticEvent = SyntheticEvent>(handler: (e: Event) => void) =>
useCallback((e: Event) => {
e.preventDefault(); e.preventDefault();
handler(); handler(e);
}; }, [handler]);