😎

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