feat(connection-modal): 重构连接弹窗 Studio 交互

This commit is contained in:
lsx
2026-07-27 17:02:55 +08:00
parent 2d1034e77e
commit e79da2e1e6
11 changed files with 922 additions and 248 deletions

View File

@@ -1 +1 @@
83b4b7d166ed9172beba2d6236382c0b
84c1262b11d52116d262ce60a8ec5676

View File

@@ -403,6 +403,19 @@ body[data-theme='light'] .redis-viewer-workbench .ant-radio-button-wrapper-check
max-height: calc(100vh - 72px);
display: flex;
flex-direction: column;
padding: 0 !important;
overflow: hidden;
border-radius: 16px !important;
background: var(--gn-bg-panel, var(--ant-color-bg-elevated)) !important;
border: 1px solid var(--gn-br-2, var(--ant-color-border-secondary)) !important;
box-shadow: var(--gn-shadow-lg, var(--ant-box-shadow-secondary)) !important;
}
.connection-modal-wrap .ant-modal-header {
margin: 0 !important;
padding: 0 !important;
border-bottom: 1px solid var(--gn-br-1, rgba(16, 24, 40, 0.06));
background: var(--gn-bg-chrome, var(--ant-color-fill-quaternary)) !important;
}
.connection-modal-wrap .ant-modal-body {
@@ -412,6 +425,168 @@ body[data-theme='light'] .redis-viewer-workbench .ant-radio-button-wrapper-check
.connection-modal-wrap .ant-modal-footer {
flex-shrink: 0;
margin: 0 !important;
padding: 8px 12px !important;
border-top: 1px solid var(--gn-br-1, rgba(16, 24, 40, 0.06));
background: var(--gn-bg-chrome, var(--ant-color-fill-quaternary)) !important;
}
/* v2-theme.css 后加载且包含通用 Modal !important这里仅提高连接弹窗作用域优先级。 */
body[data-ui-version='v2'] .connection-modal-wrap .ant-modal .ant-modal-content {
border: 1px solid var(--gn-br-2) !important;
border-radius: 16px !important;
}
body[data-ui-version='v2'] .connection-modal-wrap .ant-modal .ant-modal-header {
margin: 0 !important;
padding: 0 !important;
border-bottom: 1px solid var(--gn-br-1) !important;
background: var(--gn-bg-chrome) !important;
}
body[data-ui-version='v2'] .connection-modal-wrap .ant-modal .ant-modal-body {
padding: 0 !important;
}
body[data-ui-version='v2'] .connection-modal-wrap .ant-modal .ant-modal-footer {
margin: 0 !important;
padding: 8px 12px !important;
border-top: 1px solid var(--gn-br-1) !important;
background: var(--gn-bg-chrome) !important;
}
.connection-modal-picker-wrap .ant-modal-footer {
display: none;
}
/* A · Studio 标题栏:左右列等宽,步骤始终保持几何居中。 */
.gn-conn-studio-header {
display: grid;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
align-items: center;
column-gap: 12px;
width: 100%;
padding: 12px 16px;
box-sizing: border-box;
}
.connection-modal-wrap.connection-modal-form .gn-conn-studio-header {
column-gap: 8px;
padding: 8px 12px;
}
.gn-conn-studio-header-left {
justify-self: start;
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.gn-conn-studio-type-badge {
width: 36px;
height: 36px;
border-radius: 10px;
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 36px;
overflow: hidden;
box-sizing: border-box;
color: #fff;
background: var(--gn-accent, #1677ff);
font-size: 22px;
line-height: 1;
}
.connection-modal-wrap.connection-modal-form .gn-conn-studio-type-badge {
width: 28px;
height: 28px;
flex-basis: 28px;
border-radius: 7px;
font-size: 11px;
}
.gn-conn-studio-heading {
min-width: 0;
}
.gn-conn-studio-title {
overflow: hidden;
color: var(--gn-fg-1, var(--ant-color-text));
font-size: 14.5px;
font-weight: 700;
line-height: 1.25;
text-overflow: ellipsis;
white-space: nowrap;
}
.connection-modal-wrap.connection-modal-form .gn-conn-studio-title {
font-size: 13.5px;
}
.gn-conn-studio-subtitle {
margin-top: 2px;
overflow: hidden;
color: var(--gn-fg-4, var(--ant-color-text-secondary));
font-size: 11.5px;
font-weight: 400;
line-height: 1.25;
text-overflow: ellipsis;
white-space: nowrap;
}
.gn-conn-studio-steps {
grid-column: 2;
justify-self: center;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
color: var(--gn-fg-5, var(--ant-color-text-quaternary));
font-size: 11.5px;
font-weight: 400;
line-height: 1;
white-space: nowrap;
}
.gn-conn-studio-steps [data-active='true'] {
color: var(--gn-accent, var(--ant-color-primary));
font-weight: 700;
}
.gn-conn-studio-steps [data-done='true'] {
color: var(--gn-fg-3, var(--ant-color-text-secondary));
}
.gn-conn-studio-step-arrow {
color: var(--gn-fg-5, var(--ant-color-text-quaternary));
font-size: 9px;
}
.gn-conn-studio-close {
grid-column: 3;
justify-self: end;
display: inline-grid;
place-items: center;
width: 28px;
height: 28px;
padding: 0;
border: 1px solid transparent;
border-radius: 7px;
background: transparent;
color: var(--gn-fg-4, var(--ant-color-text-secondary));
cursor: pointer;
}
.gn-conn-studio-close:hover {
background: var(--gn-bg-hover, var(--ant-color-fill-tertiary));
color: var(--gn-fg-1, var(--ant-color-text));
}
.gn-conn-studio-close:focus-visible {
outline: 2px solid color-mix(in srgb, var(--gn-accent, #1677ff) 55%, transparent);
outline-offset: 1px;
}
/* 连接配置的表单分组改用「小标题 + 细分割线」代替卡片(见 ConnectionModal.tsx 的
@@ -464,8 +639,6 @@ body[data-theme='dark'] .gn-connection-type-group:hover:not([aria-pressed='true'
grid-template-columns: minmax(200px, 24%) minmax(0, 1fr);
flex: 1;
min-height: 0;
border: 1px solid var(--gn-br-2, rgba(16, 24, 40, 0.08));
border-radius: 12px;
overflow: hidden;
background: var(--gn-bg-panel, transparent);
}
@@ -488,8 +661,10 @@ body[data-theme='dark'] .gn-connection-type-group:hover:not([aria-pressed='true'
background: var(--gn-bg-input, var(--gn-bg-panel-2, rgba(16, 24, 40, 0.04)));
border: 1px solid var(--gn-br-2, rgba(16, 24, 40, 0.1));
border-radius: 10px;
padding: 2px 8px 2px 10px;
padding: 7px 8px 7px 10px;
min-width: 0;
box-sizing: border-box;
overflow: hidden;
}
.gn-conn-picker-search-ico {
@@ -501,7 +676,8 @@ body[data-theme='dark'] .gn-connection-type-group:hover:not([aria-pressed='true'
.gn-conn-picker-search-input {
flex: 1 1 auto;
min-width: 0;
padding: 4px 0 !important;
width: 0;
padding: 0 !important;
}
.gn-conn-picker-search-input.ant-input-affix-wrapper,
@@ -510,6 +686,20 @@ body[data-theme='dark'] .gn-connection-type-group:hover:not([aria-pressed='true'
box-shadow: none !important;
}
.gn-conn-picker-search-shortcut {
flex-shrink: 0;
padding: 2px 5px;
border: 1px solid var(--gn-br-2, rgba(16, 24, 40, 0.1));
border-radius: 4px;
background: var(--gn-bg-panel-2, rgba(16, 24, 40, 0.03));
color: var(--gn-fg-5, rgba(0, 0, 0, 0.4));
font-family: var(--mono, ui-monospace, SFMono-Regular, Consolas, monospace);
font-size: 10px;
font-weight: 400;
line-height: 1.2;
white-space: nowrap;
}
.gn-conn-picker-sec-label {
font-size: 10.5px;
letter-spacing: 0.06em;
@@ -660,7 +850,7 @@ body[data-theme='dark'] .gn-connection-type-group:hover:not([aria-pressed='true'
width: 100%;
}
@media (min-width: 1100px) {
@media (min-width: 1200px) {
.connection-modal-wrap .gn-conn-picker-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
@@ -674,7 +864,7 @@ body[data-theme='dark'] .gn-connection-type-group:hover:not([aria-pressed='true'
color: inherit;
background: var(--gn-bg-panel-2, rgba(16, 24, 40, 0.03));
border: 1px solid var(--gn-br-2, rgba(16, 24, 40, 0.1));
border-radius: 12px;
border-radius: var(--radius, 8px);
padding: 12px;
transition: border-color 0.15s, transform 0.15s, background 0.15s;
min-width: 0;
@@ -732,6 +922,23 @@ body[data-theme='dark'] .gn-connection-type-group:hover:not([aria-pressed='true'
line-height: 1.25;
}
.gn-conn-type-card-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 10px;
}
.gn-conn-type-card-tag {
padding: 2px 7px;
border-radius: 999px;
background: var(--gn-bg-chrome, rgba(16, 24, 40, 0.03));
color: var(--gn-fg-4, rgba(0, 0, 0, 0.45));
font-size: 10.5px;
font-weight: 400;
line-height: 1.2;
}
.gn-conn-picker-empty {
padding: 18px 10px;
color: var(--gn-fg-4, rgba(0, 0, 0, 0.45));
@@ -743,16 +950,18 @@ body[data-theme='dark'] .gn-connection-type-group:hover:not([aria-pressed='true'
display: flex;
flex-direction: column;
min-height: 0;
height: 100%;
flex: 0 0 auto;
max-height: min(720px, calc(100vh - 132px));
}
.gn-conn-form-nav {
display: flex;
flex-wrap: wrap;
gap: 2px;
padding: 0 2px;
margin: 0 0 10px;
padding: 6px 12px 0;
margin: 0;
border-bottom: 1px solid var(--gn-br-1, rgba(16, 24, 40, 0.08));
background: var(--gn-bg-chrome, var(--ant-color-fill-quaternary));
flex: 0 0 auto;
}
@@ -762,10 +971,10 @@ body[data-theme='dark'] .gn-connection-type-group:hover:not([aria-pressed='true'
background: transparent;
text-align: center;
font: inherit;
font-size: 12.5px;
font-size: 12px;
font-weight: 600;
color: var(--gn-fg-4, rgba(0, 0, 0, 0.45));
padding: 7px 14px 8px;
padding: 7px 12px 8px;
border-radius: 8px 8px 0 0;
cursor: pointer;
display: inline-flex;
@@ -789,8 +998,326 @@ body[data-theme='dark'] .gn-connection-type-group:hover:not([aria-pressed='true'
.gn-conn-form-main {
flex: 1 1 auto;
min-height: 0;
width: 100%;
max-width: none;
overflow: auto;
padding: 2px 0 4px;
padding: 10px 2% 12px;
box-sizing: border-box;
}
/* URI 命令按钮:仅当前操作组使用绿色交互态,不影响弹窗底栏。 */
.gn-conn-uri-actions .ant-space-item > .ant-btn {
transition: background-color 0.14s ease, border-color 0.14s ease, color 0.14s ease, transform 0.1s ease, box-shadow 0.14s ease;
}
.gn-conn-uri-actions .ant-space-item > .ant-btn:hover,
.gn-conn-uri-actions .ant-space-item > .ant-btn:focus,
.gn-conn-uri-actions .ant-space-item > .ant-btn:focus-visible {
border-color: var(--gn-accent, #15803d) !important;
background: var(--gn-accent, #15803d) !important;
color: #fff !important;
box-shadow: 0 0 0 2px color-mix(in srgb, var(--gn-accent, #15803d) 22%, transparent);
}
.gn-conn-uri-actions .ant-space-item > .ant-btn:active {
border-color: var(--gn-accent, #15803d) !important;
background: var(--gn-accent, #15803d) !important;
color: #fff !important;
transform: scale(0.98);
}
/* URI 提示关闭按钮仅在悬浮或键盘聚焦时显示。 */
.gn-conn-uri-block .ant-alert-close-icon {
opacity: 0;
pointer-events: none;
transition: opacity 0.14s ease;
}
.gn-conn-uri-block .ant-alert:hover .ant-alert-close-icon,
.gn-conn-uri-block .ant-alert:focus-within .ant-alert-close-icon {
opacity: 1;
pointer-events: auto;
}
/* Studio 测试反馈:与 Demo 的 test-banner 保持同一信息密度。 */
.gn-conn-studio-test-banner {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 8px;
padding: 4px 8px;
border: 1px solid var(--gn-br-2, rgba(16, 24, 40, 0.1));
border-radius: 6px;
font-size: 12px;
line-height: 1.35;
}
.gn-conn-studio-test-banner > span:nth-child(2) {
min-width: 0;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.gn-conn-studio-test-banner[data-status='success'] {
border-color: color-mix(in srgb, var(--gn-accent, #1677ff) 35%, transparent);
background: var(--gn-accent-soft, rgba(22, 119, 255, 0.1));
color: var(--gn-accent, #1677ff);
}
.gn-conn-studio-test-banner[data-status='error'] {
border-color: color-mix(in srgb, var(--gn-danger, #ff4d4f) 35%, transparent);
background: color-mix(in srgb, var(--gn-danger, #ff4d4f) 12%, transparent);
color: var(--gn-danger, #ff4d4f);
}
.gn-conn-studio-test-banner .ant-btn-link {
height: auto;
padding: 0 2px;
color: currentColor;
font-size: 11.5px;
}
/* Studio 第二步底栏。 */
.gn-conn-studio-foot {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
width: 100%;
}
.gn-conn-studio-foot-left {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
flex: 1;
color: var(--gn-fg-4, var(--ant-color-text-secondary));
font-size: 12px;
}
.gn-conn-studio-foot-right {
flex: 0 0 auto;
}
.gn-conn-studio-button.ant-btn {
height: 28px;
padding: 0 10px;
border-radius: 7px;
font-size: 12px;
font-weight: 600;
box-shadow: none;
}
.gn-conn-studio-foot-status {
display: inline-flex;
align-items: center;
gap: 5px;
flex: 0 0 auto;
white-space: nowrap;
}
.gn-conn-studio-foot-status[data-status='success'] {
color: var(--gn-accent, #1677ff);
}
.gn-conn-studio-foot-status[data-status='error'],
.gn-conn-studio-foot-error {
color: var(--gn-danger, #ff4d4f);
}
.gn-conn-studio-foot-error {
min-width: 0;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.gn-conn-studio-details-button.ant-btn {
height: 24px;
padding: 0 8px;
border-radius: 6px;
border-color: color-mix(in srgb, var(--gn-danger, #ff4d4f) 35%, transparent);
background: color-mix(in srgb, var(--gn-danger, #ff4d4f) 10%, transparent);
color: var(--gn-danger, #ff4d4f);
font-size: 11.5px;
}
/* 外观页:对应 Demo 的 icon-grid / color-row / appear-preview。 */
.gn-conn-appearance {
display: grid;
gap: 14px;
}
.gn-conn-appearance-label {
margin: 0 0 6px;
color: var(--gn-fg-5, var(--ant-color-text-quaternary));
font-size: 11px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.gn-conn-appearance-label span {
margin-left: 6px;
color: var(--gn-fg-4, var(--ant-color-text-secondary));
font-weight: 500;
letter-spacing: 0;
text-transform: none;
}
.gn-conn-appearance-icon-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(36px, 1fr));
gap: 6px;
max-height: 148px;
overflow: auto;
padding: 2px;
}
.gn-conn-appearance-icon {
appearance: none;
display: grid;
place-items: center;
width: 100%;
aspect-ratio: 1;
padding: 4px;
border: 1.5px solid var(--gn-br-2, rgba(16, 24, 40, 0.1));
border-radius: 8px;
background: var(--gn-bg-input, var(--ant-color-bg-container));
color: inherit;
cursor: pointer;
}
.gn-conn-appearance-icon:hover {
border-color: var(--gn-br-3, rgba(16, 24, 40, 0.18));
background: var(--gn-bg-hover, var(--ant-color-fill-tertiary));
}
.gn-conn-appearance-icon[data-active='true'] {
border-color: var(--gn-accent, #1677ff);
box-shadow: 0 0 0 2px var(--gn-accent-soft, rgba(22, 119, 255, 0.12));
}
.gn-conn-appearance-colors {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
}
.gn-conn-appearance-color,
.gn-conn-appearance-custom-color {
width: 22px;
height: 22px;
flex: 0 0 22px;
padding: 0;
border: 2px solid transparent;
border-radius: 50%;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.12);
cursor: pointer;
}
.gn-conn-appearance-color[data-active='true'] {
border-color: var(--gn-fg-1, var(--ant-color-text));
box-shadow: 0 0 0 2px var(--gn-bg-panel, var(--ant-color-bg-container)), 0 0 0 3.5px var(--gn-fg-1, var(--ant-color-text));
}
.gn-conn-appearance-custom-color {
overflow: hidden;
border: 1px solid var(--gn-br-2, rgba(16, 24, 40, 0.1));
background: var(--gn-bg-input, var(--ant-color-bg-container));
}
.gn-conn-appearance-custom-color::-webkit-color-swatch-wrapper {
padding: 1px;
}
.gn-conn-appearance-custom-color::-webkit-color-swatch {
border: 0;
border-radius: 50%;
}
.gn-conn-appearance-preview {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
border: 1px solid var(--gn-br-2, rgba(16, 24, 40, 0.1));
border-radius: 8px;
background: var(--gn-bg-panel-2, rgba(16, 24, 40, 0.03));
}
.gn-conn-appearance-preview-main {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.gn-conn-appearance-preview-icon {
display: grid;
place-items: center;
width: 28px;
height: 28px;
flex: 0 0 28px;
overflow: hidden;
border: 1.5px solid var(--gn-br-2, rgba(16, 24, 40, 0.1));
border-radius: 7px;
background: var(--gn-bg-input, var(--ant-color-bg-container));
}
.gn-conn-appearance-preview-copy {
min-width: 0;
}
.gn-conn-appearance-preview-name {
overflow: hidden;
color: var(--gn-fg-1, var(--ant-color-text));
font-size: 13px;
font-weight: 650;
text-overflow: ellipsis;
white-space: nowrap;
}
.gn-conn-appearance-preview-meta {
margin-top: 1px;
color: var(--gn-fg-5, var(--ant-color-text-quaternary));
font-size: 11px;
}
.gn-conn-appearance-reset.ant-btn {
margin-left: auto;
}
@media (max-width: 700px) {
.gn-conn-studio-header {
grid-template-columns: minmax(0, 1fr) auto;
}
.gn-conn-studio-steps {
display: none;
}
.gn-conn-studio-close {
grid-column: 2;
}
.gn-conn-picker-body {
grid-template-columns: minmax(155px, 36%) minmax(0, 1fr);
}
.gn-conn-picker-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.gn-conn-studio-foot-error,
.gn-conn-studio-details-button {
display: none;
}
}
/* URI block in basic tab — flat, no heavy card chrome */
@@ -853,8 +1380,34 @@ body[data-theme='dark'] .gn-connection-type-group:hover:not([aria-pressed='true'
}
.connection-modal-wrap.connection-modal-form .ant-modal-body {
padding-left: 12px !important;
padding-right: 12px !important;
padding-left: 0 !important;
padding-right: 0 !important;
}
/* 垂直拖拽后,表单内容区消费新增高度,底栏保持固定。 */
.connection-modal-wrap.connection-modal-form .gn-resizable-draggable-modal[data-has-resized-height='true'] .ant-modal-content {
height: var(--gn-modal-resized-height);
max-height: var(--gn-modal-resized-height);
}
.connection-modal-wrap.connection-modal-form .gn-resizable-draggable-modal[data-has-resized-height='true'] .ant-modal-body {
display: flex;
flex: 1 1 auto;
flex-direction: column;
min-height: 0;
max-height: none !important;
overflow: hidden !important;
}
.connection-modal-wrap.connection-modal-form .gn-resizable-draggable-modal[data-has-resized-height='true'] .gn-conn-studio-form,
.connection-modal-wrap.connection-modal-form .gn-resizable-draggable-modal[data-has-resized-height='true'] .gn-conn-form-layout {
display: flex;
flex: 1 1 auto;
flex-direction: column;
width: 100%;
height: 100%;
min-height: 0;
max-height: none;
}
/* demo .f-row */

View File

@@ -193,9 +193,9 @@ vi.mock("@ant-design/icons", () => {
CloudOutlined: Icon,
CheckCircleFilled: Icon,
CloseCircleFilled: Icon,
LinkOutlined: Icon,
EditOutlined: Icon,
AppstoreOutlined: Icon,
ArrowLeftOutlined: Icon,
CloseOutlined: Icon,
PlusOutlined: Icon,
BgColorsOutlined: Icon,
ApiOutlined: Icon,
ClusterOutlined: Icon,
@@ -382,6 +382,8 @@ describe("ConnectionModal i18n", () => {
vi.stubGlobal("window", {
setTimeout,
clearTimeout,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
});
storeState.theme = "light";
storeState.languagePreference = "zh-CN";
@@ -504,7 +506,8 @@ describe("ConnectionModal i18n", () => {
expect(textContent(renderer!.toJSON())).toContain("选择数据源类型");
expect(textContent(renderer!.toJSON())).toContain("选择数据源");
expect(textContent(renderer!.toJSON())).toContain("取消");
expect(textContent(renderer!.toJSON())).toContain("1 选类型");
expect(textContent(renderer!.toJSON())).toContain("SSL");
await act(async () => {
findClickableCard(renderer!, "MySQL").props.onClick();
@@ -519,7 +522,9 @@ describe("ConnectionModal i18n", () => {
renderer!.update(<ConnectionModal open onClose={vi.fn()} />);
});
expect(textContent(renderer!.toJSON())).toContain("新建 MySQL 连接");
expect(textContent(renderer!.toJSON())).toContain("MySQL 连接");
expect(textContent(renderer!.toJSON())).toContain("2 参数");
expect(textContent(renderer!.toJSON())).toContain("未测试");
expect(textContent(renderer!.toJSON())).toContain("名称");
expect(textContent(renderer!.toJSON())).toContain("测试连接");
expect(textContent(renderer!.toJSON())).toContain("保存");
@@ -552,13 +557,15 @@ describe("ConnectionModal i18n", () => {
expect(textContent(renderer!.toJSON())).toContain("Select connection type");
expect(textContent(renderer!.toJSON())).toContain("Click to configure");
expect(textContent(renderer!.toJSON())).toContain("Categories");
expect(textContent(renderer!.toJSON())).toContain("Cancel");
expect(textContent(renderer!.toJSON())).toContain("1 Choose type");
await act(async () => {
findClickableCard(renderer!, "MySQL").props.onClick();
});
expect(textContent(renderer!.toJSON())).toContain("New MySQL connection");
expect(textContent(renderer!.toJSON())).toContain("MySQL connection");
expect(textContent(renderer!.toJSON())).toContain("2 Parameters");
expect(textContent(renderer!.toJSON())).toContain("Not tested");
expect(textContent(renderer!.toJSON())).toContain("Name");
expect(textContent(renderer!.toJSON())).toContain("Test connection");
expect(textContent(renderer!.toJSON())).toContain("Save");
@@ -630,7 +637,7 @@ describe("ConnectionModal i18n", () => {
findClickableCard(renderer!, sourceLabel).props.onClick();
});
// Demo生产保护默认展开折叠后选项文案应消失
// A · Studio生产保护默认展开折叠后选项文案隐藏。
let pageText = textContent(renderer!.toJSON());
expect(pageText).toContain(expectations[0]);
expect(pageText).toContain(expectations[2]);
@@ -647,22 +654,8 @@ describe("ConnectionModal i18n", () => {
pageText = textContent(renderer!.toJSON());
expect(pageText).toContain(expectations[0]);
expect(pageText).toContain(
language === "zh-CN" ? "未限制" : "No restrictions",
);
expect(pageText).not.toContain(expectations[2]);
expect(pageText).not.toContain(expectations[3]);
await act(async () => {
protectionToggle.props.onClick({ stopPropagation: vi.fn() });
});
pageText = textContent(renderer!.toJSON());
expectations
.filter((expected) => expected !== expectations[1])
.forEach((expected) => {
expect(pageText).toContain(expected);
});
expect(pageText).not.toContain("connection.modal.section.undefined.title");
expect(pageText).not.toContain("connection.modal.section.undefined.description");
},
@@ -1096,6 +1089,15 @@ describe("ConnectionModal i18n", () => {
message: "backend raw error: /tmp/app.db",
});
const { default: ConnectionModal } = await import("./ConnectionModal");
let dismissUriFeedback: (() => void) | undefined;
const uriFeedbackTimer = vi.fn((callback: () => void, delay: number) => {
if (delay === 4000) dismissUriFeedback = callback;
return 1;
});
Object.assign(window, {
setTimeout: uriFeedbackTimer,
clearTimeout: vi.fn(),
});
let renderer: ReactTestRenderer;
await act(async () => {
@@ -1115,6 +1117,12 @@ describe("ConnectionModal i18n", () => {
});
expect(textContent(renderer!.toJSON())).toContain("URI generated.");
expect(uriFeedbackTimer).toHaveBeenCalledWith(expect.any(Function), 4000);
await act(async () => {
dismissUriFeedback?.();
});
expect(textContent(renderer!.toJSON())).not.toContain("URI generated.");
await act(async () => {
findButton(renderer!, "Back").props.onClick();
@@ -1132,6 +1140,45 @@ describe("ConnectionModal i18n", () => {
);
});
it("automatically dismisses URI warning feedback after four seconds", async () => {
storeState.appearance.uiVersion = "legacy";
setCurrentLanguage("en-US");
const { default: ConnectionModal } = await import("./ConnectionModal");
let dismissUriFeedback: (() => void) | undefined;
const uriFeedbackTimer = vi.fn((callback: () => void, delay: number) => {
if (delay === 4000) dismissUriFeedback = callback;
return 1;
});
Object.assign(window, {
setTimeout: uriFeedbackTimer,
clearTimeout: vi.fn(),
});
let renderer: ReactTestRenderer;
await act(async () => {
renderer = create(<ConnectionModal open onClose={vi.fn()} />);
});
await act(async () => {
findClickableCard(renderer!, "MySQL").props.onClick();
});
await act(async () => {
expandUriSection(renderer!);
});
const parseUriButton = findButton(renderer!, "Parse from URI");
expect(parseUriButton, textContent(renderer!.toJSON())).toBeDefined();
await act(async () => {
parseUriButton.props.onClick();
});
expect(textContent(renderer!.toJSON())).toContain("Enter a URI first");
expect(uriFeedbackTimer).toHaveBeenCalledWith(expect.any(Function), 4000);
await act(async () => {
dismissUriFeedback?.();
});
expect(textContent(renderer!.toJSON())).not.toContain("Enter a URI first");
});
it("retranslates test failure feedback while preserving raw detail when language changes in-place", async () => {
storeState.appearance.uiVersion = "legacy";
setCurrentLanguage("zh-CN");

View File

@@ -20,9 +20,9 @@ import {
FileTextOutlined,
CheckCircleFilled,
CloseCircleFilled,
LinkOutlined,
EditOutlined,
AppstoreOutlined,
ArrowLeftOutlined,
CloseOutlined,
PlusOutlined,
ApiOutlined,
ClusterOutlined,
CodeOutlined,
@@ -170,7 +170,8 @@ const PRIMARY_USERNAME_OPTIONAL_TYPES = new Set([
/** Step1 选型网格需要较宽Step2 密排表单对齐 Demo ~600避免右侧空洞或输入框被拉超长 */
const CONNECTION_MODAL_WIDTH_STEP1 = 960;
const CONNECTION_MODAL_WIDTH_STEP2 = 600;
const CONNECTION_MODAL_BODY_HEIGHT = 620;
// A · Studio 第一步整体高度约 560px扣除头部后主体固定为 500px。
const CONNECTION_MODAL_BODY_HEIGHT = 500;
const REDIS_DEFAULT_DATABASE_COUNT = 16;
const CLICKHOUSE_PROTOCOL_OPTIONS: Array<{
value: ClickHouseProtocolChoice;
@@ -2440,6 +2441,25 @@ const ConnectionModal: React.FC<{
setDbTypeQuery("");
};
useEffect(() => {
if (!open || step !== 1 || typeof window === "undefined") return;
// Studio 选型页快捷键处理器,仅在第一步挂载。
const focusStudioSearch = (event: KeyboardEvent) => {
if (!(event.metaKey || event.ctrlKey) || event.key.toLowerCase() !== "k") {
return;
}
// 当前弹窗内的数据源搜索框。
const searchInput = document.querySelector<HTMLInputElement>(
'.connection-modal-wrap [data-connection-type-search="true"]',
);
if (!searchInput) return;
event.preventDefault();
searchInput.focus();
};
window.addEventListener("keydown", focusStudioSearch);
return () => window.removeEventListener("keydown", focusStudioSearch);
}, [open, step]);
const dbTypes = getAllConnectionTypeCatalogItems();
const recentConnectionChips = useMemo(() => {
@@ -2511,8 +2531,12 @@ const ConnectionModal: React.FC<{
onChange={(event) => handleDbTypeQueryChange(event.target.value)}
placeholder={t("connection.modal.step1.search.placeholder")}
className="gn-conn-picker-search-input"
data-connection-type-search="true"
{...noAutoCapInputProps}
/>
<kbd className="gn-conn-picker-search-shortcut">
{isMacLikePlatform() ? "Cmd K" : "Ctrl K"}
</kbd>
</div>
{recentConnectionChips.length > 0 ? (
@@ -2588,6 +2612,15 @@ const ConnectionModal: React.FC<{
<div className="gn-conn-picker-grid">
{visibleDbTypeItems.map((item) => {
const color = getDbDefaultColor(item.key);
// 卡片所属的数据源分类展示标签。
const categoryLabel = localizedDbTypeGroups.find((group) =>
group.items.some((groupItem) => groupItem.key === item.key),
)?.label;
// Studio 卡片底部展示标签,不参与连接逻辑。
const studioTags = [
categoryLabel,
supportsSSLForType(item.key) ? "SSL" : null,
].filter((tag): tag is string => Boolean(tag));
return (
<button
key={item.key}
@@ -2613,6 +2646,15 @@ const ConnectionModal: React.FC<{
</div>
</div>
</div>
{studioTags.length > 0 ? (
<div className="gn-conn-type-card-tags" aria-hidden="true">
{studioTags.map((tag) => (
<span key={tag} className="gn-conn-type-card-tag">
{tag}
</span>
))}
</div>
) : null}
</button>
);
})}
@@ -2696,6 +2738,7 @@ const ConnectionModal: React.FC<{
renderJvmSectionHeader,
renderStoredSecretControls,
resolvedUriFeedbackMessage,
resolvedTestResultMessage,
rocketmqTopology,
selectingCertificateField,
selectingDbFile,
@@ -2735,11 +2778,7 @@ const ConnectionModal: React.FC<{
const getFooter = () => {
if (step === 1) {
return [
<Button key="cancel" onClick={onClose}>
{t("common.action.cancel")}
</Button>,
];
return null;
}
const isTestSuccess = testResult?.type === "success";
const hasTestError = !!testResult && !isTestSuccess;
@@ -2754,74 +2793,40 @@ const ConnectionModal: React.FC<{
driverStatusChecking ||
!!unsupportedJvmModeMessage;
return (
<div
style={{
display: "flex",
width: "100%",
alignItems: "center",
justifyContent: "space-between",
gap: 12,
padding: "4px 2px 0",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
gap: 8,
flex: 1,
minWidth: 0,
}}
>
<div className="gn-conn-studio-foot">
<div className="gn-conn-studio-foot-left">
{!initialValues && (
<Button key="back" onClick={() => setStep(1)}>
<Button
key="back"
className="gn-conn-studio-button"
icon={<ArrowLeftOutlined />}
onClick={() => setStep(1)}
>
{t("common.action.back")}
</Button>
)}
{testResult ? (
<span
style={{
display: "inline-flex",
alignItems: "center",
gap: 6,
height: 24,
padding: "0 10px",
borderRadius: 999,
border: isTestSuccess
? "1px solid rgba(82, 196, 26, 0.35)"
: "1px solid rgba(255, 77, 79, 0.35)",
background: isTestSuccess
? "rgba(82, 196, 26, 0.10)"
: "rgba(255, 77, 79, 0.10)",
color: isTestSuccess ? "#389e0d" : "#cf1322",
fontSize: 12,
lineHeight: "22px",
whiteSpace: "nowrap",
boxSizing: "border-box",
}}
>
<span
className="gn-conn-studio-foot-status"
data-status={testResult ? (isTestSuccess ? "success" : "error") : "idle"}
>
{testResult ? (
<>
{isTestSuccess ? <CheckCircleFilled /> : <CloseCircleFilled />}
<span>
{isTestSuccess
? t("connection.status.success")
: t("connection.status.failure")}
</span>
</span>
) : null}
<span>
{isTestSuccess
? t("connection.status.success")
: t("connection.status.failure")}
</span>
</>
) : (
t("connection.modal.studio.test.idle")
)}
</span>
{hasTestError && (
<span
data-connection-test-error-summary="true"
title={testFailureSummary}
style={{
minWidth: 0,
flex: 1,
color: "#cf1322",
fontSize: 12,
lineHeight: "20px",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
className="gn-conn-studio-foot-error"
>
{testFailureSummary}
</span>
@@ -2830,35 +2835,30 @@ const ConnectionModal: React.FC<{
<Button
size="small"
icon={<FileTextOutlined />}
style={{
height: 24,
borderRadius: 999,
padding: "0 10px",
borderColor: "#ffccc7",
background: "#fff2f0",
color: "#cf1322",
}}
className="gn-conn-studio-details-button"
onClick={() => setTestErrorLogOpen(true)}
>
{t("connection.action.viewDetails")}
</Button>
)}
</div>
<Space size={8} style={{ flexShrink: 0 }}>
<Space size={8} className="gn-conn-studio-foot-right">
<Button
key="test"
className="gn-conn-studio-button"
loading={testingConnection}
disabled={operationBlocked || saving}
onClick={requestTest}
>
{t("connection.action.test")}
</Button>
<Button key="cancel" onClick={onClose}>
<Button key="cancel" className="gn-conn-studio-button" onClick={onClose}>
{t("common.action.cancel")}
</Button>
<Button
key="submit"
type="primary"
className="gn-conn-studio-button"
loading={saving}
disabled={operationBlocked || testingConnection}
onClick={handleOk}
@@ -2870,46 +2870,97 @@ const ConnectionModal: React.FC<{
);
};
const getTitle = () => {
if (step === 1) {
return renderConnectionModalTitle(
<AppstoreOutlined />,
t("connection.modal.title.step1"),
t("connection.modal.description.step1"),
);
}
const getStudioTitle = () => {
const typeName = dbTypes.find((t) => t.key === dbType)?.name || dbType;
return initialValues
? renderConnectionModalTitle(
<EditOutlined />,
t("connection.modal.title.edit"),
t("connection.modal.description.edit", { type: typeName }),
)
: renderConnectionModalTitle(
<LinkOutlined />,
t("connection.modal.title.create", { type: typeName }),
t("connection.modal.description.create"),
);
// 是否处于第一步数据源选型页。
const pickerStep = step === 1;
// Studio 标题栏三段进度文案。
const stepItems = pickerStep
? [
t("connection.modal.studio.step.chooseType"),
t("connection.modal.studio.step.configure"),
t("connection.modal.studio.step.testSave"),
]
: [
t("connection.modal.studio.step.type"),
t("connection.modal.studio.step.params"),
t("connection.modal.studio.step.save"),
];
// 当前步骤对应的标题文案。
const title = pickerStep
? t("connection.modal.title.step1")
: initialValues
? t("connection.modal.studio.title.edit", { type: typeName })
: t("connection.modal.studio.title.connection", { type: typeName });
return (
<div className="gn-conn-studio-header" data-connection-step={step}>
<div className="gn-conn-studio-header-left">
<div
className="gn-conn-studio-type-badge"
style={pickerStep ? undefined : { background: getDbDefaultColor(dbType) }}
aria-hidden="true"
>
{pickerStep ? <PlusOutlined /> : getDbIcon(dbType, "#ffffff", 28)}
</div>
<div className="gn-conn-studio-heading">
<div className="gn-conn-studio-title">{title}</div>
{pickerStep ? (
<div className="gn-conn-studio-subtitle">
{t("connection.modal.description.step1")}
</div>
) : null}
</div>
</div>
<div className="gn-conn-studio-steps" aria-label={t("connection.modal.studio.progress")}>
{stepItems.map((item, index) => {
// 当前高亮的进度节点。
const active = pickerStep ? index === 0 : index === 1;
// 已完成的进度节点。
const done = !pickerStep && index === 0;
return (
<React.Fragment key={item}>
{index > 0 ? (
<RightOutlined className="gn-conn-studio-step-arrow" aria-hidden="true" />
) : null}
<span data-active={active ? "true" : undefined} data-done={done ? "true" : undefined}>
{index + 1} {item}
</span>
</React.Fragment>
);
})}
</div>
<button
type="button"
className="gn-conn-studio-close"
aria-label={t("common.action.close")}
title={t("common.action.close")}
onClick={onClose}
>
<CloseOutlined />
</button>
</div>
);
};
const isFormStep = step !== 1;
// Step2 对齐 demo `.frame-form { height: fit-content }`,避免固定 620 撑出大片空白
const modalBodyStyle = {
padding: isFormStep ? "10px 12px 12px" : "12px 24px 18px",
padding: 0,
height: isFormStep ? "auto" : CONNECTION_MODAL_BODY_HEIGHT,
maxHeight: isFormStep ? "min(720px, calc(100vh - 160px))" : undefined,
maxHeight: isFormStep ? "min(720px, calc(100vh - 132px))" : undefined,
minHeight: isFormStep ? 0 : CONNECTION_MODAL_BODY_HEIGHT,
overflowY: "auto" as const,
overflowY: "hidden" as const,
overflowX: "hidden" as const,
};
return (
<>
<Modal
title={getTitle()}
title={getStudioTitle()}
open={open}
onCancel={onClose}
footer={getFooter()}
closable={false}
centered
wrapClassName={
isFormStep
@@ -2922,17 +2973,9 @@ const ConnectionModal: React.FC<{
maskClosable={false}
styles={{
content: modalShellStyle,
header: {
background: "transparent",
borderBottom: "none",
paddingBottom: 8,
},
header: { background: "transparent" },
body: modalBodyStyle,
footer: {
background: "transparent",
borderTop: "none",
paddingTop: 10,
},
footer: { background: "transparent" },
}}
>
{step === 1 ? renderStep1() : renderStep2()}

View File

@@ -15,7 +15,9 @@ import {
} from "antd";
import {
ApiOutlined,
CheckCircleFilled,
ClusterOutlined,
CloseCircleFilled,
CodeOutlined,
DatabaseOutlined,
FileTextOutlined,
@@ -89,6 +91,9 @@ const PRIMARY_USERNAME_OPTIONAL_TYPES = new Set([
"rabbitmq",
]);
// URI 操作反馈统一保留 4 秒,便于用户读取后自动回收空间。
const URI_FEEDBACK_AUTO_DISMISS_MS = 4000;
type ConnectionModalStep2Props = Record<string, any>;
const ConnectionModalStep2: React.FC<ConnectionModalStep2Props> = (props) => {
@@ -158,6 +163,7 @@ const ConnectionModalStep2: React.FC<ConnectionModalStep2Props> = (props) => {
renderConfigSectionCard,
renderJvmSectionHeader,
renderStoredSecretControls,
resolvedTestResultMessage,
resolvedUriFeedbackMessage,
rocketmqTopology,
selectingCertificateField,
@@ -194,7 +200,7 @@ const ConnectionModalStep2: React.FC<ConnectionModalStep2Props> = (props) => {
useSSL,
} = props;
// Demo进入配置后生产保护默认展开便于看到限制项
// A · Studio 默认展开生产保护,保留用户按需折叠的交互。
const [readOnlyProtectionExpanded, setReadOnlyProtectionExpanded] =
React.useState(true);
@@ -202,6 +208,14 @@ const ConnectionModalStep2: React.FC<ConnectionModalStep2Props> = (props) => {
setReadOnlyProtectionExpanded(true);
}, [dbType]);
React.useEffect(() => {
if (!uriFeedback) return undefined;
const dismissTimer = window.setTimeout(() => {
setUriFeedback(null);
}, URI_FEEDBACK_AUTO_DISMISS_MS);
return () => window.clearTimeout(dismissTimer);
}, [setUriFeedback, uriFeedback]);
const renderStep2 = () => {
const showConnectionReadOnlyField = supportsConnectionReadOnlyMode({
type: dbType,
@@ -238,8 +252,13 @@ const ConnectionModalStep2: React.FC<ConnectionModalStep2Props> = (props) => {
placeholder={getUriPlaceholder(dbType)}
/>
</Form.Item>
<Space size={8} style={{ marginBottom: uriFeedback ? 8 : 0 }} wrap>
<Button size="small" type="primary" onClick={handleParseURI}>
<Space
size={8}
className="gn-conn-uri-actions"
style={{ marginBottom: uriFeedback ? 8 : 0 }}
wrap
>
<Button size="small" onClick={handleParseURI}>
{t("connection.modal.uri.action.parse")}
</Button>
<Button size="small" onClick={handleGenerateURI}>
@@ -2521,6 +2540,7 @@ const ConnectionModalStep2: React.FC<ConnectionModalStep2Props> = (props) => {
<Form
form={form}
layout="vertical"
className="gn-conn-studio-form"
initialValues={{
type: "mysql",
host: "localhost",
@@ -2812,33 +2832,17 @@ const ConnectionModalStep2: React.FC<ConnectionModalStep2Props> = (props) => {
customIconColor || getDbDefaultColor(effectiveIconType);
const appearanceSection = (
<div style={{ display: "grid", gap: 14 }}>
<div className="gn-conn-appearance">
<div>
<div
style={{
marginBottom: 8,
fontSize: 12,
fontWeight: 700,
color: darkMode ? "#f5f7ff" : "#162033",
}}
>
<div className="gn-conn-appearance-label">
{t("connection.modal.appearance.icon")}
<span
style={{
marginLeft: 8,
fontSize: 11,
fontWeight: 500,
color: darkMode
? "rgba(255,255,255,0.45)"
: "rgba(0,0,0,0.35)",
}}
>
<span>
{t("connection.modal.appearance.current", {
name: getDbIconLabel(effectiveIconType),
})}
</span>
</div>
<div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
<div className="gn-conn-appearance-icon-grid">
{DB_ICON_TYPES.map((iconKey) => {
const isActive = effectiveIconType === iconKey;
return (
@@ -2846,26 +2850,13 @@ const ConnectionModalStep2: React.FC<ConnectionModalStep2Props> = (props) => {
key={iconKey}
type="button"
title={getDbIconLabel(iconKey)}
className="gn-conn-appearance-icon"
data-active={isActive ? "true" : undefined}
onClick={() =>
setCustomIconType(
iconKey === dbType ? undefined : iconKey,
)
}
style={{
width: 40,
height: 40,
borderRadius: 9,
display: "grid",
placeItems: "center",
border: `2px solid ${isActive ? effectiveIconColor : darkMode ? "rgba(255,255,255,0.08)" : "rgba(0,0,0,0.06)"}`,
background: isActive
? darkMode
? "rgba(255,255,255,0.08)"
: "rgba(24,144,255,0.06)"
: "transparent",
cursor: "pointer",
transition: "all 120ms ease",
}}
>
{getDbIcon(
iconKey,
@@ -2878,30 +2869,19 @@ const ConnectionModalStep2: React.FC<ConnectionModalStep2Props> = (props) => {
</div>
</div>
<div>
<div
style={{
marginBottom: 8,
fontSize: 12,
fontWeight: 700,
color: darkMode ? "#f5f7ff" : "#162033",
}}
>
<div className="gn-conn-appearance-label">
{t("connection.modal.appearance.color")}
</div>
<div
style={{
display: "flex",
flexWrap: "wrap",
gap: 8,
alignItems: "center",
}}
>
<div className="gn-conn-appearance-colors">
{PRESET_ICON_COLORS.map((presetColor) => {
const isActive = effectiveIconColor === presetColor;
return (
<button
key={presetColor}
type="button"
className="gn-conn-appearance-color"
data-active={isActive ? "true" : undefined}
aria-label={presetColor}
onClick={() =>
setCustomIconColor(
presetColor === getDbDefaultColor(effectiveIconType)
@@ -2910,18 +2890,7 @@ const ConnectionModalStep2: React.FC<ConnectionModalStep2Props> = (props) => {
)
}
style={{
width: 26,
height: 26,
borderRadius: 7,
background: presetColor,
border: isActive
? `2.5px solid ${darkMode ? "#fff" : "#162033"}`
: "2px solid transparent",
cursor: "pointer",
transition: "all 120ms ease",
boxShadow: isActive
? `0 0 0 2px ${presetColor}40`
: "none",
}}
/>
);
@@ -2937,45 +2906,21 @@ const ConnectionModalStep2: React.FC<ConnectionModalStep2Props> = (props) => {
)
}
title={t("connection.modal.appearance.customColor")}
style={{
width: 26,
height: 26,
border: "none",
padding: 0,
cursor: "pointer",
borderRadius: 6,
background: "transparent",
}}
className="gn-conn-appearance-custom-color"
/>
</div>
</div>
<div
style={{
display: "flex",
alignItems: "center",
gap: 12,
padding: "10px 12px",
borderRadius: 10,
border: `1px solid ${darkMode ? "rgba(255,255,255,0.08)" : "rgba(16,24,40,0.08)"}`,
background: darkMode
? "rgba(255,255,255,0.03)"
: "rgba(16,24,40,0.02)",
}}
>
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
{getDbIcon(effectiveIconType, effectiveIconColor, 24)}
<div style={{ minWidth: 0 }}>
<div
style={{
fontSize: 13,
fontWeight: 700,
color: darkMode ? "#e0e0e0" : "#333",
}}
>
<div className="gn-conn-appearance-preview">
<div className="gn-conn-appearance-preview-main">
<div className="gn-conn-appearance-preview-icon">
{getDbIcon(effectiveIconType, effectiveIconColor, 18)}
</div>
<div className="gn-conn-appearance-preview-copy">
<div className="gn-conn-appearance-preview-name">
{form.getFieldValue("name") ||
t("connection.modal.appearance.previewName")}
</div>
<div style={{ ...modalMutedTextStyle, fontSize: 11 }}>
<div className="gn-conn-appearance-preview-meta">
{t("connection.modal.appearance.preview")}
</div>
</div>
@@ -2984,7 +2929,7 @@ const ConnectionModalStep2: React.FC<ConnectionModalStep2Props> = (props) => {
<Button
size="small"
type="link"
style={{ marginLeft: "auto" }}
className="gn-conn-appearance-reset"
onClick={() => {
setCustomIconType(undefined);
setCustomIconColor(undefined);
@@ -3024,7 +2969,33 @@ const ConnectionModalStep2: React.FC<ConnectionModalStep2Props> = (props) => {
);
})}
</nav>
<div className="gn-conn-form-main">{currentSectionContent}</div>
<div className="gn-conn-form-main">
{testResult ? (
<div
className="gn-conn-studio-test-banner"
data-status={testResult.type === "success" ? "success" : "error"}
role="status"
>
{testResult.type === "success" ? (
<CheckCircleFilled aria-hidden="true" />
) : (
<CloseCircleFilled aria-hidden="true" />
)}
<span>{resolvedTestResultMessage}</span>
{testResult.type !== "success" ? (
<Button
type="link"
size="small"
icon={<FileTextOutlined />}
onClick={() => setTestErrorLogOpen(true)}
>
{t("connection.action.viewDetails")}
</Button>
) : null}
</div>
) : null}
{currentSectionContent}
</div>
</div>
);
})()}

View File

@@ -8776,6 +8776,16 @@
"connection_modal.step1.categories": "Kategorien",
"connection_modal.step1.clickToConfigure": "Zum Konfigurieren klicken",
"connection_modal.step1.recent.empty": "Keine letzten Verbindungen",
"connection_modal.studio.step.chooseType": "Typ wählen",
"connection_modal.studio.step.configure": "Konfigurieren",
"connection_modal.studio.step.testSave": "Testen & speichern",
"connection_modal.studio.step.type": "Typ",
"connection_modal.studio.step.params": "Parameter",
"connection_modal.studio.step.save": "Speichern",
"connection_modal.studio.title.edit": "{{type}}-Verbindung bearbeiten",
"connection_modal.studio.title.connection": "{{type}}-Verbindung",
"connection_modal.studio.progress": "Fortschritt der Verbindungseinrichtung",
"connection_modal.studio.test.idle": "Nicht getestet",
"connection_modal.config.advanced.title": "Erweitert",
"connection_modal.config.advanced.description": "Verbindungsparameter und Typ-Extras",
"connection_modal.config.advanced.empty": "Keine erweiterten Optionen für diesen Typ",

View File

@@ -8776,6 +8776,16 @@
"connection_modal.step1.categories": "Categories",
"connection_modal.step1.clickToConfigure": "Click to configure",
"connection_modal.step1.recent.empty": "No recent connections",
"connection_modal.studio.step.chooseType": "Choose type",
"connection_modal.studio.step.configure": "Configure",
"connection_modal.studio.step.testSave": "Test & save",
"connection_modal.studio.step.type": "Type",
"connection_modal.studio.step.params": "Parameters",
"connection_modal.studio.step.save": "Save",
"connection_modal.studio.title.edit": "Edit {{type}} connection",
"connection_modal.studio.title.connection": "{{type}} connection",
"connection_modal.studio.progress": "Connection setup progress",
"connection_modal.studio.test.idle": "Not tested",
"connection_modal.config.advanced.title": "Advanced",
"connection_modal.config.advanced.description": "Connection parameters and type extras",
"connection_modal.config.advanced.empty": "No advanced options for this type",

View File

@@ -8776,6 +8776,16 @@
"connection_modal.step1.categories": "カテゴリ",
"connection_modal.step1.clickToConfigure": "クリックして設定",
"connection_modal.step1.recent.empty": "最近の接続なし",
"connection_modal.studio.step.chooseType": "種類を選択",
"connection_modal.studio.step.configure": "パラメータ",
"connection_modal.studio.step.testSave": "テストと保存",
"connection_modal.studio.step.type": "種類",
"connection_modal.studio.step.params": "パラメータ",
"connection_modal.studio.step.save": "保存",
"connection_modal.studio.title.edit": "{{type}} 接続を編集",
"connection_modal.studio.title.connection": "{{type}} 接続",
"connection_modal.studio.progress": "接続作成の進行状況",
"connection_modal.studio.test.idle": "未テスト",
"connection_modal.config.advanced.title": "詳細",
"connection_modal.config.advanced.description": "接続パラメータとタイプ別拡張",
"connection_modal.config.advanced.empty": "このタイプに詳細設定はありません",

View File

@@ -8776,6 +8776,16 @@
"connection_modal.step1.categories": "Категории",
"connection_modal.step1.clickToConfigure": "Нажмите для настройки",
"connection_modal.step1.recent.empty": "Нет недавних подключений",
"connection_modal.studio.step.chooseType": "Выбрать тип",
"connection_modal.studio.step.configure": "Параметры",
"connection_modal.studio.step.testSave": "Тест и сохранение",
"connection_modal.studio.step.type": "Тип",
"connection_modal.studio.step.params": "Параметры",
"connection_modal.studio.step.save": "Сохранить",
"connection_modal.studio.title.edit": "Изменить подключение {{type}}",
"connection_modal.studio.title.connection": "Подключение {{type}}",
"connection_modal.studio.progress": "Ход настройки подключения",
"connection_modal.studio.test.idle": "Не проверено",
"connection_modal.config.advanced.title": "Дополнительно",
"connection_modal.config.advanced.description": "Параметры подключения и расширения",
"connection_modal.config.advanced.empty": "Нет доп. параметров для этого типа",

View File

@@ -8776,6 +8776,16 @@
"connection_modal.step1.categories": "分类",
"connection_modal.step1.clickToConfigure": "单击进入配置表单",
"connection_modal.step1.recent.empty": "暂无连接记录",
"connection_modal.studio.step.chooseType": "选类型",
"connection_modal.studio.step.configure": "配参数",
"connection_modal.studio.step.testSave": "测试保存",
"connection_modal.studio.step.type": "类型",
"connection_modal.studio.step.params": "参数",
"connection_modal.studio.step.save": "保存",
"connection_modal.studio.title.edit": "编辑 {{type}} 连接",
"connection_modal.studio.title.connection": "{{type}} 连接",
"connection_modal.studio.progress": "连接创建进度",
"connection_modal.studio.test.idle": "未测试",
"connection_modal.config.advanced.title": "高级",
"connection_modal.config.advanced.description": "连接参数与类型扩展项",
"connection_modal.config.advanced.empty": "当前类型暂无额外高级参数",

View File

@@ -8776,6 +8776,16 @@
"connection_modal.step1.categories": "分類",
"connection_modal.step1.clickToConfigure": "按一下進入設定表單",
"connection_modal.step1.recent.empty": "尚無連線紀錄",
"connection_modal.studio.step.chooseType": "選類型",
"connection_modal.studio.step.configure": "設參數",
"connection_modal.studio.step.testSave": "測試儲存",
"connection_modal.studio.step.type": "類型",
"connection_modal.studio.step.params": "參數",
"connection_modal.studio.step.save": "儲存",
"connection_modal.studio.title.edit": "編輯 {{type}} 連線",
"connection_modal.studio.title.connection": "{{type}} 連線",
"connection_modal.studio.progress": "連線建立進度",
"connection_modal.studio.test.idle": "未測試",
"connection_modal.config.advanced.title": "進階",
"connection_modal.config.advanced.description": "連線參數與類型擴充項",
"connection_modal.config.advanced.empty": "目前類型沒有額外進階參數",