😎
sample code
npm create vite@latest workflow-prototype -- --template react-ts
cd workflow-prototype
npm install @mui/material @emotion/react @emotion/styled @mui/icons-material
npm run dev
workflow-prototype/
├── src/
│ ├── App.tsx (このファイルを編集)
│ ├── WorkflowDisplay.tsx (新規作成)
│ └── main.tsx (そのまま)
└── package.json
// WorkflowDisplay.tsx
import React, { useState, useEffect } from 'react';
import {
Stepper,
Step,
StepLabel,
Button,
Paper,
Typography,
Box,
Chip,
Card,
CardContent,
LinearProgress,
Grid
} from '@mui/material';
import {
PlayArrow,
CheckCircle,
Schedule,
Error,
Refresh,
Stop
} from '@mui/icons-material';
interface WorkflowStep {
id: string;
name: string;
status: 'completed' | 'running' | 'pending' | 'error';
description: string;
}
const WorkflowDisplay: React.FC = () => {
const [activeStep, setActiveStep] = useState(1);
const [isPolling, setIsPolling] = useState(false);
// サンプルワークフローデータ(実際の画像に基づく)
const workflowSteps: WorkflowStep[] = [
{
id: 'prep',
name: '事前作業',
status: 'completed',
description: 'システム稼働確認、必要ツール準備完了'
},
{
id: 'isolation',
name: '切り離し',
status: 'running',
description: 'ネットワークからの切り離し実行中'
},
{
id: 'version-up',
name: 'Ver.Up',
status: 'pending',
description: 'システムバージョンアップ待機中'
},
{
id: 'integration',
name: '組み込み',
status: 'pending',
description: 'ネットワークへの組み込み待機中'
},
{
id: 'verification',
name: '稼働確認',
status: 'pending',
description: '正常稼働の確認テスト待機中'
},
{
id: 'completion',
name: '完了報告',
status: 'pending',
description: '作業完了の最終報告待機中'
}
];
// ステータスアイコンの取得
const getStatusIcon = (status: WorkflowStep['status']) => {
switch (status) {
case 'completed':
return <CheckCircle sx={{ color: 'success.main', fontSize: 28 }} />;
case 'running':
return (
<PlayArrow
sx={{
color: 'primary.main',
fontSize: 28,
animation: 'pulse 2s infinite'
}}
/>
);
case 'error':
return <Error sx={{ color: 'error.main', fontSize: 28 }} />;
default:
return <Schedule sx={{ color: 'grey.400', fontSize: 28 }} />;
}
};
// ステータスカラーの取得
const getStatusColor = (status: WorkflowStep['status']) => {
switch (status) {
case 'completed':
return 'success';
case 'running':
return 'primary';
case 'error':
return 'error';
default:
return 'default';
}
};
// ポーリング機能のシミュレーション(実際のAPIに置き換え)
useEffect(() => {
let interval: NodeJS.Timeout;
if (isPolling) {
interval = setInterval(() => {
setActiveStep((prev) => {
if (prev < workflowSteps.length - 1) {
// 実際の実装では、ここでAPIからワークフロー状態を取得
return prev + 1;
} else {
setIsPolling(false);
return prev;
}
});
}, 3000); // 3秒間隔(実際は5秒推奨)
}
return () => clearInterval(interval);
}, [isPolling, workflowSteps.length]);
// 画像の複雑なレイアウトを再現
const ComplexWorkflow = () => (
<Paper elevation={3} sx={{ p: 3, mb: 3 }}>
<Typography variant="h6" gutterBottom align="center">
作業ワークフロー表示
</Typography>
{/* 上部ステータス表示 */}
<Box sx={{ display: 'flex', justifyContent: 'center', mb: 2, gap: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<CheckCircle sx={{ color: 'success.main' }} />
<Typography variant="body2">完了</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<PlayArrow sx={{ color: 'primary.main' }} />
<Typography variant="body2">実行中</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<CheckCircle sx={{ color: 'success.main' }} />
<Typography variant="body2">完了</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Error sx={{ color: 'error.main' }} />
<Typography variant="body2">異常</Typography>
</Box>
</Box>
{/* メインフロー表示 */}
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', flexWrap: 'wrap', gap: 2 }}>
{workflowSteps.map((step, index) => (
<React.Fragment key={step.id}>
<Card
elevation={step.status === 'running' ? 8 : 2}
sx={{
minWidth: 140,
minHeight: 100,
border: step.status === 'running' ? '3px solid' : '1px solid',
borderColor: step.status === 'running' ? 'primary.main' : 'grey.300',
bgcolor: step.status === 'completed' ? 'success.light' :
step.status === 'error' ? 'error.light' : 'background.paper',
position: 'relative',
overflow: 'visible'
}}
>
<CardContent sx={{ textAlign: 'center', p: 2 }}>
<Box sx={{ mb: 1 }}>
{getStatusIcon(step.status)}
</Box>
<Typography variant="body2" fontWeight="bold" gutterBottom>
{step.name}
</Typography>
<Chip
label={step.status}
size="small"
color={getStatusColor(step.status)}
/>
{/* 実行中の場合はプログレスバー表示 */}
{step.status === 'running' && (
<Box sx={{ mt: 2 }}>
<LinearProgress />
</Box>
)}
</CardContent>
</Card>
{/* 矢印の表示(SVGカスタム) */}
{index < workflowSteps.length - 1 && (
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<svg width="40" height="24" viewBox="0 0 40 24">
<defs>
<marker id="arrowhead" markerWidth="10" markerHeight="7"
refX="10" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7"
fill={step.status === 'completed' ? '#4caf50' : '#9e9e9e'} />
</marker>
</defs>
<line x1="0" y1="12" x2="30" y2="12"
stroke={step.status === 'completed' ? '#4caf50' : '#9e9e9e'}
strokeWidth="2"
markerEnd="url(#arrowhead)" />
</svg>
</Box>
)}
</React.Fragment>
))}
</Box>
</Paper>
);
// 従来のMUI Stepperレイアウト
const StandardStepper = () => (
<Paper elevation={2} sx={{ p: 3, mb: 3 }}>
<Typography variant="h6" gutterBottom>
標準MUIステッパー
</Typography>
<Stepper activeStep={activeStep} alternativeLabel>
{workflowSteps.map((step) => (
<Step key={step.id}>
<StepLabel
StepIconComponent={() => getStatusIcon(step.status)}
sx={{
'& .MuiStepLabel-label': {
fontSize: '0.875rem',
fontWeight: step.status === 'running' ? 'bold' : 'normal'
}
}}
>
{step.name}
</StepLabel>
</Step>
))}
</Stepper>
</Paper>
);
// 詳細情報表示(実際のシステムで必要な情報)
const DetailView = () => (
<Paper elevation={2} sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom>
ステップ詳細情報
</Typography>
<Grid container spacing={2}>
{workflowSteps.map((step, index) => (
<Grid item xs={12} md={6} key={step.id}>
<Card
variant={index === activeStep ? "outlined" : "elevation"}
sx={{
bgcolor: index === activeStep ? 'action.selected' : 'background.paper',
border: index === activeStep ? '2px solid' : undefined,
borderColor: index === activeStep ? 'primary.main' : undefined
}}
>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 1 }}>
{getStatusIcon(step.status)}
<Typography variant="h6">{step.name}</Typography>
<Chip
label={step.status}
size="small"
color={getStatusColor(step.status)}
/>
</Box>
<Typography variant="body2" color="text.secondary">
{step.description}
</Typography>
{step.status === 'running' && (
<Box sx={{ mt: 2 }}>
<Typography variant="body2" gutterBottom>
処理実行中...(ポーリング中)
</Typography>
<LinearProgress variant="indeterminate" />
</Box>
)}
</CardContent>
</Card>
</Grid>
))}
</Grid>
</Paper>
);
return (
<Box sx={{ maxWidth: 1400, mx: 'auto', p: 3 }}>
<Typography variant="h4" gutterBottom align="center" color="primary">
ワークフロー表示プロトタイプ
</Typography>
<Typography variant="h6" gutterBottom align="center" color="text.secondary">
DQNW Leaf Ver.Up2 - 作業管理ID: TM-0237935
</Typography>
{/* 複雑なワークフロー表示(画像再現) */}
<ComplexWorkflow />
{/* 標準MUIステッパー比較 */}
<StandardStepper />
{/* コントロールボタン */}
<Box sx={{
mt: 3,
display: 'flex',
gap: 2,
justifyContent: 'center',
flexWrap: 'wrap'
}}>
<Button
variant="outlined"
startIcon={<Refresh />}
onClick={() => {
setActiveStep(0);
setIsPolling(false);
}}
>
リセット
</Button>
<Button
variant="contained"
color={isPolling ? "error" : "primary"}
startIcon={isPolling ? <Stop /> : <PlayArrow />}
onClick={() => setIsPolling(!isPolling)}
disabled={activeStep >= workflowSteps.length - 1 && !isPolling}
>
{isPolling ? 'ポーリング停止' : 'ポーリング開始'}
</Button>
<Button
variant="outlined"
onClick={() => setActiveStep(Math.min(activeStep + 1, workflowSteps.length - 1))}
disabled={activeStep >= workflowSteps.length - 1}
>
手動進行
</Button>
</Box>
<DetailView />
{/* 技術検証結果 */}
<Paper elevation={1} sx={{ mt: 3, p: 3, bgcolor: 'grey.50' }}>
<Typography variant="h6" gutterBottom color="primary">
技術的検証結果
</Typography>
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<Typography variant="body2" paragraph>
<strong>✅ MUI Stepper活用:</strong><br />
基本的なワークフロー表示は標準コンポーネントで対応可能
</Typography>
<Typography variant="body2" paragraph>
<strong>✅ カスタムアイコン・アニメーション:</strong><br />
状態に応じたアイコンとCSS animationで視覚的フィードバック実現
</Typography>
<Typography variant="body2" paragraph>
<strong>✅ SVG活用:</strong><br />
複雑な矢印や接続線はSVGで柔軟に対応、色の動的変更も可能
</Typography>
</Grid>
<Grid item xs={12} md={6}>
<Typography variant="body2" paragraph>
<strong>✅ ポーリング機能:</strong><br />
useEffectとsetIntervalでリアルタイム更新をシミュレーション
</Typography>
<Typography variant="body2" paragraph>
<strong>✅ レスポンシブ対応:</strong><br />
Grid systemとflexboxで画面サイズに応じた表示調整
</Typography>
<Typography variant="body2" paragraph>
<strong>✅ 複雑なレイアウト:</strong><br />
画像の複雑なUIもMUI + CSS + SVGの組み合わせで実現可能
</Typography>
</Grid>
</Grid>
<Typography variant="body1" sx={{ mt: 2, fontWeight: 'bold', color: 'success.main' }}>
結論: MUIベースでワークフロー表示は確実に実現可能
</Typography>
</Paper>
</Box>
);
};
export default WorkflowDisplay;
// App.tsx
import WorkflowDisplay from './WorkflowDisplay'
function App() {
return <WorkflowDisplay />
}
export default App
// main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { ThemeProvider, createTheme } from '@mui/material/styles'
import CssBaseline from '@mui/material/CssBaseline'
import App from './App.tsx'
const theme = createTheme({
palette: {
primary: {
main: '#1976d2',
},
success: {
main: '#4caf50',
},
},
})
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<ThemeProvider theme={theme}>
<CssBaseline />
<App />
</ThemeProvider>
</React.StrictMode>,
)
Discussion