Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
2 changes: 2 additions & 0 deletions src/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -619,5 +619,7 @@
"Break Off": "中断",
"Please enter a legal domain": "请输入合法的域名",
"External Access Domain": "外部访问域名",
"Failed to load task logs": "任务日志加载失败",
"Retry loading logs": "重新加载日志",
"WorkLoad": "工作负载"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 <Spin className={styles['spin-center']} />;
} else {
return <pre className={styles['log-item-content']}>{logdata}</pre>;
return (
<div className={styles['log-item-content']}>
{logdata && <pre>{logdata}</pre>}
{logError && (
<Alert
type="error"
showIcon
message={t('Failed to load task logs')}
description={logError}
action={
<Button size="small" onClick={onRetry}>
{t('Retry loading logs')}
</Button>
}
/>
)}
</div>
);
}
}
return '';
Expand All @@ -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') {
Expand Down Expand Up @@ -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 (
Expand All @@ -113,6 +137,8 @@ const LogItem = observer((props) => {
<LogItemContent
isExpand={isExpand}
logdata={logdata}
logError={logError}
onRetry={() => logStore.getStepLog(params)}
isLoading={isLoading}
/>
</div>
Expand Down
48 changes: 29 additions & 19 deletions src/stores/log.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { APIVERSION } from 'utils/constants';
export default class LogStore extends BaseStore {
logdata = '';

logError = '';

isExpand = false;

cumulativeSize = 0;
Expand All @@ -33,6 +35,7 @@ export default class LogStore extends BaseStore {
makeObservable(this, {
isExpand: observable,
logdata: observable,
logError: observable,
});
}

Expand All @@ -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 = '';
}
}
82 changes: 82 additions & 0 deletions src/stores/log.test.js
Original file line number Diff line number Diff line change
@@ -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');
});
Loading