mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-10 00:33:28 +08:00
✨ feat(connection-modal): 重构连接弹窗 Studio 交互 (#740)
2026年7月27日 17:05:54 提交
This commit is contained in:
@@ -1 +1 @@
|
||||
83b4b7d166ed9172beba2d6236382c0b
|
||||
84c1262b11d52116d262ce60a8ec5676
|
||||
@@ -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 */
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
})()}
|
||||
|
||||
@@ -8783,6 +8783,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",
|
||||
|
||||
@@ -8783,6 +8783,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",
|
||||
|
||||
@@ -8783,6 +8783,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": "このタイプに詳細設定はありません",
|
||||
|
||||
@@ -8783,6 +8783,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": "Нет доп. параметров для этого типа",
|
||||
|
||||
@@ -8783,6 +8783,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": "当前类型暂无额外高级参数",
|
||||
|
||||
@@ -8783,6 +8783,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": "目前類型沒有額外進階參數",
|
||||
|
||||
Reference in New Issue
Block a user