diff --git a/src/main/ipc-handlers.ts b/src/main/ipc-handlers.ts index bd0d401..9f3ed5f 100644 --- a/src/main/ipc-handlers.ts +++ b/src/main/ipc-handlers.ts @@ -24,6 +24,7 @@ import type { Result } from "@renderer/types/ipc"; import type { CreateDomainParams, UpdateDomainParams, + TestDomainConnectionParams, GetSpacesParams, GetAppsParams, GetAppDetailParams, @@ -206,6 +207,37 @@ function registerTestConnection(): void { }); } +/** + * Test domain connection with temporary credentials + */ +function registerTestDomainConnection(): void { + handleWithParams( + "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 ==================== /** @@ -521,6 +553,7 @@ export function registerIpcHandlers(): void { registerUpdateDomain(); registerDeleteDomain(); registerTestConnection(); + registerTestDomainConnection(); // Browse registerGetSpaces(); diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index a41aa03..d411a8e 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -3,6 +3,7 @@ import type { Result, CreateDomainParams, UpdateDomainParams, + TestDomainConnectionParams, GetSpacesParams, GetAppsParams, GetAppDetailParams, @@ -40,6 +41,7 @@ export interface ElectronAPI { updateDomain: (params: UpdateDomainParams) => Promise>; deleteDomain: (id: string) => Promise>; testConnection: (id: string) => Promise>; + testDomainConnection: (params: TestDomainConnectionParams) => Promise>; // ==================== Browse ==================== getSpaces: (params: GetSpacesParams) => Promise>; diff --git a/src/preload/index.ts b/src/preload/index.ts index 30396ae..23df7e0 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -13,6 +13,7 @@ const api: ElectronAPI = { updateDomain: (params) => ipcRenderer.invoke("updateDomain", params), deleteDomain: (id) => ipcRenderer.invoke("deleteDomain", id), testConnection: (id) => ipcRenderer.invoke("testConnection", id), + testDomainConnection: (params) => ipcRenderer.invoke("testDomainConnection", params), // ==================== Browse ==================== getSpaces: (params) => ipcRenderer.invoke("getSpaces", params), diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 7b5d056..43905b2 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -34,6 +34,10 @@ import { DeployDialog } from "@renderer/components/DeployDialog"; const { Header, Content, Sider } = Layout; 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 }) => ({ layout: css` min-height: 100vh; @@ -102,13 +106,13 @@ const useStyles = createStyles(({ token, css }) => ({ overflow: hidden; `, domainSection: css` - height: 200px; border-bottom: 1px solid ${token.colorBorderSecondary}; overflow: hidden; + transition: height 0.2s ease-in-out; `, spaceSection: css` - height: calc(100% - 200px); overflow: hidden; + transition: height 0.2s ease-in-out; `, })); @@ -120,6 +124,11 @@ const App: React.FC = () => { const { currentDomain } = useDomainStore(); const [deployDialogOpen, setDeployDialogOpen] = React.useState(false); + const [domainExpanded, setDomainExpanded] = React.useState(true); + + const domainSectionHeight = domainExpanded + ? DOMAIN_SECTION_EXPANDED + : DOMAIN_SECTION_COLLAPSED; return ( @@ -132,10 +141,19 @@ const App: React.FC = () => { Kintone Manager
-
- +
+ setDomainExpanded(!domainExpanded)} + />
-
+
diff --git a/src/renderer/src/components/DomainManager/DomainForm.tsx b/src/renderer/src/components/DomainManager/DomainForm.tsx index 292696c..fe04f97 100644 --- a/src/renderer/src/components/DomainManager/DomainForm.tsx +++ b/src/renderer/src/components/DomainManager/DomainForm.tsx @@ -64,11 +64,23 @@ const DomainForm: React.FC = ({ open, onClose, domainId }) => { try { 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) { const params: UpdateDomainParams = { id: domainId, - name: values.name, - domain: values.domain, + name, + domain: processedDomain, username: values.username, authType: values.authType, apiToken: @@ -89,8 +101,8 @@ const DomainForm: React.FC = ({ open, onClose, domainId }) => { } } else { const params: CreateDomainParams = { - name: values.name, - domain: values.domain, + name, + domain: processedDomain, username: values.username, password: values.password, authType: values.authType, @@ -112,6 +124,48 @@ const DomainForm: React.FC = ({ open, onClose, domainId }) => { }; 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 ( = ({ open, onClose, domainId }) => { className={styles.form} initialValues={{ authType: "password" }} > - - - + + = ({ open, onClose, domainId }) => { rules={[ { required: true, message: "请输入域名" }, { - pattern: /^[\w.-]+$/, - message: "请输入有效的域名,例如:company.kintone.com", + validator: (_, value) => { + 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("请输入有效的域名")); + }, }, ]} > - + - + = ({ open, onClose, domainId }) => { + diff --git a/src/renderer/src/components/DomainManager/DomainManager.tsx b/src/renderer/src/components/DomainManager/DomainManager.tsx index 259704b..01b5727 100644 --- a/src/renderer/src/components/DomainManager/DomainManager.tsx +++ b/src/renderer/src/components/DomainManager/DomainManager.tsx @@ -1,11 +1,18 @@ /** * DomainManager Component * Main container for domain management + * Supports collapsed/expanded view */ import React from "react"; -import { Button, Empty, Spin } from "antd"; -import { PlusOutlined, ReloadOutlined } from "@ant-design/icons"; +import { Button, Empty, Spin, Tooltip, Avatar, Space } from "antd"; +import { + PlusOutlined, + ReloadOutlined, + CloudServerOutlined, + UpOutlined, + DownOutlined, +} from "@ant-design/icons"; import { createStyles } from "antd-style"; import { useDomainStore } from "@renderer/stores"; import DomainList from "./DomainList"; @@ -19,6 +26,12 @@ const useStyles = createStyles(({ token, css }) => ({ padding: ${token.paddingLG}px; background: ${token.colorBgContainer}; `, + collapsedContainer: css` + height: 100%; + display: flex; + flex-direction: column; + background: ${token.colorBgContainer}; + `, header: css` display: flex; justify-content: space-between; @@ -47,11 +60,61 @@ const useStyles = createStyles(({ token, css }) => ({ align-items: center; 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 = ({ + collapsed = false, + onToggleCollapse, +}) => { const { styles } = useStyles(); - const { domains, loading, loadDomains } = useDomainStore(); + const { domains, loading, loadDomains, currentDomain } = useDomainStore(); const [formOpen, setFormOpen] = React.useState(false); const [editingDomain, setEditingDomain] = React.useState(null); @@ -78,20 +141,83 @@ const DomainManager: React.FC = () => { loadDomains(); }; + // Collapsed view - show current domain only + if (collapsed) { + return ( +
+
+
+ } + size="small" + style={{ + backgroundColor: currentDomain ? "#1890ff" : "#d9d9d9", + }} + /> +
+ {currentDomain ? ( + <> +
+ {currentDomain.name} +
+
+ {currentDomain.domain} +
+ + ) : ( +
未选择 Domain
+ )} +
+
+ + +
+ + +
+ ); + } + + // Expanded view - full list return (

Domain 管理

- + +
diff --git a/src/renderer/src/types/ipc.ts b/src/renderer/src/types/ipc.ts index fd4c6ef..1c0e910 100644 --- a/src/renderer/src/types/ipc.ts +++ b/src/renderer/src/types/ipc.ts @@ -38,6 +38,14 @@ export interface UpdateDomainParams { apiToken?: string; } +export interface TestDomainConnectionParams { + domain: string; + username: string; + authType: "password" | "api_token"; + password?: string; + apiToken?: string; +} + // ==================== Browse IPC Types ==================== export interface GetSpacesParams { @@ -126,6 +134,7 @@ export interface ElectronAPI { updateDomain: (params: UpdateDomainParams) => Promise>; deleteDomain: (id: string) => Promise>; testConnection: (id: string) => Promise>; + testDomainConnection: (params: TestDomainConnectionParams) => Promise>; // Browse getSpaces: (params: GetSpacesParams) => Promise>;