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
3 changes: 2 additions & 1 deletion e2e/tests/stream_routes.show-disabled-error.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import { exec } from 'node:child_process';
import { readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { promisify } from 'node:util';

import { streamRoutesPom } from '@e2e/pom/stream_routes';
Expand All @@ -51,7 +52,7 @@ type APISIXConf = {
};

const getE2EServerDir = () => {
const currentDir = new URL('.', import.meta.url).pathname;
const currentDir = path.dirname(fileURLToPath(import.meta.url));
return path.join(currentDir, '../server');
};

Expand Down
3 changes: 2 additions & 1 deletion e2e/utils/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
import { access, readFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

import { nanoid } from 'nanoid';
import selfsigned from 'selfsigned';
Expand All @@ -25,7 +26,7 @@ type APISIXConf = {
deployment: { admin: { admin_key: { key: string }[] } };
};
export const getAPISIXConf = async () => {
const currentDir = new URL('.', import.meta.url).pathname;
const currentDir = path.dirname(fileURLToPath(import.meta.url));
const confPath = path.join(currentDir, '../server/apisix_conf.yml');
const file = await readFile(confPath, 'utf-8');
const res = parse(file) as APISIXConf;
Expand Down
10 changes: 9 additions & 1 deletion e2e/utils/ui/upstreams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,15 @@ export async function uiFillUpstreamAllFields(
await tlsSection
.getByRole('textbox', { name: 'Client Key', exact: true })
.fill(tls.key);
await tlsSection.getByRole('switch', { name: 'Verify' }).click();
const verifySwitch = tlsSection
.locator('input[name$="tls.verify"]')
.first();
if (!(await verifySwitch.isChecked())) {
await verifySwitch.evaluate((node) => {
(node as HTMLInputElement).click();
});
}
await expect(verifySwitch).toBeChecked();

// 12. Health Check settings
// Activate active health check
Expand Down
151 changes: 151 additions & 0 deletions src/components/CopyableText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { CheckOutlined, CopyOutlined } from '@ant-design/icons';
import {
type CSSProperties,
type ReactNode,
useEffect,
useId,
useRef,
useState,
} from 'react';
import { useTranslation } from 'react-i18next';

export type CopyableTextProps = {
text: string;
children?: ReactNode;
emptyPlaceholder?: ReactNode;
};

const wrapperStyle: CSSProperties = {
display: 'inline-flex',
alignItems: 'center',
gap: 4,
maxWidth: '100%',
};

const copyButtonStyle: CSSProperties = {
border: 'none',
background: 'transparent',
color: '#1677ff',
cursor: 'pointer',
padding: 0,
display: 'inline-flex',
alignItems: 'center',
lineHeight: 1,
};

const srOnlyStyle: CSSProperties = {
position: 'absolute',
width: 1,
height: 1,
padding: 0,
margin: -1,
overflow: 'hidden',
clip: 'rect(0, 0, 0, 0)',
whiteSpace: 'nowrap',
border: 0,
};

const fallbackCopy = (value: string) => {
const textarea = document.createElement('textarea');
textarea.value = value;
textarea.setAttribute('readonly', '');
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
const copied = document.execCommand('copy');
document.body.removeChild(textarea);
return copied;
};

const writeToClipboard = async (value: string) => {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(value);
return;
}

if (!fallbackCopy(value)) {
throw new Error('Failed to copy text');
}
};

export const CopyableText = (props: CopyableTextProps) => {
const { text, children, emptyPlaceholder = '-' } = props;
const { t } = useTranslation();
const [copied, setCopied] = useState(false);
const copiedResetTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const announceId = useId();

useEffect(() => {
return () => {
if (copiedResetTimer.current) {
clearTimeout(copiedResetTimer.current);
}
};
}, []);

const hasValue = text.trim().length > 0;
const displayContent = children ?? (hasValue ? text : emptyPlaceholder);

const onCopy = async () => {
if (!hasValue) {
return;
}

try {
await writeToClipboard(text);
setCopied(true);
if (copiedResetTimer.current) {
clearTimeout(copiedResetTimer.current);
}
copiedResetTimer.current = setTimeout(() => {
setCopied(false);
}, 1500);
} catch {
setCopied(false);
}
};

return (
<span style={wrapperStyle}>
<span>{displayContent}</span>
{hasValue && (
<>
<button
type="button"
onClick={onCopy}
style={copyButtonStyle}
title={copied ? t('copy_success') : t('copy')}
aria-label={`${copied ? t('copy_success') : t('copy')}: ${text}`}
aria-describedby={announceId}
>
{copied ? (
<CheckOutlined aria-hidden="true" />
) : (
<CopyOutlined aria-hidden="true" />
)}
</button>
<span id={announceId} aria-live="polite" style={srOnlyStyle}>
{copied ? t('copy_success') : ''}
</span>
</>
)}
</span>
);
};
20 changes: 13 additions & 7 deletions src/components/form-slice/FormItemPlugins/PluginCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@
* 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 { useTranslation } from 'react-i18next';

import { CopyableText } from '@/components/CopyableText';

export type PluginCardProps = {
name: string;
desc?: string;
Expand All @@ -30,12 +32,16 @@ export type PluginCardProps = {
export const PluginCard = (props: PluginCardProps) => {
const { name, desc, mode, onAdd, onEdit, onView, onDelete } = props;
const { t } = useTranslation();
const pluginName = String(name);

return (
<Card withBorder radius="md" p="md" data-testid={`plugin-${name}`}>
<Card withBorder radius="md" p="md" data-testid={`plugin-${pluginName}`}>
<Card.Section withBorder inheritPadding py="xs">
<Group justify="space-between">
<Group>
<Text fw={500}>{name}</Text>
<Text fw={500}>
<CopyableText text={pluginName} />
</Text>
</Group>
</Group>
</Card.Section>
Expand All @@ -50,7 +56,7 @@ export const PluginCard = (props: PluginCardProps) => {
size="compact-xs"
variant="light"
color="blue"
onClick={() => onAdd?.(name)}
onClick={() => onAdd?.(pluginName)}
>
{t('form.btn.add')}
</Button>
Expand All @@ -59,7 +65,7 @@ export const PluginCard = (props: PluginCardProps) => {
<Button
size="compact-xs"
variant="light"
onClick={() => onView?.(name)}
onClick={() => onView?.(pluginName)}
>
{t('form.btn.view')}
</Button>
Expand All @@ -70,15 +76,15 @@ export const PluginCard = (props: PluginCardProps) => {
size="compact-xs"
variant="light"
color="blue"
onClick={() => onEdit?.(name)}
onClick={() => onEdit?.(pluginName)}
>
{t('form.btn.edit')}
</Button>
<Button
size="compact-xs"
variant="light"
color="red"
onClick={() => onDelete?.(name)}
onClick={() => onDelete?.(pluginName)}
>
{t('form.btn.delete')}
</Button>
Expand Down
1 change: 1 addition & 0 deletions src/components/form-slice/FormPartUpstream/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export const FormSectionTLS = () => {
control={control}
name={np('tls.verify')}
label={t('form.upstreams.tls.verify')}
data-testid="upstream-tls-verify-switch"
/>
<FormSection legend={t('form.upstreams.tls.clientCertKeyPair')}>
<FormItemTextareaWithUpload
Expand Down
2 changes: 2 additions & 0 deletions src/locales/de/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"credentials": {
"singular": "Anmeldeinformation"
},
"copy": "Kopieren",
"copy_success": "Kopiert",
"form": {
"basic": {
"desc": "Beschreibung",
Expand Down
2 changes: 2 additions & 0 deletions src/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"credentials": {
"singular": "Credential"
},
"copy": "Copy",
"copy_success": "Copied",
"form": {
"basic": {
"desc": "Description",
Expand Down
2 changes: 2 additions & 0 deletions src/locales/es/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"credentials": {
"singular": "Credencial"
},
"copy": "Copiar",
"copy_success": "Copiado",
"form": {
"basic": {
"desc": "Descripción",
Expand Down
2 changes: 2 additions & 0 deletions src/locales/tr/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"credentials": {
"singular": "Credential"
},
"copy": "Kopyala",
"copy_success": "Kopyalandi",
"form": {
"basic": {
"desc": "Açıklama",
Expand Down
2 changes: 2 additions & 0 deletions src/locales/zh/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"credentials": {
"singular": "凭证"
},
"copy": "复制",
"copy_success": "复制成功",
"form": {
"basic": {
"desc": "描述",
Expand Down
4 changes: 4 additions & 0 deletions src/routes/consumer_groups/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';

import { getConsumerGroupListQueryOptions, useConsumerGroupList } from '@/apis/hooks';
import { CopyableText } from '@/components/CopyableText';
import { DeleteResourceBtn } from '@/components/page/DeleteResourceBtn';
import PageHeader from '@/components/page/PageHeader';
import { ToAddPageBtn, ToDetailPageBtn } from '@/components/page/ToAddPageBtn';
Expand All @@ -43,6 +44,9 @@ function ConsumerGroupsList() {
title: 'ID',
key: 'id',
valueType: 'text',
render: (_, record) => (
<CopyableText text={String(record.value.id ?? '')} />
),
},
{
dataIndex: ['value', 'name'],
Expand Down
4 changes: 4 additions & 0 deletions src/routes/consumers/detail.$username/credentials/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
getCredentialListQueryOptions,
useCredentialsList,
} from '@/apis/hooks';
import { CopyableText } from '@/components/CopyableText';
import { DeleteResourceBtn } from '@/components/page/DeleteResourceBtn';
import PageHeader from '@/components/page/PageHeader';
import { ToAddPageBtn, ToDetailPageBtn } from '@/components/page/ToAddPageBtn';
Expand All @@ -48,6 +49,9 @@ function CredentialsList() {
title: 'ID',
key: 'id',
valueType: 'text',
render: (_, record) => (
<CopyableText text={String(record.value.id ?? '')} />
),
},
{
dataIndex: ['value', 'desc'],
Expand Down
4 changes: 4 additions & 0 deletions src/routes/consumers/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';

import { getConsumerListQueryOptions, useConsumerList } from '@/apis/hooks';
import { CopyableText } from '@/components/CopyableText';
import { DeleteResourceBtn } from '@/components/page/DeleteResourceBtn';
import PageHeader from '@/components/page/PageHeader';
import { ToAddPageBtn, ToDetailPageBtn } from '@/components/page/ToAddPageBtn';
Expand All @@ -41,6 +42,9 @@ function ConsumersList() {
title: t('form.consumers.username'),
key: 'username',
valueType: 'text',
render: (_, record) => (
<CopyableText text={String(record.value.username ?? '')} />
),
},
{
dataIndex: ['value', 'desc'],
Expand Down
4 changes: 4 additions & 0 deletions src/routes/global_rules/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';

import { getGlobalRuleListQueryOptions, useGlobalRuleList } from '@/apis/hooks';
import { CopyableText } from '@/components/CopyableText';
import { DeleteResourceBtn } from '@/components/page/DeleteResourceBtn';
import PageHeader from '@/components/page/PageHeader';
import { ToAddPageBtn, ToDetailPageBtn } from '@/components/page/ToAddPageBtn';
Expand Down Expand Up @@ -56,6 +57,9 @@ function GlobalRulesList() {
title: 'ID',
key: 'id',
valueType: 'text',
render: (_, record) => (
<CopyableText text={String(record.value.id ?? '')} />
),
},
{
title: t('table.actions'),
Expand Down
Loading