Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add 'Manage' tab to Digital Twins page preview #906

Closed
Prev Previous commit
Next Next commit
manual rebase in progress
  • Loading branch information
prasadtalasila committed Sep 9, 2024
commit 25158f301dd940a279c2a210b8d25bb084010951
2 changes: 1 addition & 1 deletion client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@into-cps-association/dtaas-web",
"version": "0.4.0",
"version": "0.4.2",
"description": "Web client for Digital Twin as a Service (DTaaS)",
"main": "index.tsx",
"author": "prasadtalasila <prasad.talasila@gmail.com> (http://prasad.talasila.in/)",
Expand Down
39 changes: 39 additions & 0 deletions client/src/route/digitaltwins/DigitalTwinCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from 'react';
import { Card, CardContent, Typography } from '@mui/material';

const DigitalTwinCard: React.FC<{
name: string;
description: string;
buttons: React.ReactNode;
}> = (props) => (
<Card
style={{
width: 300,
height: 200,
margin: '20px',
display: 'flex',
flexDirection: 'column',
position: 'relative',
}}
>
<CardContent style={{ flex: 1, overflow: 'hidden' }}>
<Typography variant="h5" component="div" gutterBottom>
{props.name}
</Typography>
<div
style={{
maxHeight: '85px',
overflowY: 'auto',
paddingRight: '8px',
}}
>
<Typography variant="body2" color="textSecondary">
{props.description}
</Typography>
</div>
</CardContent>
{props.buttons}
</Card>
);

export default DigitalTwinCard;
63 changes: 63 additions & 0 deletions client/src/route/digitaltwins/DigitalTwinsPreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React, { useState, useEffect } from 'react';
import { Typography } from '@mui/material';
import Layout from 'page/Layout';
import TabComponent from 'components/tab/TabComponent';
import { TabData } from 'components/tab/subcomponents/TabRender';
import { Asset } from 'components/asset/Asset';
import AssetBoard from 'components/asset/AssetBoard';
import tabs from './DigitalTwinTabData';
import GitlabService from './GitlabService';

const createDTTab = (
subfolders: Asset[],
error: string | null,
gitlabInstance: GitlabService,
): TabData[] => tabs
.filter((tab) => tab.label === 'Execute')
.map((tab) => ({
label: tab.label,
body: (
<>
<Typography variant="body1">{tab.body}</Typography>
<AssetBoard
subfolders={subfolders}
gitlabInstance={gitlabInstance.getInstance()}
error={error}
/>
</>
),
}));

const fetchSubfolders = async (
gitlabService: GitlabService,
setSubfolders: React.Dispatch<React.SetStateAction<Asset[]>>,
setError: React.Dispatch<React.SetStateAction<string | null>>,
) => {
const result = await gitlabService.getSubfolders();
if (typeof result === 'string') {
setError(result);
} else {
setSubfolders(result);
}
};

function DTContent() {
const [subfolders, setSubfolders] = useState<Asset[]>([]);
const [error, setError] = useState<string | null>(null);
const gitlabService = new GitlabService();

useEffect(() => {
fetchSubfolders(gitlabService, setSubfolders, setError);
}, [gitlabService]);

return (
<Layout>
<TabComponent
assetType={createDTTab(subfolders, error, gitlabService)}
scope={[]}
/>
</Layout>
);
}

export default DTContent;
280 changes: 280 additions & 0 deletions client/src/route/digitaltwins/ExecuteDigitalTwin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
import React, { useState, useEffect } from 'react';
import {
CardActions,
Typography,
Button,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Snackbar,
Alert,
AlertColor,
CircularProgress,
} from '@mui/material';
import { GitlabInstance } from 'util/gitlab';
import DigitalTwin from 'util/gitlabDigitalTwin';
import stripAnsi from 'strip-ansi';
import { getAuthority } from 'util/envUtil';
import DigitalTwinCard from './DigitalTwinCard';

const formatName = (name: string) =>
name.replace(/-/g, ' ').replace(/^./, (char) => char.toUpperCase());

const ExecuteDigitalTwin: React.FC<{ name: string }> = (props) => {
const [gitlabInstance] = useState<GitlabInstance>(
new GitlabInstance(
sessionStorage.getItem('username') || '',
getAuthority(),
sessionStorage.getItem('access_token') || '',
),
);
const [digitalTwin, setDigitalTwin] = useState<DigitalTwin | null>(null);
const [description, setDescription] = useState<string>('');
const [executionStatus, setExecutionStatus] = useState<string | null>(null);
const [snackbarOpen, setSnackbarOpen] = useState(false);
const [snackbarMessage, setSnackbarMessage] = useState('');
const [snackbarSeverity, setSnackbarSeverity] =
useState<AlertColor>('success');
const [jobLogs, setJobLogs] = useState<{ jobName: string; log: string }[]>(
[],
);
const [showLog, setShowLog] = useState(false);
const [pipelineCompleted, setPipelineCompleted] = useState(false);
const [pipelineLoading, setPipelineLoading] = useState(false);
const [buttonText, setButtonText] = useState('Start');
const [executionCount, setExecutionCount] = useState(0);

const initialize = async () => {
await gitlabInstance.init();
const dt = new DigitalTwin(props.name, gitlabInstance);
await dt.init();
setDigitalTwin(dt);
console.log('Digital twin:', dt);
setDescription(dt.description);
};

useEffect(() => {
initialize();
}, []);

useEffect(() => {
if (executionStatus) {
setSnackbarMessage(
`Execution ${executionStatus} for ${formatName(props.name)} (Run #${executionCount})`,
);
setSnackbarSeverity(executionStatus === 'success' ? 'success' : 'error');
setSnackbarOpen(true);
}
}, [executionStatus, executionCount, props.name]);

const checkSecondPipelineStatus = async (
projectId: number,
pipelineId: number,
) => {
const pipelineStatus = gitlabInstance
? await gitlabInstance.getPipelineStatus(projectId, pipelineId)
: null;
if (pipelineStatus === 'success' || pipelineStatus === 'failed') {
const pipelineIdJobs = pipelineId;
setJobLogs(await fetchJobLogs(projectId, pipelineIdJobs));
setPipelineCompleted(true);
setPipelineLoading(false);
setButtonText('Start');
} else {
setTimeout(() => checkSecondPipelineStatus(projectId, pipelineId), 5000);
}
};

const checkFirstPipelineStatus = async (
projectId: number,
pipelineId: number,
) => {
const pipelineStatus = gitlabInstance
? await gitlabInstance.getPipelineStatus(projectId, pipelineId)
: null;
if (pipelineStatus === 'success' || pipelineStatus === 'failed') {
checkSecondPipelineStatus(projectId, pipelineId + 1);
} else {
setTimeout(() => checkFirstPipelineStatus(projectId, pipelineId), 5000);
}
};

const fetchJobLogs = async (projectId: number, pipelineId: number) => {
const jobs = gitlabInstance
? await gitlabInstance.getPipelineJobs(projectId, pipelineId)
: [];
console.log('gitlabinstance job', gitlabInstance);
console.log(jobs);
const logPromises = jobs.map(async (job) => {
let log = gitlabInstance
? await gitlabInstance.getJobTrace(projectId, job.id)
: '';
console.log('Log in fetchJobLogs:', log);
if (typeof log === 'string') {
log = stripAnsi(log)
.split('\n')
.map((line) =>
line
.replace(/section_start:\d+:[^A-Z]*/, '')
.replace(/section_end:\d+:[^A-Z]*/, ''),
)
.join('\n');
}
return { jobName: job.name, log };
});
return (await Promise.all(logPromises)).reverse();
};

const handleStart = async () => {
if (digitalTwin) {
if (buttonText === 'Start') {
setButtonText('Stop');
setJobLogs([]);
setPipelineCompleted(false);
setPipelineLoading(true);
const pipelineId = await digitalTwin.execute();
setExecutionStatus(digitalTwin.executionStatus());
setExecutionCount((prevCount) => prevCount + 1);

if (
gitlabInstance &&
gitlabInstance.projectId &&
digitalTwin?.pipelineId &&
pipelineId
) {
checkFirstPipelineStatus(gitlabInstance.projectId, pipelineId);
}
} else {
setButtonText('Start');
}
}
};

const handleStop = async () => {
if (digitalTwin) {
try {
if (
gitlabInstance &&
gitlabInstance.projectId &&
digitalTwin.pipelineId
) {
await digitalTwin.stop(
gitlabInstance.projectId,
digitalTwin.pipelineId,
);
}
setSnackbarMessage(
`${formatName(props.name)} (Run #${executionCount}) execution stopped successfully`,
);
setSnackbarSeverity('success');
} catch (error) {
setSnackbarMessage(
`Failed to stop ${formatName(props.name)} (Run #${executionCount}) execution`,
);
setSnackbarSeverity('error');
} finally {
setSnackbarOpen(true);
setButtonText('Start');
setPipelineCompleted(true);
setPipelineLoading(false);
}
}
};

const handleButtonClick = () => {
if (buttonText === 'Start') {
handleStart();
} else {
handleStop();
}
};

const handleToggleLog = () => {
setShowLog((prev) => !prev);
};

const handleCloseLog = () => {
setShowLog(false);
};

const handleCloseSnackbar = () => {
setSnackbarOpen(false);
};

return (
<>
{description ? (
<DigitalTwinCard
name={formatName(props.name)}
description={description}
buttons={
<CardActions
style={{
padding: '16px',
marginTop: 'auto',
display: 'flex',
justifyContent: 'flex-end',
}}
>
<Button
size="small"
color="primary"
onClick={handleButtonClick}
style={{ flexShrink: 0 }}
>
{buttonText}
</Button>
<Button
size="small"
color="primary"
onClick={handleToggleLog}
disabled={!pipelineCompleted}
>
Log
</Button>
{pipelineLoading ? <CircularProgress size={24} /> : null}
</CardActions>
}
/>
) : (
<Typography>Loading...</Typography>
)}

<Dialog open={showLog} onClose={handleCloseLog} maxWidth="md">
<DialogTitle>{`${formatName(props.name)} - log (run #${executionCount})`}</DialogTitle>
<DialogContent dividers>
{jobLogs.length > 0 ? (
jobLogs.map((jobLog, index) => (
<div key={index} style={{ marginBottom: '16px' }}>
<Typography variant="h6">{jobLog.jobName}</Typography>
<Typography variant="body2" style={{ whiteSpace: 'pre-wrap' }}>
{jobLog.log}
</Typography>
</div>
))
) : (
<Typography variant="body2">No logs available</Typography>
)}
</DialogContent>
<DialogActions>
<Button onClick={handleCloseLog} color="primary">
Close
</Button>
</DialogActions>
</Dialog>

<Snackbar
open={snackbarOpen}
autoHideDuration={6000}
onClose={handleCloseSnackbar}
>
<Alert onClose={handleCloseSnackbar} severity={snackbarSeverity}>
{snackbarMessage}
</Alert>
</Snackbar>
</>
);
};

export default ExecuteDigitalTwin;
Loading