You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@devlake.apache.org by kl...@apache.org on 2023/06/19 01:43:25 UTC
[incubator-devlake] branch main updated: fix(config-ui): some bugs for jira dev panel (#5514)
This is an automated email from the ASF dual-hosted git repository.
klesh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git
The following commit(s) were added to refs/heads/main by this push:
new 188df62ec fix(config-ui): some bugs for jira dev panel (#5514)
188df62ec is described below
commit 188df62ec2d162fdc2dc5351b2088e0f6d7a459a
Author: 青湛 <0x...@gmail.com>
AuthorDate: Mon Jun 19 09:43:21 2023 +0800
fix(config-ui): some bugs for jira dev panel (#5514)
---
config-ui/src/plugins/register/jira/api.ts | 15 ++
.../jira/transformation-fields/cross-domain.tsx | 253 ++++-----------------
.../jira/transformation-fields/dev-panel.tsx | 210 +++++++++++++++++
.../jira/transformation-fields/remote-link.tsx | 119 ++++++++++
.../register/jira/transformation-fields/styled.ts | 20 +-
config-ui/src/utils/operator.ts | 2 +-
6 files changed, 409 insertions(+), 210 deletions(-)
diff --git a/config-ui/src/plugins/register/jira/api.ts b/config-ui/src/plugins/register/jira/api.ts
index cf1068b30..07f6fd8ef 100644
--- a/config-ui/src/plugins/register/jira/api.ts
+++ b/config-ui/src/plugins/register/jira/api.ts
@@ -37,3 +37,18 @@ export const getApplicationTypes = (connectionId: ID, query: { key: string }) =>
export const getDevPanelCommits = (connectionId: ID, query: { key: string; applicationType: string }) =>
request(`/plugins/jira/connections/${connectionId}/dev-panel-commits`, { data: query });
+
+export const generateRegex = (pattern: string) =>
+ request('/plugins/jira/generate-regex', {
+ method: 'post',
+ data: { pattern },
+ });
+
+export const applyRegex = (regex: string, urls: string[]) =>
+ request('/plugins/jira/apply-regex', {
+ method: 'post',
+ data: {
+ regex,
+ urls,
+ },
+ });
diff --git a/config-ui/src/plugins/register/jira/transformation-fields/cross-domain.tsx b/config-ui/src/plugins/register/jira/transformation-fields/cross-domain.tsx
index bce16432d..fd8fc243f 100644
--- a/config-ui/src/plugins/register/jira/transformation-fields/cross-domain.tsx
+++ b/config-ui/src/plugins/register/jira/transformation-fields/cross-domain.tsx
@@ -16,15 +16,14 @@
*
*/
-import { useState, useEffect } from 'react';
-import { Radio, Icon, Collapse, InputGroup, Button, Intent, RadioGroup } from '@blueprintjs/core';
+import { useEffect, useState } from 'react';
+import { Radio, Icon, Collapse, Button } from '@blueprintjs/core';
-import { ExternalLink, IconButton, Dialog, FormItem } from '@/components';
+import { ExternalLink } from '@/components';
import JiraIssueTipsImg from '@/images/jira-issue-tips.png';
-import { operator } from '@/utils';
-
-import * as API from '../api';
+import { RemoteLink } from './remote-link';
+import { DevPanel } from './dev-panel';
import * as S from './styled';
interface Props {
@@ -34,113 +33,25 @@ interface Props {
}
export const CrossDomain = ({ connectionId, transformation, setTransformation }: Props) => {
- const [radio, setRadio] = useState<'repo' | 'commitSha'>('repo');
- const [repoTips, setRepoTips] = useState(false);
- const [repoLinks, setRepoLinks] = useState([]);
+ const [radio, setRadio] = useState<'remote-link' | 'dev-panel'>('remote-link');
+ const [showTip, setShowTip] = useState(false);
const [isOpen, setIsOpen] = useState(false);
- const [step, setStep] = useState(1);
- const [issueKey, setIssueKey] = useState('');
- const [searching, setSearching] = useState(false);
- const [applicationTypes, setApplicationTypes] = useState<string[]>([]);
- const [applicationType, setApplicationType] = useState<string>();
- const [operating, setOperating] = useState(false);
- const [devPanelCommits, setDevPanelCommits] = useState<string[]>([]);
useEffect(() => {
- if (transformation.remotelinkCommitShaPattern) {
- setRadio('commitSha');
- }
-
- if (transformation.remotelinkRepoPattern) {
- setRepoLinks(transformation.remotelinkRepoPattern);
- }
- }, [transformation]);
-
- const handleChangeRadio = (r: 'repo' | 'commitSha') => {
- if (r === 'repo') {
- setTransformation({
- ...transformation,
- remotelinkCommitShaPattern: '',
- });
- }
-
- if (r === 'commitSha') {
- setTransformation({
- ...transformation,
- remotelinkRepoPattern: [],
- });
+ if (transformation.applicationType) {
+ setRadio('dev-panel');
+ } else {
+ setRadio('remote-link');
}
+ }, []);
- setRadio(r);
- };
-
- const handleToggleRepoTips = () => setRepoTips(!repoTips);
-
- const handleChangeRepoLinks = (index: number, value: string) => {
- const newValue = repoLinks.map((link, i) => (index === i ? value : link));
+ const handleChangeRadio = (r: 'remote-link' | 'dev-panel') => {
setTransformation({
...transformation,
- remotelinkRepoPattern: newValue,
- });
- };
-
- const handleAddRepoLinks = () => {
- const newValue = [...repoLinks, ''];
- setTransformation({
- ...transformation,
- remotelinkRepoPattern: newValue,
- });
- };
-
- const handleDeleteRepoLinks = (index: number) => {
- const newValue = repoLinks.filter((_, i) => i !== index);
- setTransformation({
- ...transformation,
- remotelinkRepoPattern: newValue,
- });
- };
-
- const handleShowDevPanel = () => setIsOpen(true);
- const handleHideDevPanel = () => setIsOpen(false);
-
- const handleSearch = async () => {
- const [success, res] = await operator(() => API.getApplicationTypes(connectionId, { key: issueKey }), {
- setOperating: setSearching,
- hideToast: true,
+ applicationType: r === 'remote-link' ? '' : transformation.applicationType,
+ remotelinkRepoPattern: [],
});
-
- if (success) {
- setApplicationTypes(res);
- }
- };
-
- const handleSubmit = async () => {
- if (step === 1 && applicationType) {
- const [success, res] = await operator(
- () => API.getDevPanelCommits(connectionId, { key: issueKey, applicationType }),
- {
- setOperating,
- hideToast: true,
- },
- );
-
- if (success) {
- setStep(2);
- setDevPanelCommits(res);
- return;
- }
- }
-
- handleHideDevPanel();
- };
-
- const handleCancel = () => {
- if (step === 1) {
- handleHideDevPanel();
- return;
- }
-
- setStep(1);
+ setRadio(r);
};
return (
@@ -155,129 +66,55 @@ export const CrossDomain = ({ connectionId, transformation, setTransformation }:
</p>
<div className="radio">
<div className="radio-item">
- <Radio checked={radio === 'repo'} onChange={() => handleChangeRadio('repo')} />
+ <Radio checked={radio === 'remote-link'} onChange={() => handleChangeRadio('remote-link')} />
<div className="content">
<h5>Connect Jira issues and commits via Jira issues’ remote links that match the following pattern</h5>
- <p onClick={handleToggleRepoTips}>
+ <p style={{ display: 'flex', alignItems: 'center' }} onClick={() => setShowTip(!showTip)}>
Input pattern(s) to match and parse commits and repo identifiers from issue remote links. See examples{' '}
- <Icon icon={!repoTips ? 'chevron-down' : 'chevron-up'} style={{ marginLeft: 8, cursor: 'pointer' }} />
+ <Icon icon={showTip ? 'chevron-down' : 'chevron-up'} style={{ cursor: 'pointer' }} />
</p>
- <Collapse isOpen={repoTips}>
+ <Collapse isOpen={showTip}>
<img src={JiraIssueTipsImg} width="100%" alt="" />
</Collapse>
- {radio === 'repo' && (
- <>
- {repoLinks.map((link, i) => (
- <div className="input">
- <InputGroup
- key={i}
- placeholder="E.g. https://gitlab.com/{namespace}/{repo_name}/-/commit/{commit_sha}"
- value={link}
- onChange={(e) => handleChangeRepoLinks(i, e.target.value)}
- />
- {repoLinks.length > 1 && (
- <IconButton icon="cross" tooltip="Delete" onClick={() => handleDeleteRepoLinks(i)} />
- )}
- </div>
- ))}
- <Button
- outlined
- intent={Intent.PRIMARY}
- icon="add"
- text="Add a Pattern"
- onClick={() => handleAddRepoLinks()}
- />
- </>
+ {radio === 'remote-link' && (
+ <RemoteLink transformation={transformation} setTransformation={setTransformation} />
)}
</div>
</div>
<div className="radio-item">
- <Radio checked={radio === 'commitSha'} onChange={() => handleChangeRadio('commitSha')} />
+ <Radio checked={radio === 'dev-panel'} onChange={() => handleChangeRadio('dev-panel')} />
<div className="content">
<h5>
Connect Jira issues and commits via{' '}
- <ExternalLink link="Links to: https://support.atlassian.com/jira-software-cloud/docs/view-development-information-for-an-issue/">
+ <ExternalLink link="https://support.atlassian.com/jira-software-cloud/docs/view-development-information-for-an-issue/">
development panel
</ExternalLink>
</h5>
<p>Finish the configuration so DevLake can get your Git data from your Jira development panel.</p>
- {radio === 'commitSha' && <Button text="Configure" onClick={handleShowDevPanel} />}
+ {radio === 'dev-panel' && (
+ <>
+ {transformation.applicationType && (
+ <div className="application">
+ <span>{transformation.applicationType}</span>
+ <span>{transformation.remotelinkRepoPattern[0]?.pattern}</span>
+ </div>
+ )}
+ <Button
+ text={!transformation.applicationType ? 'Configure' : 'Edit Configuration'}
+ onClick={() => setIsOpen(true)}
+ />
+ <DevPanel
+ connectionId={connectionId}
+ transformation={transformation}
+ setTransformation={setTransformation}
+ isOpen={isOpen}
+ onCancel={() => setIsOpen(false)}
+ />
+ </>
+ )}
</div>
</div>
</div>
- <Dialog
- style={{ width: 820 }}
- isOpen={isOpen}
- title="Configure the `Application Type` and `Commit Pattern` by sending request(s) to Jira."
- okText={step === 1 ? 'Next' : 'Save'}
- okDisabled={(step === 1 && !applicationType) || (step === 2 && !transformation.remotelinkCommitShaPattern)}
- okLoading={operating}
- cancelText={step === 2 ? 'Prev' : 'Cancel'}
- onOk={handleSubmit}
- onCancel={handleCancel}
- >
- <S.DialogBody>
- {step === 1 && (
- <>
- <FormItem
- label="Jira Issue Key"
- subLabel="Input any issue key that has connected commit(s) in the development panel"
- required
- >
- <div className="search">
- <InputGroup
- placeholder="Please enter..."
- value={issueKey}
- onChange={(e) => setIssueKey(e.target.value)}
- />
- <Button loading={searching} disabled={!issueKey} text="See Results" onClick={handleSearch} />
- </div>
- </FormItem>
- {applicationTypes.length > 0 && (
- <FormItem label="Application Type" subLabel="Please choose an application type." required>
- <RadioGroup
- selectedValue={applicationType}
- onChange={(e) => setApplicationType((e.target as HTMLInputElement).value)}
- >
- {applicationTypes.map((at) => (
- <Radio key={at} value={at} label={at} />
- ))}
- </RadioGroup>
- </FormItem>
- )}
- </>
- )}
- {step === 2 && (
- <>
- <FormItem label="Jira Issue Key">{issueKey}</FormItem>
- <FormItem label="Application Type">{applicationType}</FormItem>
- <FormItem label="Commit Url Preview" subLabel="The latest five commit(s) associated with the issue.">
- <ul>
- {devPanelCommits.map((commit) => (
- <li key={commit}>{commit}</li>
- ))}
- </ul>
- </FormItem>
- <FormItem
- label="Commit Pattern"
- subLabel={
- <>
- Input pattern(s) to match and parse commits and repo identifiers from above commit URLs. See
- examples
- </>
- }
- required
- >
- <InputGroup
- placeholder="eg. https://gitlab.com/{namespace}/{repo_name}/commit/{commit_sha}"
- value={transformation.remotelinkCommitShaPattern}
- onChange={(e) => setTransformation({ ...transformation, remotelinkCommitShaPattern: e.target.value })}
- />
- </FormItem>
- </>
- )}
- </S.DialogBody>
- </Dialog>
</S.CrossDomain>
);
};
diff --git a/config-ui/src/plugins/register/jira/transformation-fields/dev-panel.tsx b/config-ui/src/plugins/register/jira/transformation-fields/dev-panel.tsx
new file mode 100644
index 000000000..c469fd734
--- /dev/null
+++ b/config-ui/src/plugins/register/jira/transformation-fields/dev-panel.tsx
@@ -0,0 +1,210 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import { useEffect, useState } from 'react';
+import { InputGroup, Button, RadioGroup, Radio, Icon, Collapse } from '@blueprintjs/core';
+
+import { Dialog, FormItem } from '@/components';
+import JiraIssueTipsImg from '@/images/jira-issue-tips.png';
+import { operator } from '@/utils';
+
+import * as API from '../api';
+import * as S from './styled';
+
+interface Props {
+ connectionId: ID;
+ transformation: any;
+ setTransformation: React.Dispatch<React.SetStateAction<any>>;
+ isOpen: boolean;
+ onCancel: () => void;
+}
+
+export const DevPanel = ({ connectionId, transformation, setTransformation, isOpen, onCancel }: Props) => {
+ const [step, setStep] = useState(1);
+ const [issueKey, setIssueKey] = useState('');
+ const [searching, setSearching] = useState(false);
+ const [applicationTypes, setApplicationTypes] = useState<string[]>([]);
+ const [applicationType, setApplicationType] = useState<string>();
+ const [showTip, setShowTip] = useState(false);
+ const [operating, setOperating] = useState(false);
+ const [devPanelCommits, setDevPanelCommits] = useState<string[]>([]);
+ const [pattern, setPattern] = useState('');
+ const [regex, setRegex] = useState('');
+ const [preview, setPreview] = useState([]);
+
+ const getRegex = async () => {
+ if (!pattern) return;
+
+ const [success, res] = await operator(
+ async () => {
+ const { regex } = await API.generateRegex(pattern);
+ const preview = await API.applyRegex(regex, devPanelCommits);
+ return {
+ regex,
+ preview,
+ };
+ },
+ {
+ setOperating,
+ hideToast: true,
+ },
+ );
+
+ if (success) {
+ setRegex(res.regex);
+ setPreview(res.preview);
+ setTransformation({
+ ...transformation,
+ applicationType,
+ remotelinkRepoPattern: [{ pattern, regex: res.regex }],
+ });
+ }
+ };
+
+ useEffect(() => {
+ const timer = setTimeout(getRegex, 500);
+ return () => clearTimeout(timer);
+ }, [pattern]);
+
+ const handleSearch = async () => {
+ const [success, res] = await operator(() => API.getApplicationTypes(connectionId, { key: issueKey }), {
+ setOperating: setSearching,
+ hideToast: true,
+ });
+
+ if (success) {
+ setApplicationTypes(res);
+ setApplicationType(res[0]);
+ }
+ };
+
+ const handleSubmit = async () => {
+ if (step === 1 && applicationType) {
+ const [success, res] = await operator(
+ () => API.getDevPanelCommits(connectionId, { key: issueKey, applicationType }),
+ {
+ setOperating,
+ hideToast: true,
+ },
+ );
+
+ if (success) {
+ setStep(2);
+ setDevPanelCommits(res);
+ return;
+ }
+ }
+
+ onCancel();
+ };
+
+ const handleCancel = () => {
+ if (step === 1) {
+ onCancel();
+ return;
+ }
+
+ setStep(1);
+ };
+
+ return (
+ <Dialog
+ style={{ width: 820 }}
+ isOpen={isOpen}
+ title="Configure the `Application Type` and `Commit Pattern` by sending request(s) to Jira."
+ okText={step === 1 ? 'Next' : 'Save'}
+ okDisabled={(step === 1 && !applicationType) || (step === 2 && (!pattern || !regex))}
+ okLoading={operating}
+ cancelText={step === 2 ? 'Prev' : 'Cancel'}
+ onOk={handleSubmit}
+ onCancel={handleCancel}
+ >
+ <S.DialogBody>
+ {step === 1 && (
+ <>
+ <FormItem
+ label="Jira Issue Key"
+ subLabel="Input any issue key that has connected commit(s) in the development panel"
+ required
+ >
+ <div className="search">
+ <InputGroup
+ placeholder="Please enter..."
+ value={issueKey}
+ onChange={(e) => setIssueKey(e.target.value)}
+ />
+ <Button loading={searching} disabled={!issueKey} text="See Results" onClick={handleSearch} />
+ </div>
+ </FormItem>
+ {applicationTypes.length > 0 && (
+ <FormItem label="Application Type" subLabel="Please choose an application type." required>
+ <RadioGroup
+ selectedValue={applicationType}
+ onChange={(e) => setApplicationType((e.target as HTMLInputElement).value)}
+ >
+ {applicationTypes.map((at) => (
+ <Radio key={at} value={at} label={at} />
+ ))}
+ </RadioGroup>
+ </FormItem>
+ )}
+ </>
+ )}
+ {step === 2 && (
+ <>
+ <FormItem label="Jira Issue Key">{issueKey}</FormItem>
+ <FormItem label="Application Type">{applicationType}</FormItem>
+ <FormItem label="Commit Url Preview" subLabel="The latest five commit(s) associated with the issue.">
+ <ul>
+ {devPanelCommits.map((commit) => (
+ <li key={commit}>{commit}</li>
+ ))}
+ </ul>
+ </FormItem>
+ <FormItem
+ label="Commit Pattern"
+ subLabel={
+ <>
+ <p style={{ display: 'flex', alignItems: 'center' }} onClick={() => setShowTip(!showTip)}>
+ Input pattern(s) to match and parse commits and repo identifiers from above commit URLs. See
+ examples <Icon icon={showTip ? 'chevron-down' : 'chevron-up'} style={{ cursor: 'pointer' }} />
+ </p>
+ <Collapse isOpen={showTip}>
+ <img src={JiraIssueTipsImg} width="100%" alt="" />
+ </Collapse>
+ </>
+ }
+ required
+ >
+ <InputGroup
+ placeholder="eg. https://gitlab.com/{namespace}/{repo_name}/commit/{commit_sha}"
+ value={pattern}
+ onChange={(e) => setPattern(e.target.value)}
+ />
+ </FormItem>
+ <FormItem label="Configuration Results Preview">
+ <code>
+ <pre>{JSON.stringify(preview, null, ' ')}</pre>
+ </code>
+ </FormItem>
+ </>
+ )}
+ </S.DialogBody>
+ </Dialog>
+ );
+};
diff --git a/config-ui/src/plugins/register/jira/transformation-fields/remote-link.tsx b/config-ui/src/plugins/register/jira/transformation-fields/remote-link.tsx
new file mode 100644
index 000000000..a9117381d
--- /dev/null
+++ b/config-ui/src/plugins/register/jira/transformation-fields/remote-link.tsx
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import { useEffect, useState } from 'react';
+import { InputGroup, Button, Intent } from '@blueprintjs/core';
+
+import { IconButton } from '@/components';
+import { operator } from '@/utils';
+
+import * as API from '../api';
+import * as S from './styled';
+
+interface Props {
+ transformation: any;
+ setTransformation: React.Dispatch<React.SetStateAction<any>>;
+}
+
+export const RemoteLink = ({ transformation, setTransformation }: Props) => {
+ const [links, setLinks] = useState<Array<{ pattern: string; regex: string }>>(
+ transformation.remotelinkRepoPattern ?? [],
+ );
+ const [generating, setGenerating] = useState(false);
+
+ const generateRegex = async (pattern: string) => {
+ try {
+ const res = await API.generateRegex(pattern);
+ return { pattern, regex: res.regex };
+ } catch {
+ return { pattern, regex: '' };
+ }
+ };
+
+ const getRegex = async () => {
+ const [success, res] = await operator(
+ () =>
+ Promise.all(
+ links.map((link) => {
+ if (!link.pattern || link.regex) {
+ return link;
+ }
+ return generateRegex(link.pattern);
+ }),
+ ),
+ {
+ setOperating: setGenerating,
+ hideToast: true,
+ },
+ );
+
+ if (success) {
+ setTransformation({
+ ...transformation,
+ remotelinkRepoPattern: res.filter((it: any) => it.regex),
+ });
+ }
+ };
+
+ useEffect(() => {
+ const timer = setTimeout(getRegex, 1000);
+ return () => clearTimeout(timer);
+ }, [links]);
+
+ const handleChangeLinks = (index: number, value: string) => {
+ const newValue = links.map((link, i) => (index === i ? { pattern: value, regex: link.regex } : link));
+ setLinks(newValue);
+ };
+
+ const handleAddLink = () => {
+ const newValue = [...links, { pattern: '', regex: '' }];
+ setLinks(newValue);
+ };
+
+ const handleDeleteLink = (index: number) => {
+ const newValue = links.filter((_, i) => i !== index);
+ setLinks(newValue);
+ };
+
+ return (
+ <S.RemoteLinkWrapper>
+ {links.map((link, i) => (
+ <div key={i} className="input">
+ <InputGroup
+ key={i}
+ placeholder="E.g. https://gitlab.com/{namespace}/{repo_name}/-/commit/{commit_sha}"
+ value={link.pattern}
+ onChange={(e) => handleChangeLinks(i, e.target.value)}
+ // onBlur={() => handleGenerateRegex(i, link.pattern)}
+ />
+ {links.length > 1 && (
+ <IconButton disabled={generating} icon="cross" tooltip="Delete" onClick={() => handleDeleteLink(i)} />
+ )}
+ </div>
+ ))}
+ <Button
+ outlined
+ disabled={generating}
+ intent={Intent.PRIMARY}
+ icon="add"
+ text="Add a Pattern"
+ onClick={() => handleAddLink()}
+ />
+ </S.RemoteLinkWrapper>
+ );
+};
diff --git a/config-ui/src/plugins/register/jira/transformation-fields/styled.ts b/config-ui/src/plugins/register/jira/transformation-fields/styled.ts
index 2ea1165bb..0e575dd8d 100644
--- a/config-ui/src/plugins/register/jira/transformation-fields/styled.ts
+++ b/config-ui/src/plugins/register/jira/transformation-fields/styled.ts
@@ -27,6 +27,21 @@ export const CrossDomain = styled.div`
margin-top: 24px;
}
+ .application {
+ margin-bottom: 8px;
+
+ span {
+ padding: 4px 8px;
+ background-color: #efefef;
+ }
+
+ span + span {
+ margin-left: 8px;
+ }
+ }
+`;
+
+export const RemoteLinkWrapper = styled.div`
.input {
display: flex;
align-items: center;
@@ -40,8 +55,11 @@ export const CrossDomain = styled.div`
`;
export const DialogBody = styled.div`
- ul {
+ ul,
+ pre {
padding: 8px 16px;
+ max-height: 240px;
+ overflow-y: auto;
background: #efefef;
}
diff --git a/config-ui/src/utils/operator.ts b/config-ui/src/utils/operator.ts
index d1aeb1d4c..45fb15b91 100644
--- a/config-ui/src/utils/operator.ts
+++ b/config-ui/src/utils/operator.ts
@@ -34,7 +34,7 @@ export type OperateConfig = {
* @param config.formatReason -> Show the reason for the failure
* @returns
*/
-export const operator = async <T>(request: () => Promise<T>, config?: OperateConfig) => {
+export const operator = async <T>(request: () => Promise<T>, config?: OperateConfig): Promise<[boolean, any?]> => {
const { setOperating, formatMessage, formatReason } = config || {};
try {