| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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 React, { useEffect, useState, useCallback, useRef } from 'react' |
| import { useParams, useHistory } from 'react-router-dom' |
| // import { CSSTransition } from 'react-transition-group' |
| import { DEVLAKE_ENDPOINT } from '@/utils/config' |
| import request from '@/utils/request' |
| import dayjs from '@/utils/time' |
| import { saveAs } from 'file-saver' |
| import { |
| Button, |
| Elevation, |
| Intent, |
| Switch, |
| Card, |
| Tooltip, |
| Icon, |
| Colors, |
| Divider, |
| Spinner, |
| Classes, |
| Position, |
| Popover, |
| Collapse, |
| Dialog |
| } from '@blueprintjs/core' |
| import { NullBlueprint } from '@/data/NullBlueprint' |
| import { NullPipelineRun } from '@/data/NullPipelineRun' |
| import { Providers, ProviderLabels, ProviderIcons } from '@/data/Providers' |
| import { |
| StageStatus, |
| TaskStatus, |
| TaskStatusLabels, |
| StatusColors, |
| StatusBgColors |
| } from '@/data/Task' |
| |
| import Nav from '@/components/Nav' |
| import Sidebar from '@/components/Sidebar' |
| import Content from '@/components/Content' |
| // import TaskActivity from '@/components/pipelines/TaskActivity' |
| import CodeInspector from '@/components/pipelines/CodeInspector' |
| import StageLane from '@/components/pipelines/StageLane' |
| import { ToastNotification } from '@/components/Toast' |
| import BlueprintNavigationLinks from '@/components/blueprints/BlueprintNavigationLinks' |
| |
| import useBlueprintManager from '@/hooks/useBlueprintManager' |
| import usePipelineManager from '@/hooks/usePipelineManager' |
| import usePaginator from '@/hooks/usePaginator' |
| |
| const BlueprintDetail = (props) => { |
| // eslint-disable-next-line no-unused-vars |
| const history = useHistory() |
| const { bId } = useParams() |
| |
| const [blueprintId, setBlueprintId] = useState() |
| const [activeBlueprint, setActiveBlueprint] = useState(NullBlueprint) |
| // eslint-disable-next-line no-unused-vars |
| const [blueprintConnections, setBlueprintConnections] = useState([]) |
| const [blueprintPipelines, setBlueprintPipelines] = useState([]) |
| const [lastPipeline, setLastPipeline] = useState() |
| const [inspectedPipeline, setInspectedPipeline] = useState(NullPipelineRun) |
| const [currentRun, setCurrentRun] = useState() |
| const [showCurrentRunTasks, setShowCurrentRunTasks] = useState(true) |
| const [showInspector, setShowInspector] = useState(false) |
| const [currentStages, setCurrentStages] = useState([]) |
| |
| const pollTimer = 5000 |
| const pollInterval = useRef() |
| const [autoRefresh, setAutoRefresh] = useState(false) |
| |
| const [expandRun, setExpandRun] = useState(null) |
| const [isDownloading, setIsDownloading] = useState(false) |
| |
| const { |
| // eslint-disable-next-line no-unused-vars |
| blueprint, |
| blueprints, |
| name, |
| cronConfig, |
| customCronConfig, |
| cronPresets, |
| tasks, |
| detectedProviderTasks, |
| enable, |
| setName: setBlueprintName, |
| setCronConfig, |
| setCustomCronConfig, |
| setTasks: setBlueprintTasks, |
| setDetectedProviderTasks, |
| setEnable: setEnableBlueprint, |
| isFetching: isFetchingBlueprints, |
| isSaving, |
| isDeleting, |
| createCronExpression: createCron, |
| getCronSchedule: getSchedule, |
| getCronPreset, |
| getCronPresetByConfig, |
| getNextRunDate, |
| activateBlueprint, |
| deactivateBlueprint, |
| // eslint-disable-next-line no-unused-vars |
| fetchBlueprint, |
| fetchAllBlueprints, |
| saveBlueprint, |
| deleteBlueprint, |
| saveComplete, |
| deleteComplete |
| } = useBlueprintManager() |
| |
| const { |
| activePipeline, |
| pipelines, |
| isFetchingAll: isFetchingAllPipelines, |
| fetchPipeline, |
| runPipeline, |
| cancelPipeline, |
| fetchAllPipelines, |
| lastRunId, |
| setSettings: setPipelineSettings, |
| // eslint-disable-next-line no-unused-vars |
| allowedProviders, |
| // eslint-disable-next-line no-unused-vars |
| detectPipelineProviders, |
| logfile: pipelineLogFilename, |
| getPipelineLogfile |
| } = usePipelineManager() |
| |
| const { |
| data: historicalRuns, |
| pagedData: pagedHistoricalRuns, |
| setData: setHistoricalRuns, |
| renderControlsComponent: renderPagnationControls |
| } = usePaginator() |
| |
| const buildPipelineStages = useCallback((tasks = []) => { |
| let stages = {} |
| console.log('>>>> RECEIVED PIPELINE TASKS FOR STAGE...', tasks) |
| tasks?.forEach((tS) => { |
| stages = { |
| ...stages, |
| [tS.pipelineRow]: tasks?.filter((t) => t.pipelineRow === tS.pipelineRow) |
| } |
| }) |
| console.log('>>> BUILDING PIPELINE STAGES...', stages) |
| return stages |
| }, []) |
| |
| const runBlueprint = useCallback(() => { |
| if (activeBlueprint !== null) { |
| runPipeline() |
| } |
| }, [activeBlueprint, runPipeline]) |
| |
| const handleBlueprintActivation = useCallback( |
| (blueprint) => { |
| if (blueprint.enable) { |
| deactivateBlueprint(blueprint) |
| } else { |
| activateBlueprint(blueprint) |
| } |
| // fetchBlueprint(blueprint?.id) |
| // fetchAllPipelines() |
| }, |
| [activateBlueprint, deactivateBlueprint] |
| ) |
| |
| const handlePipelineDialogClose = useCallback(() => { |
| setExpandRun(null) |
| }, []) |
| |
| const inspectRun = useCallback((pipelineRun) => { |
| setInspectedPipeline(pipelineRun) |
| setShowInspector(true) |
| }, []) |
| |
| const viewPipelineRun = useCallback( |
| (pipelineRun) => { |
| const fetchPipelineTasks = async () => { |
| const t = await request.get( |
| `${DEVLAKE_ENDPOINT}/pipelines/${pipelineRun?.id}/tasks` |
| ) |
| setExpandRun({ |
| ...pipelineRun, |
| tasks: t.data?.tasks || [] |
| }) |
| } |
| if (pipelineRun?.id !== null) { |
| fetchPipelineTasks() |
| } |
| }, |
| [setExpandRun] |
| ) |
| |
| const handleInspectorClose = useCallback(() => { |
| setInspectedPipeline(NullPipelineRun) |
| setShowInspector(false) |
| }, []) |
| |
| const cancelRun = () => {} |
| |
| const getTaskStatusIcon = (status) => { |
| let icon = null |
| switch (status) { |
| case TaskStatus.ACTIVE: |
| case TaskStatus.RUNNING: |
| icon = <Spinner size={14} intent={Intent.PRIMARY} /> |
| break |
| case TaskStatus.COMPLETE: |
| icon = <Icon icon='tick-circle' size={14} color={Colors.GREEN5} /> |
| break |
| case TaskStatus.FAILED: |
| icon = <Icon icon='delete' size={14} color={Colors.RED5} /> |
| break |
| case TaskStatus.CANCELLED: |
| icon = <Icon icon='undo' size={14} color={Colors.RED5} /> |
| break |
| case TaskStatus.CREATED: |
| icon = <Icon icon='stopwatch' size={14} color={Colors.GRAY3} /> |
| break |
| } |
| return icon |
| } |
| |
| const downloadPipelineLog = useCallback( |
| (pipeline) => { |
| console.log( |
| `>>> DOWNLOADING PIPELINE #${pipeline?.id} LOG...`, |
| getPipelineLogfile(pipeline?.id) |
| ) |
| setIsDownloading(true) |
| ToastNotification.clear() |
| let downloadStatus = 404 |
| const checkStatusAndDownload = async (pipeline) => { |
| const d = await request.get(getPipelineLogfile(pipeline?.id)) |
| downloadStatus = d?.status |
| if (pipeline?.id && downloadStatus === 200) { |
| saveAs(getPipelineLogfile(pipeline?.id), pipelineLogFilename) |
| setIsDownloading(false) |
| } else if (pipeline?.id && downloadStatus === 404) { |
| ToastNotification.show({ |
| message: d?.message || 'Logfile not available', |
| intent: 'danger', |
| icon: 'error' |
| }) |
| setIsDownloading(false) |
| } else { |
| ToastNotification.show({ |
| message: 'Pipeline Invalid or Missing', |
| intent: 'danger', |
| icon: 'error' |
| }) |
| setIsDownloading(false) |
| } |
| } |
| checkStatusAndDownload(pipeline) |
| }, |
| [getPipelineLogfile, pipelineLogFilename] |
| ) |
| |
| useEffect(() => { |
| setBlueprintId(bId) |
| console.log('>>> REQUESTED BLUEPRINT ID ===', bId) |
| }, [bId]) |
| |
| useEffect(() => { |
| if (blueprintId) { |
| fetchBlueprint(blueprintId) |
| fetchAllPipelines() |
| } |
| }, [blueprintId, fetchBlueprint, fetchAllPipelines]) |
| |
| useEffect(() => { |
| console.log('>>>> SETTING ACTIVE BLUEPRINT...', blueprint) |
| if (blueprint?.id) { |
| setActiveBlueprint((b) => ({ |
| ...b, |
| ...blueprint, |
| id: blueprint.id, |
| name: blueprint.name |
| })) |
| setBlueprintConnections( |
| blueprint?.settings?.connections.map((connection, cIdx) => ({ |
| id: cIdx, |
| provider: connection?.plugin, |
| name: `${ |
| ProviderLabels[connection?.plugin.toUpperCase()] |
| } Connection (ID #${connection?.connectionId})`, |
| dataScope: connection?.scope |
| .map((s) => [`${s.options?.owner}/${s?.options?.repo}`]) |
| .join(', '), |
| dataEntities: [] |
| })) |
| ) |
| setPipelineSettings({ |
| name: `${blueprint?.name} ${Date.now()}`, |
| blueprintId: blueprint?.id, |
| plan: blueprint?.plan |
| }) |
| } |
| }, [blueprint, setPipelineSettings]) |
| |
| useEffect(() => { |
| console.log('>>>> FETCHED ALL PIPELINES..', pipelines, activeBlueprint?.id) |
| setBlueprintPipelines( |
| pipelines.filter((p) => p.blueprintId === activeBlueprint?.id) |
| ) |
| }, [pipelines, activeBlueprint]) |
| |
| useEffect(() => { |
| console.log('>>>> RELATED BLUEPRINT PIPELINES..', blueprintPipelines) |
| setLastPipeline(blueprintPipelines[0]) |
| setHistoricalRuns( |
| blueprintPipelines.map((p, pIdx) => ({ |
| id: p.id, |
| status: p.status, |
| statusLabel: TaskStatusLabels[p.status], |
| statusIcon: getTaskStatusIcon(p.status), |
| startedAt: p.beganAt ? dayjs(p.beganAt).format('L LTS') : '-', |
| completedAt: p.finishedAt ? dayjs(p.updatedAt).format('L LTS') : ' - ', |
| duration: |
| p.beganAt && p.finishedAt |
| ? dayjs(p.beganAt).from(p.finishedAt, true) |
| : p.beganAt && |
| [TaskStatus.RUNNING, TaskStatus.CREATED].includes(p?.status) |
| ? dayjs(p.beganAt).toNow(true) |
| : ' - ' |
| })) |
| ) |
| }, [blueprintPipelines, setHistoricalRuns]) |
| |
| useEffect(() => { |
| if ( |
| lastPipeline?.id && |
| [ |
| TaskStatus.CREATED, |
| TaskStatus.RUNNING, |
| TaskStatus.COMPLETE, |
| TaskStatus.FAILED |
| ].includes(lastPipeline.status) |
| ) { |
| fetchPipeline(lastPipeline?.id) |
| setCurrentRun((cR) => ({ |
| ...cR, |
| id: lastPipeline.id, |
| status: lastPipeline.status, |
| statusLabel: TaskStatusLabels[lastPipeline.status], |
| icon: getTaskStatusIcon(lastPipeline.status), |
| startedAt: lastPipeline.beganAt |
| ? dayjs(lastPipeline.beganAt).format('L LTS') |
| : '-', |
| duration: [TaskStatus.CREATED, TaskStatus.RUNNING].includes( |
| lastPipeline.status |
| ) |
| ? dayjs(lastPipeline.beganAt || lastPipeline.createdAt).toNow(true) |
| : dayjs(lastPipeline.beganAt).from( |
| lastPipeline.finishedAt || lastPipeline.updatedAt, |
| true |
| ), |
| stage: `Stage ${lastPipeline.stage}`, |
| tasksFinished: Number(lastPipeline.finishedTasks), |
| tasksTotal: Number(lastPipeline.totalTasks), |
| error: lastPipeline.message || null |
| })) |
| } |
| }, [fetchPipeline, lastPipeline]) |
| |
| useEffect(() => { |
| fetchAllPipelines() |
| }, [lastRunId, fetchAllPipelines]) |
| |
| useEffect(() => { |
| if (activePipeline?.id && activePipeline?.id !== null) { |
| setCurrentStages(buildPipelineStages(activePipeline.tasks)) |
| setAutoRefresh( |
| [TaskStatus.RUNNING, TaskStatus.CREATED].includes( |
| activePipeline?.status |
| ) |
| ) |
| setCurrentRun((cR) => ({ |
| ...cR, |
| startedAt: activePipeline?.beganAt |
| ? dayjs(activePipeline?.beganAt).format('L LTS') |
| : '-', |
| stage: `Stage ${activePipeline.stage}`, |
| status: activePipeline?.status, |
| statusLabel: TaskStatusLabels[activePipeline?.status], |
| icon: getTaskStatusIcon(activePipeline?.status) |
| })) |
| } |
| }, [activePipeline, buildPipelineStages]) |
| |
| useEffect(() => { |
| console.log('>> BUILDING CURRENT STAGES...', currentStages) |
| }, [currentStages]) |
| |
| useEffect(() => { |
| if (autoRefresh && activePipeline?.id) { |
| console.log('>> ACTIVITY POLLING ENABLED!') |
| pollInterval.current = setInterval(() => { |
| fetchPipeline(activePipeline?.id) |
| // setLastPipeline(activePipeline) |
| }, pollTimer) |
| } else { |
| console.log('>> ACTIVITY POLLING DISABLED!') |
| clearInterval(pollInterval.current) |
| if (activePipeline?.id) { |
| fetchPipeline(activePipeline?.id) |
| fetchAllPipelines() |
| } |
| } |
| }, [ |
| autoRefresh, |
| fetchPipeline, |
| fetchAllPipelines, |
| activePipeline?.id, |
| pollTimer |
| ]) |
| |
| // useEffect(() => { |
| // console.log('>> VIEW PIPELINE RUN....', expandRun) |
| // }, [expandRun]) |
| |
| return ( |
| <> |
| <div className='container'> |
| <Nav /> |
| <Sidebar /> |
| <Content> |
| <main className='main'> |
| <div |
| className='blueprint-header' |
| style={{ |
| display: 'flex', |
| width: '100%', |
| justifyContent: 'space-between', |
| marginBottom: '10px' |
| }} |
| > |
| <div className='blueprint-name' style={{}}> |
| <h2 style={{ fontWeight: 'bold' }}>{activeBlueprint?.name}</h2> |
| </div> |
| <div |
| className='blueprint-info' |
| style={{ display: 'flex', alignItems: 'center' }} |
| > |
| <div className='blueprint-schedule'> |
| <span |
| className='blueprint-schedule-interval' |
| style={{ textTransform: 'capitalize', padding: '0 10px' }} |
| > |
| {activeBlueprint?.interval} (at{' '} |
| {dayjs(getNextRunDate(activeBlueprint?.cronConfig)).format( |
| 'hh:mm A' |
| )} |
| ) |
| </span>{' '} |
| {' '} |
| <span className='blueprint-schedule-nextrun'> |
| {activeBlueprint?.isManual ? ( |
| <strong>Manual Mode</strong> |
| ) : ( |
| <> |
| Next Run{' '} |
| {dayjs( |
| getNextRunDate(activeBlueprint?.cronConfig) |
| ).fromNow()} |
| </> |
| )} |
| </span> |
| </div> |
| <div |
| className='blueprint-actions' |
| style={{ padding: '0 10px' }} |
| > |
| <Button |
| intent={Intent.PRIMARY} |
| small |
| text='Run Now' |
| onClick={runBlueprint} |
| disabled={ |
| !activeBlueprint?.enable || |
| [TaskStatus.CREATED, TaskStatus.RUNNING].includes( |
| currentRun?.status |
| ) |
| } |
| /> |
| </div> |
| <div className='blueprint-enabled'> |
| <Switch |
| id='blueprint-enable' |
| name='blueprint-enable' |
| checked={activeBlueprint?.enable} |
| label={ |
| activeBlueprint?.enable |
| ? 'Blueprint Enabled' |
| : 'Blueprint Disabled' |
| } |
| onChange={() => handleBlueprintActivation(activeBlueprint)} |
| style={{ |
| marginBottom: 0, |
| marginTop: 0, |
| color: !activeBlueprint?.enable ? Colors.GRAY3 : 'inherit' |
| }} |
| disabled={currentRun?.status === TaskStatus.RUNNING} |
| /> |
| </div> |
| <div style={{ padding: '0 10px' }}> |
| <Button |
| intent={Intent.PRIMARY} |
| icon='trash' |
| small |
| minimal |
| disabled |
| /> |
| </div> |
| </div> |
| </div> |
| |
| <BlueprintNavigationLinks blueprint={activeBlueprint} /> |
| |
| <div |
| className='blueprint-run' |
| style={{ |
| width: '100%', |
| alignSelf: 'flex-start', |
| minWidth: '750px' |
| }} |
| > |
| <h3>Current Run</h3> |
| <Card |
| className={`current-run status-${currentRun?.status.toLowerCase()}`} |
| elevation={Elevation.TWO} |
| style={{ padding: '12px', marginBottom: '8px' }} |
| > |
| {currentRun && ( |
| <div |
| style={{ display: 'flex', justifyContent: 'space-between' }} |
| > |
| <div> |
| <label style={{ color: '#94959F' }}>Status</label> |
| <div style={{ display: 'flex' }}> |
| <span style={{ marginRight: '6px', marginTop: '2px' }}> |
| {currentRun?.icon} |
| </span> |
| <h4 |
| className={`status-${currentRun?.status.toLowerCase()}`} |
| style={{ fontSize: '15px', margin: 0, padding: 0 }} |
| > |
| {currentRun?.statusLabel} |
| </h4> |
| </div> |
| </div> |
| <div> |
| <label style={{ color: '#94959F' }}>Started at</label> |
| <h4 style={{ fontSize: '15px', margin: 0, padding: 0 }}> |
| {currentRun?.startedAt} |
| </h4> |
| </div> |
| <div> |
| <label style={{ color: '#94959F' }}>Duration</label> |
| <h4 style={{ fontSize: '15px', margin: 0, padding: 0 }}> |
| {currentRun?.duration} |
| </h4> |
| </div> |
| <div> |
| <label style={{ color: '#94959F' }}>Current Stage</label> |
| <h4 style={{ fontSize: '15px', margin: 0, padding: 0 }}> |
| {currentRun?.stage} |
| </h4> |
| </div> |
| <div> |
| <label style={{ color: '#94959F' }}> |
| Tasks Completed |
| </label> |
| <h4 style={{ fontSize: '15px', margin: 0, padding: 0 }}> |
| {currentRun?.tasksFinished} / {currentRun?.tasksTotal} |
| </h4> |
| </div> |
| <div |
| style={{ |
| display: 'flex', |
| justifyContent: 'center', |
| alignItems: 'center' |
| }} |
| > |
| <div style={{ display: 'block' }}> |
| {/* <Button intent={Intent.PRIMARY} outlined text='Cancel' onClick={cancelRun} /> */} |
| <Popover |
| key='popover-help-key-cancel-run' |
| className='trigger-pipeline-cancel' |
| popoverClassName='popover-pipeline-cancel' |
| position={Position.BOTTOM} |
| autoFocus={false} |
| enforceFocus={false} |
| usePortal={true} |
| disabled={currentRun?.status !== 'TASK_RUNNING'} |
| > |
| <Button |
| // icon='stop' |
| text='Cancel' |
| intent={Intent.PRIMARY} |
| outlined |
| disabled={currentRun?.status !== 'TASK_RUNNING'} |
| /> |
| <> |
| <div |
| style={{ |
| fontSize: '12px', |
| padding: '12px', |
| maxWidth: '200px' |
| }} |
| > |
| <p> |
| Are you Sure you want to cancel this{' '} |
| <strong>Pipeline Run</strong>? |
| </p> |
| <div |
| style={{ |
| display: 'flex', |
| width: '100%', |
| justifyContent: 'flex-end' |
| }} |
| > |
| <Button |
| text='NO' |
| minimal |
| small |
| className={Classes.POPOVER_DISMISS} |
| style={{ |
| marginLeft: 'auto', |
| marginRight: '3px' |
| }} |
| /> |
| <Button |
| className={Classes.POPOVER_DISMISS} |
| text='YES' |
| icon='small-tick' |
| intent={Intent.DANGER} |
| small |
| onClick={() => cancelPipeline(currentRun?.id)} |
| /> |
| </div> |
| </div> |
| </> |
| </Popover> |
| </div> |
| </div> |
| </div> |
| )} |
| {!currentRun && ( |
| <> |
| <p style={{ margin: 0 }}> |
| There is no current run for this blueprint. |
| </p> |
| </> |
| )} |
| {currentRun?.error && ( |
| <div style={{ marginTop: '10px' }}> |
| <p className='error-msg' style={{ color: '#E34040' }}> |
| {currentRun?.error} |
| </p> |
| </div> |
| )} |
| </Card> |
| {currentRun && ( |
| <Card |
| elevation={Elevation.TWO} |
| style={{ padding: '12px', marginBottom: '8px' }} |
| > |
| <div |
| className='blueprint-run-activity' |
| style={{ display: 'flex', width: '100%' }} |
| > |
| <div |
| className='pipeline-task-activity' |
| style={{ |
| flex: 1, |
| padding: |
| Object.keys(currentStages).length === 1 ? '0' : 0, |
| overflow: 'hidden', |
| textOverflow: 'ellipsis' |
| }} |
| > |
| {Object.keys(currentStages).length > 0 && ( |
| <div className='pipeline-multistage-activity'> |
| {Object.keys(currentStages).map((sK, sIdx) => ( |
| <StageLane |
| key={`stage-lane-key-${sIdx}`} |
| stages={currentStages} |
| sK={sK} |
| sIdx={sIdx} |
| showStageTasks={showCurrentRunTasks} |
| /> |
| ))} |
| </div> |
| )} |
| </div> |
| |
| <Button |
| icon={ |
| showCurrentRunTasks ? 'chevron-down' : 'chevron-right' |
| } |
| intent={Intent.NONE} |
| minimal |
| small |
| style={{ |
| textAlign: 'center', |
| display: 'block', |
| float: 'right', |
| margin: '0 10px', |
| marginBottom: 'auto' |
| }} |
| onClick={() => setShowCurrentRunTasks((s) => !s)} |
| /> |
| </div> |
| </Card> |
| )} |
| </div> |
| |
| <div |
| className='blueprint-historical-runs' |
| style={{ |
| width: '100%', |
| alignSelf: 'flex-start', |
| minWidth: '750px' |
| }} |
| > |
| <h3>Historical Runs</h3> |
| <Card |
| elevation={Elevation.TWO} |
| style={{ padding: '0', marginBottom: '8px' }} |
| > |
| <table |
| className='bp3-html-table bp3-html-table historical-runs-table' |
| style={{ width: '100%' }} |
| > |
| <thead> |
| <tr> |
| <th style={{ minWidth: '100px', whiteSpace: 'nowrap' }}> |
| Status |
| </th> |
| <th style={{ minWidth: '100px', whiteSpace: 'nowrap' }}> |
| Started at |
| </th> |
| <th style={{ minWidth: '100px', whiteSpace: 'nowrap' }}> |
| Completed at |
| </th> |
| <th style={{ minWidth: '100px', whiteSpace: 'nowrap' }}> |
| Duration |
| </th> |
| <th style={{ width: '100%', whiteSpace: 'nowrap' }} /> |
| </tr> |
| </thead> |
| <tbody> |
| {pagedHistoricalRuns.map((run, runIdx) => ( |
| <tr key={`historical-run-key-${runIdx}`}> |
| <td |
| style={{ |
| width: '15%', |
| whiteSpace: 'nowrap', |
| borderBottom: '1px solid #f0f0f0' |
| }} |
| > |
| <span |
| style={{ |
| display: 'inline-block', |
| float: 'left', |
| marginRight: '5px' |
| }} |
| > |
| {run.statusIcon} |
| </span>{' '} |
| {run.statusLabel} |
| </td> |
| <td |
| style={{ |
| width: '25%', |
| whiteSpace: 'nowrap', |
| borderBottom: '1px solid #f0f0f0' |
| }} |
| > |
| {run.startedAt} |
| </td> |
| <td |
| style={{ |
| width: '25%', |
| whiteSpace: 'nowrap', |
| borderBottom: '1px solid #f0f0f0' |
| }} |
| > |
| {run.completedAt} |
| </td> |
| <td |
| style={{ |
| width: '15%', |
| whiteSpace: 'nowrap', |
| borderBottom: '1px solid #f0f0f0' |
| }} |
| > |
| {run.duration} |
| </td> |
| <td |
| style={{ |
| textAlign: 'right', |
| borderBottom: '1px solid #f0f0f0', |
| whiteSpace: 'nowrap' |
| }} |
| > |
| <Tooltip intent={Intent.PRIMARY} content='View JSON'> |
| <Button |
| intent={Intent.PRIMARY} |
| minimal |
| small |
| icon='code' |
| onClick={() => |
| inspectRun( |
| blueprintPipelines.find( |
| (p) => p.id === run.id |
| ) |
| ) |
| } |
| /> |
| </Tooltip> |
| <Tooltip |
| intent={Intent.PRIMARY} |
| content='Download Full Log' |
| > |
| <Button |
| intent={Intent.NONE} |
| loading={isDownloading} |
| minimal |
| small |
| icon='document' |
| style={{ marginLeft: '10px' }} |
| onClick={() => |
| downloadPipelineLog( |
| blueprintPipelines.find( |
| (p) => p.id === run.id |
| ) |
| ) |
| } |
| /> |
| </Tooltip> |
| <Tooltip |
| intent={Intent.PRIMARY} |
| content='Show Run Activity' |
| > |
| <Button |
| intent={Intent.PRIMARY} |
| minimal |
| small |
| icon={ |
| expandRun?.id === run.id |
| ? 'chevron-down' |
| : 'chevron-right' |
| } |
| style={{ marginLeft: '10px' }} |
| onClick={() => |
| viewPipelineRun( |
| blueprintPipelines.find( |
| (p) => p.id === run.id |
| ) |
| ) |
| } |
| /> |
| </Tooltip> |
| </td> |
| </tr> |
| ))} |
| {historicalRuns.length === 0 && ( |
| <tr> |
| <td colSpan={5}> |
| There are no historical runs associated with this |
| blueprint. |
| </td> |
| </tr> |
| )} |
| </tbody> |
| </table> |
| </Card> |
| </div> |
| {historicalRuns.length > 0 && ( |
| <div style={{ alignSelf: 'flex-end', padding: '10px' }}> |
| {renderPagnationControls()} |
| </div> |
| )} |
| </main> |
| </Content> |
| </div> |
| <CodeInspector |
| isOpen={showInspector} |
| activePipeline={inspectedPipeline} |
| onClose={handleInspectorClose} |
| /> |
| <Dialog |
| className='dialog-view-pipeline' |
| // icon= |
| title={`Historical Run #${expandRun?.id}`} |
| isOpen={expandRun !== null} |
| onClose={handlePipelineDialogClose} |
| onClosed={() => {}} |
| canOutsideClickClose={true} |
| style={{ backgroundColor: '#ffffff' }} |
| > |
| <div className={Classes.DIALOG_BODY}> |
| {Object.keys(buildPipelineStages(expandRun?.tasks)).length > 0 && ( |
| <div className='pipeline-multistage-activity'> |
| {Object.keys(buildPipelineStages(expandRun?.tasks)).map( |
| (sK, sIdx) => ( |
| <StageLane |
| key={`stage-lane-key-${sIdx}`} |
| stages={buildPipelineStages(expandRun?.tasks)} |
| sK={sK} |
| sIdx={sIdx} |
| showStageTasks={true} |
| /> |
| ) |
| )} |
| </div> |
| )} |
| </div> |
| </Dialog> |
| </> |
| ) |
| } |
| |
| export default BlueprintDetail |