feat(domain): improve domain form UX
- Make name field optional (defaults to domain if empty) - Simplify domain placeholder and error messages - Add test connection button for credential validation before save
This commit is contained in:
@@ -24,6 +24,7 @@ import type { Result } from "@renderer/types/ipc";
|
|||||||
import type {
|
import type {
|
||||||
CreateDomainParams,
|
CreateDomainParams,
|
||||||
UpdateDomainParams,
|
UpdateDomainParams,
|
||||||
|
TestDomainConnectionParams,
|
||||||
GetSpacesParams,
|
GetSpacesParams,
|
||||||
GetAppsParams,
|
GetAppsParams,
|
||||||
GetAppDetailParams,
|
GetAppDetailParams,
|
||||||
@@ -206,6 +207,37 @@ function registerTestConnection(): void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test domain connection with temporary credentials
|
||||||
|
*/
|
||||||
|
function registerTestDomainConnection(): void {
|
||||||
|
handleWithParams<TestDomainConnectionParams, boolean>(
|
||||||
|
"testDomainConnection",
|
||||||
|
async (params) => {
|
||||||
|
const tempDomain: DomainWithPassword = {
|
||||||
|
id: "temp",
|
||||||
|
name: "temp",
|
||||||
|
domain: params.domain,
|
||||||
|
username: params.username,
|
||||||
|
password: params.password || "",
|
||||||
|
authType: params.authType,
|
||||||
|
apiToken: params.apiToken,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = new KintoneClient(tempDomain);
|
||||||
|
const result = await client.testConnection();
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || "Connection failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== Browse IPC Handlers ====================
|
// ==================== Browse IPC Handlers ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -521,6 +553,7 @@ export function registerIpcHandlers(): void {
|
|||||||
registerUpdateDomain();
|
registerUpdateDomain();
|
||||||
registerDeleteDomain();
|
registerDeleteDomain();
|
||||||
registerTestConnection();
|
registerTestConnection();
|
||||||
|
registerTestDomainConnection();
|
||||||
|
|
||||||
// Browse
|
// Browse
|
||||||
registerGetSpaces();
|
registerGetSpaces();
|
||||||
|
|||||||
2
src/preload/index.d.ts
vendored
2
src/preload/index.d.ts
vendored
@@ -3,6 +3,7 @@ import type {
|
|||||||
Result,
|
Result,
|
||||||
CreateDomainParams,
|
CreateDomainParams,
|
||||||
UpdateDomainParams,
|
UpdateDomainParams,
|
||||||
|
TestDomainConnectionParams,
|
||||||
GetSpacesParams,
|
GetSpacesParams,
|
||||||
GetAppsParams,
|
GetAppsParams,
|
||||||
GetAppDetailParams,
|
GetAppDetailParams,
|
||||||
@@ -40,6 +41,7 @@ export interface ElectronAPI {
|
|||||||
updateDomain: (params: UpdateDomainParams) => Promise<Result<Domain>>;
|
updateDomain: (params: UpdateDomainParams) => Promise<Result<Domain>>;
|
||||||
deleteDomain: (id: string) => Promise<Result<void>>;
|
deleteDomain: (id: string) => Promise<Result<void>>;
|
||||||
testConnection: (id: string) => Promise<Result<DomainWithStatus>>;
|
testConnection: (id: string) => Promise<Result<DomainWithStatus>>;
|
||||||
|
testDomainConnection: (params: TestDomainConnectionParams) => Promise<Result<boolean>>;
|
||||||
|
|
||||||
// ==================== Browse ====================
|
// ==================== Browse ====================
|
||||||
getSpaces: (params: GetSpacesParams) => Promise<Result<KintoneSpace[]>>;
|
getSpaces: (params: GetSpacesParams) => Promise<Result<KintoneSpace[]>>;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const api: ElectronAPI = {
|
|||||||
updateDomain: (params) => ipcRenderer.invoke("updateDomain", params),
|
updateDomain: (params) => ipcRenderer.invoke("updateDomain", params),
|
||||||
deleteDomain: (id) => ipcRenderer.invoke("deleteDomain", id),
|
deleteDomain: (id) => ipcRenderer.invoke("deleteDomain", id),
|
||||||
testConnection: (id) => ipcRenderer.invoke("testConnection", id),
|
testConnection: (id) => ipcRenderer.invoke("testConnection", id),
|
||||||
|
testDomainConnection: (params) => ipcRenderer.invoke("testDomainConnection", params),
|
||||||
|
|
||||||
// ==================== Browse ====================
|
// ==================== Browse ====================
|
||||||
getSpaces: (params) => ipcRenderer.invoke("getSpaces", params),
|
getSpaces: (params) => ipcRenderer.invoke("getSpaces", params),
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ import { DeployDialog } from "@renderer/components/DeployDialog";
|
|||||||
const { Header, Content, Sider } = Layout;
|
const { Header, Content, Sider } = Layout;
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
|
|
||||||
|
// Domain section heights
|
||||||
|
const DOMAIN_SECTION_COLLAPSED = 56; // Just show current domain
|
||||||
|
const DOMAIN_SECTION_EXPANDED = 240; // Show full list
|
||||||
|
|
||||||
const useStyles = createStyles(({ token, css }) => ({
|
const useStyles = createStyles(({ token, css }) => ({
|
||||||
layout: css`
|
layout: css`
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
@@ -102,13 +106,13 @@ const useStyles = createStyles(({ token, css }) => ({
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
`,
|
`,
|
||||||
domainSection: css`
|
domainSection: css`
|
||||||
height: 200px;
|
|
||||||
border-bottom: 1px solid ${token.colorBorderSecondary};
|
border-bottom: 1px solid ${token.colorBorderSecondary};
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
transition: height 0.2s ease-in-out;
|
||||||
`,
|
`,
|
||||||
spaceSection: css`
|
spaceSection: css`
|
||||||
height: calc(100% - 200px);
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
transition: height 0.2s ease-in-out;
|
||||||
`,
|
`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -120,6 +124,11 @@ const App: React.FC = () => {
|
|||||||
|
|
||||||
const { currentDomain } = useDomainStore();
|
const { currentDomain } = useDomainStore();
|
||||||
const [deployDialogOpen, setDeployDialogOpen] = React.useState(false);
|
const [deployDialogOpen, setDeployDialogOpen] = React.useState(false);
|
||||||
|
const [domainExpanded, setDomainExpanded] = React.useState(true);
|
||||||
|
|
||||||
|
const domainSectionHeight = domainExpanded
|
||||||
|
? DOMAIN_SECTION_EXPANDED
|
||||||
|
: DOMAIN_SECTION_COLLAPSED;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfigProvider locale={zhCN}>
|
<ConfigProvider locale={zhCN}>
|
||||||
@@ -132,10 +141,19 @@ const App: React.FC = () => {
|
|||||||
<span className={styles.logoText}>Kintone Manager</span>
|
<span className={styles.logoText}>Kintone Manager</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.siderContent}>
|
<div className={styles.siderContent}>
|
||||||
<div className={styles.domainSection}>
|
<div
|
||||||
<DomainManager />
|
className={styles.domainSection}
|
||||||
|
style={{ height: domainSectionHeight }}
|
||||||
|
>
|
||||||
|
<DomainManager
|
||||||
|
collapsed={!domainExpanded}
|
||||||
|
onToggleCollapse={() => setDomainExpanded(!domainExpanded)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.spaceSection}>
|
<div
|
||||||
|
className={styles.spaceSection}
|
||||||
|
style={{ height: `calc(100% - ${domainSectionHeight}px)` }}
|
||||||
|
>
|
||||||
<SpaceTree />
|
<SpaceTree />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -64,11 +64,23 @@ const DomainForm: React.FC<DomainFormProps> = ({ open, onClose, domainId }) => {
|
|||||||
try {
|
try {
|
||||||
const values = await form.validateFields();
|
const values = await form.validateFields();
|
||||||
|
|
||||||
|
// Process domain: remove protocol prefix and trailing slashes
|
||||||
|
let processedDomain = values.domain.trim();
|
||||||
|
if (processedDomain.startsWith("https://")) {
|
||||||
|
processedDomain = processedDomain.slice(8);
|
||||||
|
} else if (processedDomain.startsWith("http://")) {
|
||||||
|
processedDomain = processedDomain.slice(7);
|
||||||
|
}
|
||||||
|
processedDomain = processedDomain.replace(/\/+$/, "");
|
||||||
|
|
||||||
|
// Use domain as name if name is empty
|
||||||
|
const name = values.name?.trim() || processedDomain;
|
||||||
|
|
||||||
if (isEdit && editingDomain) {
|
if (isEdit && editingDomain) {
|
||||||
const params: UpdateDomainParams = {
|
const params: UpdateDomainParams = {
|
||||||
id: domainId,
|
id: domainId,
|
||||||
name: values.name,
|
name,
|
||||||
domain: values.domain,
|
domain: processedDomain,
|
||||||
username: values.username,
|
username: values.username,
|
||||||
authType: values.authType,
|
authType: values.authType,
|
||||||
apiToken:
|
apiToken:
|
||||||
@@ -89,8 +101,8 @@ const DomainForm: React.FC<DomainFormProps> = ({ open, onClose, domainId }) => {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const params: CreateDomainParams = {
|
const params: CreateDomainParams = {
|
||||||
name: values.name,
|
name,
|
||||||
domain: values.domain,
|
domain: processedDomain,
|
||||||
username: values.username,
|
username: values.username,
|
||||||
password: values.password,
|
password: values.password,
|
||||||
authType: values.authType,
|
authType: values.authType,
|
||||||
@@ -112,6 +124,48 @@ const DomainForm: React.FC<DomainFormProps> = ({ open, onClose, domainId }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const authType = Form.useWatch("authType", form);
|
const authType = Form.useWatch("authType", form);
|
||||||
|
const [testing, setTesting] = React.useState(false);
|
||||||
|
|
||||||
|
// Test connection with current form values
|
||||||
|
const handleTestConnection = async () => {
|
||||||
|
try {
|
||||||
|
const values = await form.validateFields([
|
||||||
|
"domain",
|
||||||
|
"username",
|
||||||
|
"authType",
|
||||||
|
"password",
|
||||||
|
"apiToken",
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Process domain
|
||||||
|
let processedDomain = values.domain.trim();
|
||||||
|
if (processedDomain.startsWith("https://")) {
|
||||||
|
processedDomain = processedDomain.slice(8);
|
||||||
|
} else if (processedDomain.startsWith("http://")) {
|
||||||
|
processedDomain = processedDomain.slice(7);
|
||||||
|
}
|
||||||
|
processedDomain = processedDomain.replace(/\/+$/, "");
|
||||||
|
|
||||||
|
setTesting(true);
|
||||||
|
const result = await window.api.testDomainConnection({
|
||||||
|
domain: processedDomain,
|
||||||
|
username: values.username,
|
||||||
|
authType: values.authType,
|
||||||
|
password: values.authType === "password" ? values.password : undefined,
|
||||||
|
apiToken: values.authType === "api_token" ? values.apiToken : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
message.success("连接成功");
|
||||||
|
} else {
|
||||||
|
message.error(result.error || "连接失败");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Test connection failed:", error);
|
||||||
|
} finally {
|
||||||
|
setTesting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -128,13 +182,8 @@ const DomainForm: React.FC<DomainFormProps> = ({ open, onClose, domainId }) => {
|
|||||||
className={styles.form}
|
className={styles.form}
|
||||||
initialValues={{ authType: "password" }}
|
initialValues={{ authType: "password" }}
|
||||||
>
|
>
|
||||||
<Form.Item
|
<Form.Item name="name" label="名称" style={{ marginTop: 8 }}>
|
||||||
name="name"
|
<Input placeholder="可选,留空则使用域名" />
|
||||||
label="名称"
|
|
||||||
rules={[{ required: true, message: "请输入名称" }]}
|
|
||||||
>
|
|
||||||
<Input placeholder="例如:生产环境" />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="domain"
|
name="domain"
|
||||||
@@ -142,23 +191,35 @@ const DomainForm: React.FC<DomainFormProps> = ({ open, onClose, domainId }) => {
|
|||||||
rules={[
|
rules={[
|
||||||
{ required: true, message: "请输入域名" },
|
{ required: true, message: "请输入域名" },
|
||||||
{
|
{
|
||||||
pattern: /^[\w.-]+$/,
|
validator: (_, value) => {
|
||||||
message: "请输入有效的域名,例如:company.kintone.com",
|
if (!value) return Promise.resolve();
|
||||||
|
// Allow https:// or http:// prefix
|
||||||
|
let domain = value.trim();
|
||||||
|
if (domain.startsWith("https://")) {
|
||||||
|
domain = domain.slice(8);
|
||||||
|
} else if (domain.startsWith("http://")) {
|
||||||
|
domain = domain.slice(7);
|
||||||
|
}
|
||||||
|
// Remove trailing slashes
|
||||||
|
domain = domain.replace(/\/+$/, "");
|
||||||
|
// Validate domain format
|
||||||
|
if (/^[\w.-]+$/.test(domain)) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error("请输入有效的域名"));
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input placeholder="例如:company.kintone.com" />
|
<Input placeholder="https://company.kintone.com" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="username"
|
name="username"
|
||||||
label="用户名"
|
label="用户名"
|
||||||
rules={[
|
rules={[{ required: true, message: "请输入用户名" }]}
|
||||||
{ required: true, message: "请输入用户名" },
|
|
||||||
{ type: "email", message: "请输入有效的邮箱地址" },
|
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
<Input placeholder="登录 Kintone 的邮箱地址" />
|
<Input placeholder="登录 Kintone 的用户名" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@@ -197,6 +258,9 @@ const DomainForm: React.FC<DomainFormProps> = ({ open, onClose, domainId }) => {
|
|||||||
<Form.Item style={{ marginTop: 24, marginBottom: 0 }}>
|
<Form.Item style={{ marginTop: 24, marginBottom: 0 }}>
|
||||||
<Space style={{ width: "100%", justifyContent: "flex-end" }}>
|
<Space style={{ width: "100%", justifyContent: "flex-end" }}>
|
||||||
<Button onClick={onClose}>取消</Button>
|
<Button onClick={onClose}>取消</Button>
|
||||||
|
<Button onClick={handleTestConnection} loading={testing}>
|
||||||
|
测试连接
|
||||||
|
</Button>
|
||||||
<Button type="primary" onClick={handleSubmit} loading={loading}>
|
<Button type="primary" onClick={handleSubmit} loading={loading}>
|
||||||
{isEdit ? "更新" : "创建"}
|
{isEdit ? "更新" : "创建"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
/**
|
/**
|
||||||
* DomainManager Component
|
* DomainManager Component
|
||||||
* Main container for domain management
|
* Main container for domain management
|
||||||
|
* Supports collapsed/expanded view
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Button, Empty, Spin } from "antd";
|
import { Button, Empty, Spin, Tooltip, Avatar, Space } from "antd";
|
||||||
import { PlusOutlined, ReloadOutlined } from "@ant-design/icons";
|
import {
|
||||||
|
PlusOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
CloudServerOutlined,
|
||||||
|
UpOutlined,
|
||||||
|
DownOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
import { createStyles } from "antd-style";
|
import { createStyles } from "antd-style";
|
||||||
import { useDomainStore } from "@renderer/stores";
|
import { useDomainStore } from "@renderer/stores";
|
||||||
import DomainList from "./DomainList";
|
import DomainList from "./DomainList";
|
||||||
@@ -19,6 +26,12 @@ const useStyles = createStyles(({ token, css }) => ({
|
|||||||
padding: ${token.paddingLG}px;
|
padding: ${token.paddingLG}px;
|
||||||
background: ${token.colorBgContainer};
|
background: ${token.colorBgContainer};
|
||||||
`,
|
`,
|
||||||
|
collapsedContainer: css`
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: ${token.colorBgContainer};
|
||||||
|
`,
|
||||||
header: css`
|
header: css`
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -47,11 +60,61 @@ const useStyles = createStyles(({ token, css }) => ({
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
height: 200px;
|
height: 200px;
|
||||||
`,
|
`,
|
||||||
|
collapsedHeader: css`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: ${token.paddingSM}px ${token.paddingMD}px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: ${token.colorBgTextHover};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
collapsedInfo: css`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: ${token.paddingSM}px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
`,
|
||||||
|
collapsedName: css`
|
||||||
|
font-weight: ${token.fontWeightStrong};
|
||||||
|
font-size: ${token.fontSize}px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
`,
|
||||||
|
collapsedUrl: css`
|
||||||
|
color: ${token.colorTextSecondary};
|
||||||
|
font-size: ${token.fontSizeSM}px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
`,
|
||||||
|
collapsedActions: css`
|
||||||
|
display: flex;
|
||||||
|
gap: ${token.paddingXS}px;
|
||||||
|
`,
|
||||||
|
noDomainText: css`
|
||||||
|
color: ${token.colorTextSecondary};
|
||||||
|
font-size: ${token.fontSizeSM}px;
|
||||||
|
padding: ${token.paddingSM}px ${token.paddingMD}px;
|
||||||
|
`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const DomainManager: React.FC = () => {
|
interface DomainManagerProps {
|
||||||
|
collapsed?: boolean;
|
||||||
|
onToggleCollapse?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DomainManager: React.FC<DomainManagerProps> = ({
|
||||||
|
collapsed = false,
|
||||||
|
onToggleCollapse,
|
||||||
|
}) => {
|
||||||
const { styles } = useStyles();
|
const { styles } = useStyles();
|
||||||
const { domains, loading, loadDomains } = useDomainStore();
|
const { domains, loading, loadDomains, currentDomain } = useDomainStore();
|
||||||
const [formOpen, setFormOpen] = React.useState(false);
|
const [formOpen, setFormOpen] = React.useState(false);
|
||||||
const [editingDomain, setEditingDomain] = React.useState<string | null>(null);
|
const [editingDomain, setEditingDomain] = React.useState<string | null>(null);
|
||||||
|
|
||||||
@@ -78,20 +141,83 @@ const DomainManager: React.FC = () => {
|
|||||||
loadDomains();
|
loadDomains();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Collapsed view - show current domain only
|
||||||
|
if (collapsed) {
|
||||||
|
return (
|
||||||
|
<div className={styles.collapsedContainer}>
|
||||||
|
<div className={styles.collapsedHeader} onClick={onToggleCollapse}>
|
||||||
|
<div className={styles.collapsedInfo}>
|
||||||
|
<Avatar
|
||||||
|
icon={<CloudServerOutlined />}
|
||||||
|
size="small"
|
||||||
|
style={{
|
||||||
|
backgroundColor: currentDomain ? "#1890ff" : "#d9d9d9",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
{currentDomain ? (
|
||||||
|
<>
|
||||||
|
<div className={styles.collapsedName}>
|
||||||
|
{currentDomain.name}
|
||||||
|
</div>
|
||||||
|
<div className={styles.collapsedUrl}>
|
||||||
|
{currentDomain.domain}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className={styles.collapsedName}>未选择 Domain</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Space>
|
||||||
|
<Tooltip title="添加 Domain">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleAdd();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="展开">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<DownOutlined />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DomainForm
|
||||||
|
open={formOpen}
|
||||||
|
onClose={handleCloseForm}
|
||||||
|
domainId={editingDomain}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expanded view - full list
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<h2 className={styles.title}>Domain 管理</h2>
|
<h2 className={styles.title}>Domain 管理</h2>
|
||||||
<div className={styles.actions}>
|
<div className={styles.actions}>
|
||||||
|
<Tooltip title="收起">
|
||||||
|
<Button icon={<UpOutlined />} onClick={onToggleCollapse} />
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="刷新">
|
||||||
<Button
|
<Button
|
||||||
icon={<ReloadOutlined />}
|
icon={<ReloadOutlined />}
|
||||||
onClick={handleRefresh}
|
onClick={handleRefresh}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
>
|
/>
|
||||||
刷新
|
</Tooltip>
|
||||||
</Button>
|
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
|
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
|
||||||
添加 Domain
|
添加
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -38,6 +38,14 @@ export interface UpdateDomainParams {
|
|||||||
apiToken?: string;
|
apiToken?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TestDomainConnectionParams {
|
||||||
|
domain: string;
|
||||||
|
username: string;
|
||||||
|
authType: "password" | "api_token";
|
||||||
|
password?: string;
|
||||||
|
apiToken?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== Browse IPC Types ====================
|
// ==================== Browse IPC Types ====================
|
||||||
|
|
||||||
export interface GetSpacesParams {
|
export interface GetSpacesParams {
|
||||||
@@ -126,6 +134,7 @@ export interface ElectronAPI {
|
|||||||
updateDomain: (params: UpdateDomainParams) => Promise<Result<Domain>>;
|
updateDomain: (params: UpdateDomainParams) => Promise<Result<Domain>>;
|
||||||
deleteDomain: (id: string) => Promise<Result<void>>;
|
deleteDomain: (id: string) => Promise<Result<void>>;
|
||||||
testConnection: (id: string) => Promise<Result<DomainWithStatus>>;
|
testConnection: (id: string) => Promise<Result<DomainWithStatus>>;
|
||||||
|
testDomainConnection: (params: TestDomainConnectionParams) => Promise<Result<boolean>>;
|
||||||
|
|
||||||
// Browse
|
// Browse
|
||||||
getSpaces: (params: GetSpacesParams) => Promise<Result<KintoneSpace[]>>;
|
getSpaces: (params: GetSpacesParams) => Promise<Result<KintoneSpace[]>>;
|
||||||
|
|||||||
Reference in New Issue
Block a user