diff --git a/src/locales/en.json b/src/locales/en.json
index 1a17a35..16ed9b2 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -619,5 +619,7 @@
"Break Off": "Break Off",
"Please enter a legal domain": "Please enter a legal domain",
"External Access Domain": "External Access Domain",
+ "Failed to load task logs": "Failed to load task logs",
+ "Retry loading logs": "Retry loading logs",
"WorkLoad": "WorkLoad"
}
diff --git a/src/locales/zh.json b/src/locales/zh.json
index 0ba1519..452663a 100644
--- a/src/locales/zh.json
+++ b/src/locales/zh.json
@@ -619,5 +619,7 @@
"Break Off": "中断",
"Please enter a legal domain": "请输入合法的域名",
"External Access Domain": "外部访问域名",
+ "Failed to load task logs": "任务日志加载失败",
+ "Retry loading logs": "重新加载日志",
"WorkLoad": "工作负载"
}
diff --git a/src/pages/cluster/containers/Cluster/Detail/Operation/actions/PipelineLogDialog/RightLogContent.jsx b/src/pages/cluster/containers/Cluster/Detail/Operation/actions/PipelineLogDialog/RightLogContent.jsx
index a21d82b..35abb3a 100644
--- a/src/pages/cluster/containers/Cluster/Detail/Operation/actions/PipelineLogDialog/RightLogContent.jsx
+++ b/src/pages/cluster/containers/Cluster/Detail/Operation/actions/PipelineLogDialog/RightLogContent.jsx
@@ -22,7 +22,7 @@ import {
CheckCircleFilled,
CloseCircleFilled,
} from '@ant-design/icons';
-import { Spin } from 'antd';
+import { Alert, Button, Spin } from 'antd';
import { observer } from 'mobx-react';
import styles from './index.less';
import { useRootStore } from 'stores';
@@ -31,12 +31,29 @@ import { formatSeconds } from 'utils';
import { useInterval } from 'hooks';
function LogItemContent(props) {
- const { isExpand, isLoading, logdata } = props;
+ const { isExpand, isLoading, logdata, logError, onRetry } = props;
if (isExpand) {
if (isLoading) {
return ;
} else {
- return
{logdata};
+ return (
+
+ {logdata &&
{logdata}}
+ {logError && (
+
+ {t('Retry loading logs')}
+
+ }
+ />
+ )}
+
+ );
}
}
return '';
@@ -50,8 +67,14 @@ const LogItem = observer((props) => {
nodeStatus
);
logStore.isStepFinished = taskFinished;
- const { logdata, isExpand, isLoading, cumulativeSize, isStepFinished } =
- logStore;
+ const {
+ logdata,
+ logError,
+ isExpand,
+ isLoading,
+ cumulativeSize,
+ isStepFinished,
+ } = logStore;
const stateIcons = (errIgnore, status) => {
if (status === 'Succeeded') {
@@ -93,11 +116,12 @@ const LogItem = observer((props) => {
if (!taskName) return;
if (!isExpand) {
- logStore.getStepLog(params);
+ logStore.isExpand = true;
+ await logStore.getStepLog(params);
} else {
logStore.logReset();
+ logStore.isExpand = false;
}
- logStore.isExpand = !isExpand;
};
return (
@@ -113,6 +137,8 @@ const LogItem = observer((props) => {
logStore.getStepLog(params)}
isLoading={isLoading}
/>
diff --git a/src/stores/log.js b/src/stores/log.js
index eaf2187..6275868 100644
--- a/src/stores/log.js
+++ b/src/stores/log.js
@@ -21,6 +21,8 @@ import { APIVERSION } from 'utils/constants';
export default class LogStore extends BaseStore {
logdata = '';
+ logError = '';
+
isExpand = false;
cumulativeSize = 0;
@@ -33,6 +35,7 @@ export default class LogStore extends BaseStore {
makeObservable(this, {
isExpand: observable,
logdata: observable,
+ logError: observable,
});
}
@@ -42,36 +45,43 @@ export default class LogStore extends BaseStore {
return;
}
- const res = await request.get(
- `${APIVERSION.operations}/operationtasks/${encodeURIComponent(
- params.taskName
- )}/logs`,
- { offset: params.offset || 0 }
- );
- const deliverySize = res.deliverySize || 0;
- this.cumulativeSize += deliverySize;
- this.logdata += res.content || '';
+ this.logError = '';
+ try {
+ const res = await request.get(
+ `${APIVERSION.operations}/operationtasks/${encodeURIComponent(
+ params.taskName
+ )}/logs`,
+ { offset: params.offset || 0 }
+ );
+ const deliverySize = res.deliverySize || 0;
+ this.cumulativeSize += deliverySize;
+ this.logdata += res.content || '';
- if (
- this.isStepFinished &&
- deliverySize > 0 &&
- this.cumulativeSize < res.logSize
- ) {
- params = { ...params, offset: this.cumulativeSize };
- await this.fetchStepLog(params);
+ if (
+ this.isStepFinished &&
+ deliverySize > 0 &&
+ this.cumulativeSize < res.logSize
+ ) {
+ params = { ...params, offset: this.cumulativeSize };
+ await this.fetchStepLog(params);
+ }
+ } catch (error) {
+ this.logError =
+ error?.reason || error?.message || t('Failed to load task logs');
+ } finally {
+ this.isExpand = true;
+ this.isLoading = false;
}
- this.isExpand = true;
- this.isLoading = false;
}
async getStepLog(params) {
this.isLoading = true;
await this.fetchStepLog(params);
- this.isLoading = false;
}
logReset() {
this.cumulativeSize = 0;
this.logdata = '';
+ this.logError = '';
}
}
diff --git a/src/stores/log.test.js b/src/stores/log.test.js
new file mode 100644
index 0000000..5317f91
--- /dev/null
+++ b/src/stores/log.test.js
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2021 KubeClipper Authors.
+ *
+ * Licensed 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 LogStore from './log';
+
+jest.mock('utils/object.mapper', () => ({}));
+
+beforeEach(() => {
+ global.request = { get: jest.fn() };
+});
+
+afterEach(() => {
+ delete global.request;
+});
+
+it('ends loading and exposes an error when the initial log request fails', async () => {
+ const store = new LogStore();
+ request.get.mockRejectedValue({
+ reason: 'agent Task log endpoint is unavailable',
+ });
+
+ await store.fetchStepLog({ taskName: 'task-1' });
+
+ expect(store.isLoading).toBe(false);
+ expect(store.isExpand).toBe(true);
+ expect(store.logError).toBe('agent Task log endpoint is unavailable');
+ expect(store.logdata).toBe('');
+ expect(store.cumulativeSize).toBe(0);
+});
+
+it('retries from the last received offset without losing or duplicating logs', async () => {
+ const store = new LogStore();
+ store.isStepFinished = true;
+ request.get
+ .mockResolvedValueOnce({ content: 'first', deliverySize: 5, logSize: 11 })
+ .mockRejectedValueOnce(new Error('connection lost'));
+
+ await store.getStepLog({ taskName: 'task-1' });
+ expect(store.isLoading).toBe(false);
+ expect(store.logError).toBe('connection lost');
+ expect(store.logdata).toBe('first');
+ expect(store.cumulativeSize).toBe(5);
+
+ request.get.mockResolvedValueOnce({
+ content: 'second',
+ deliverySize: 6,
+ logSize: 11,
+ });
+ await store.getStepLog({ taskName: 'task-1', offset: store.cumulativeSize });
+
+ expect(request.get).toHaveBeenLastCalledWith(
+ 'api/operations.kubeclipper.io/v1alpha1/operationtasks/task-1/logs',
+ { offset: 5 }
+ );
+ expect(store.logError).toBe('');
+ expect(store.isLoading).toBe(false);
+ expect(store.logdata).toBe('firstsecond');
+ expect(store.cumulativeSize).toBe(11);
+});
+
+it('ends loading and allows retry when expanding a failed log request', async () => {
+ const store = new LogStore();
+ request.get.mockRejectedValue(new Error('request failed'));
+
+ await store.getStepLog({ taskName: 'task-1' });
+
+ expect(store.isLoading).toBe(false);
+ expect(store.logError).toBe('request failed');
+});