Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 40 additions & 8 deletions src/apis/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,37 @@ import {
import { req } from '@/config/req';
import type { APISIXType } from '@/types/schema/apisix';

const normalizePluginMap = (
payload: unknown
): Record<string, Record<string, unknown>> => {
if (!payload || typeof payload !== 'object') {
return {};
}

const obj = payload as Record<string, unknown>;

// Some APISIX variants may wrap plugin definitions under `plugins`.
if (obj.plugins && typeof obj.plugins === 'object' && !Array.isArray(obj.plugins)) {
return obj.plugins as Record<string, Record<string, unknown>>;
}

// Some variants may return an array of one-key objects.
if (Array.isArray(payload)) {
const merged: Record<string, Record<string, unknown>> = {};
for (const item of payload) {
if (!item || typeof item !== 'object' || Array.isArray(item)) continue;
for (const [name, schemaObj] of Object.entries(item)) {
if (schemaObj && typeof schemaObj === 'object') {
merged[name] = schemaObj as Record<string, unknown>;
}
}
}
return merged;
}

return obj as Record<string, Record<string, unknown>>;
};


export type NeedPluginSchema = {
schema: APISIXType['PluginSchemaKeys'];
Expand Down Expand Up @@ -53,14 +84,15 @@ export const getPluginsListWithSchemaQueryOptions = (
params: { subsystem, all: true },
})
.then((v) => {
const data = Object.entries(v.data);
const names = [];
const originObj = normalizePluginMap(v.data);
const data = Object.entries(originObj);
const names: string[] = [];
for (const [name, config] of data) {
if (config[schema]) {
names.push(name);
}
}
return { names, originObj: v.data };
return { names, originObj };
}),
});
};
Expand All @@ -73,11 +105,11 @@ export const getPluginSchemaQueryOptions = (
queryKey: ['plugin-schema', name],
queryFn: name
? () =>
req
.get<unknown, APISIXType['RespPluginSchema']>(
`${API_PLUGINS}/${name}`
)
.then((v) => v.data)
req
.get<unknown, APISIXType['RespPluginSchema']>(
`${API_PLUGINS}/${name}`
)
.then((v) => v.data)
: skipToken,
enabled,
});
Expand Down
3 changes: 2 additions & 1 deletion src/components/Header/LanguageMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ const LangMap: Record<keyof Resources, string> = {
};

const TranslationProgress = ({ lang }: { lang: string }) => {
const percent = i18nProgress[lang].percent;
const progressData = i18nProgress[lang as keyof typeof i18nProgress];
const percent = progressData?.percent ?? 0;
if (typeof percent === 'number' && percent < 100) {
return (
<span
Expand Down
31 changes: 29 additions & 2 deletions src/components/form-slice/FormItemPlugins/PluginCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Button, Card,Group, Text } from '@mantine/core';
import { Button, Card, Group, Text } from '@mantine/core';
import { useCallbackRef } from '@mantine/hooks';
import { modals } from '@mantine/modals';
import { useTranslation } from 'react-i18next';

export type PluginCardProps = {
Expand All @@ -30,6 +32,31 @@ export type PluginCardProps = {
export const PluginCard = (props: PluginCardProps) => {
const { name, desc, mode, onAdd, onEdit, onView, onDelete } = props;
const { t } = useTranslation();

const handleDelete = useCallbackRef(() =>
modals.openConfirmModal({
centered: true,
confirmProps: { color: 'red' },
title: t('info.delete.title', { name: name }),
children: (
<Text>
{t('info.delete.content', { name: name })}
<Text
component="span"
fw={700}
mx="0.25em"
style={{ wordBreak: 'break-all' }}
>
{name}
</Text>
{t('mark.question')}
</Text>
),
labels: { confirm: t('form.btn.delete'), cancel: t('form.btn.cancel') },
onConfirm: () => onDelete?.(name),
})
);

return (
<Card withBorder radius="md" p="md" data-testid={`plugin-${name}`}>
<Card.Section withBorder inheritPadding py="xs">
Expand Down Expand Up @@ -78,7 +105,7 @@ export const PluginCard = (props: PluginCardProps) => {
size="compact-xs"
variant="light"
color="red"
onClick={() => onDelete?.(name)}
onClick={handleDelete}
>
{t('form.btn.delete')}
</Button>
Expand Down
17 changes: 13 additions & 4 deletions src/components/form-slice/FormItemPlugins/PluginCardList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { PluginCard, type PluginCardProps } from './PluginCard';

type PluginCardListSearchProps = Pick<TextInputProps, 'placeholder'> & {
search: string;
setSearch: (search: string) => void;
setSearch: (search: string | string[] | null | undefined) => void;
};
export const PluginCardListSearch = (props: PluginCardListSearchProps) => {
const { placeholder, search, setSearch } = props;
Expand Down Expand Up @@ -98,10 +98,19 @@ export type PluginCardListProps = Omit<OptionProps, 'name'> &
cols?: number;
h?: number | string;
mah?: number | string;
search: string;
search: string | string[] | null | undefined;
plugins: string[];
};

const normalizeSearch = (
search: string | string[] | null | undefined
): string => {
if (Array.isArray(search)) {
return search.filter((v): v is string => typeof v === 'string').join(' ');
}
return typeof search === 'string' ? search : '';
};

export const PluginCardList = (props: PluginCardListProps) => {
const { search = '', cols = 3, h, mah, plugins } = props;
const { mode, onAdd, onEdit, onDelete, onView } = props;
Expand All @@ -111,8 +120,8 @@ export const PluginCardList = (props: PluginCardListProps) => {
search: '',
plugins: [] as string[],
mode: 'add' as OptionProps['mode'],
setSearch(search: string) {
this.search = search.toLowerCase().trim();
setSearch(search: string | string[] | null | undefined) {
this.search = normalizeSearch(search).toLowerCase().trim();
},
setPlugins(plugins: string[]) {
this.plugins = plugins;
Expand Down
26 changes: 9 additions & 17 deletions src/routes/plugin_configs/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,23 +102,15 @@ function PluginConfigsList() {
pagination={pagination}
cardProps={{ bodyStyle: { padding: 0 } }}
toolbar={{
menu: {
type: 'inline',
items: [
{
key: 'add',
label: (
<ToAddPageBtn
key="add"
to="/plugin_configs/add"
label={t('info.add.title', {
name: t('pluginConfigs.singular'),
})}
/>
),
},
],
},
actions: [
<ToAddPageBtn
key="add"
to="/plugin_configs/add"
label={t('info.add.title', {
name: t('pluginConfigs.singular'),
})}
/>,
],
}}
/>
</AntdConfigProvider>
Expand Down
31 changes: 0 additions & 31 deletions src/utils/zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,37 +14,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { xor } from 'rambdax';
import { type RefinementCtx, z } from 'zod';
import { init } from 'zod-empty';

// ref: https://github.com/colinhacks/zod/issues/61#issuecomment-1741983149
export const zOneOf =
<
A,
K1 extends Extract<keyof A, string>,
K2 extends Extract<keyof A, string>,
R extends A &
(
| (Required<Pick<A, K1>> & { [P in K2]: undefined })
| (Required<Pick<A, K2>> & { [P in K1]: undefined })
)
>(
key1: K1,
key2: K2
): ((arg: A, ctx: RefinementCtx) => arg is R) =>
(arg, ctx): arg is R => {
if (xor(arg[key1] as boolean, arg[key2] as boolean)) {
[key1, key2].forEach((key) => {
ctx.addIssue({
path: [key],
code: z.ZodIssueCode.custom,
message: `Either '${key1}' or '${key2}' must be filled, but not both`,
});
});
return false;
}
return true;
};

export const zGetDefault = init;
2 changes: 1 addition & 1 deletion vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export default defineConfig({
semicolons: false,
}),
i18nProgress({
langs: ['en', 'es', 'de', 'zh'],
langs: ['en', 'es', 'de', 'zh', 'tr'],
baseLang: 'en',
getTranslationDir: (lang) => `./src/locales/${lang}`,
}),
Expand Down