Skip to content
Open
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
5 changes: 3 additions & 2 deletions app/controllers/foreman_tasks/tasks_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ class TasksController < ::ApplicationController
before_action :find_dynflow_task, only: [:unlock, :force_unlock, :cancel, :abort, :cancel_step, :resume]

def show
@task = resource_base.find(params[:id])
render :layout => !request.xhr?
@task = resource_scope.find(params[:id])

render('react/index', :layout => 'layouts/react_application')
end

def index
Expand Down
18 changes: 0 additions & 18 deletions app/views/foreman_tasks/tasks/show.html.erb

This file was deleted.

1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
resources :tasks, :only => [:index], constraints: ->(req) { req.format == :csv }

match '/tasks', to: '/react#index', via: :get
get '/tasks/:id', :to => 'tasks#show'
match '/tasks/:id/sub_tasks', to: '/react#index', via: :get
match '/ex_tasks/:id', to: '/react#index', via: :get

Expand Down
10 changes: 9 additions & 1 deletion test/controllers/tasks_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,20 @@ def in_taxonomy_scope(organization, location = nil)
end

describe 'show' do
it 'does not allow user without permissions to see task details' do
it 'does not allow user without permissions to see task details page' do
setup_user('view', 'foreman_tasks', 'owner.id = current_user')
get :show, params: { id: FactoryBot.create(:some_task).id },
session: set_session_user(User.current)
assert_response :not_found
end

it 'serves react shell when user may view the task' do
task = FactoryBot.create(:some_task)
get :show, params: { id: task.id }, session: set_session_user

assert_response :success
assert_includes @response.body, '/webpack/'
end
end

describe 'index' do
Expand Down
255 changes: 199 additions & 56 deletions webpack/ForemanTasks/Components/TaskDetails/Components/Errors.js
Original file line number Diff line number Diff line change
@@ -1,69 +1,212 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Alert } from '@patternfly/react-core';
import { translate as __ } from 'foremanReact/common/I18n';
import {
Alert,
AlertVariant,
CodeBlock,
CodeBlockCode,
EmptyState,
EmptyStateBody,
EmptyStateHeader,
Icon,
EmptyStateVariant,
Flex,
FlexItem,
Grid,
GridItem,
Split,
SplitItem,
Stack,
StackItem,
} from '@patternfly/react-core';
import { CheckCircleIcon } from '@patternfly/react-icons';
import { translate as __, sprintf } from 'foremanReact/common/I18n';

const Errors = ({ ...props }) => {
const { failedSteps, executionPlan } = props;
if (!executionPlan)
const TRANSPARENT_CODE_BLOCK_STYLE = {
'--pf-v5-c-code-block--BackgroundColor': 'transparent',
backgroundColor: 'transparent',
};

const ErrorDetailSection = ({ label, children }) => (
<StackItem>
<Flex
direction={{ default: 'column' }}
spaceItems={{ default: 'spaceItemsXs' }}
>
<FlexItem>
<strong>{label}</strong>
</FlexItem>
<FlexItem>
<CodeBlock style={TRANSPARENT_CODE_BLOCK_STYLE}>
<CodeBlockCode>{children}</CodeBlockCode>
</CodeBlock>
</FlexItem>
</Flex>
</StackItem>
);

ErrorDetailSection.propTypes = {
label: PropTypes.node.isRequired,
children: PropTypes.node.isRequired,
};

const ErrorDetailsPane = ({ step }) => {
if (!step) {
return null;
}

return (
<Stack hasGutter>
<ErrorDetailSection label={__('Input')}>{step.input}</ErrorDetailSection>
<ErrorDetailSection label={__('Output')}>
{step.output}
</ErrorDetailSection>
{step.error && (
<>
<ErrorDetailSection label={`${__('Exception')}:`}>
{step.error.exception_class}: {step.error.message}
</ErrorDetailSection>
<ErrorDetailSection label={__('Backtrace')}>
{(step.error.backtrace || []).join('\n')}
</ErrorDetailSection>
</>
)}
</Stack>
);
};

ErrorDetailsPane.propTypes = {
step: PropTypes.shape({
input: PropTypes.node,
output: PropTypes.node,
error: PropTypes.shape({
exception_class: PropTypes.string,
message: PropTypes.string,
backtrace: PropTypes.array,
}),
}),
};

ErrorDetailsPane.defaultProps = {
step: null,
};

const Errors = ({ executionPlan, failedSteps }) => {
const [selectedIndex, setSelectedIndex] = useState(0);

useEffect(() => {
setSelectedIndex(idx => {
if (idx >= failedSteps.length) {
return Math.max(0, failedSteps.length - 1);
}

return idx;
});
}, [failedSteps.length]);

if (!executionPlan) {
return (
<Alert
variant="danger"
isInline
ouiaId="task-errors-plan-missing"
title={__('Execution plan unavailable')}
>
<Alert variant={AlertVariant.danger} ouiaId="task-errors-plan-missing">
{__('Execution plan data not available ')}
</Alert>
);
if (!failedSteps.length)
}

if (!failedSteps.length) {
return (
<Alert
variant="success"
isInline
ouiaId="task-errors-none"
title={__('No errors')}
/>
<Grid>
<GridItem span={12}>
<Flex
direction={{ default: 'column' }}
alignItems={{ default: 'alignItemsCenter' }}
justifyContent={{ default: 'justifyContentCenter' }}
fullWidth={{ default: 'fullWidth' }}
>
<FlexItem>
<EmptyState variant={EmptyStateVariant.full}>
<EmptyStateHeader
titleText={__('No errors found')}
headingLevel="h2"
icon={
<Icon size="xl" status="success">
<CheckCircleIcon />
</Icon>
}
/>
<EmptyStateBody>
{__('The task finished with no errors or warnings.')}
</EmptyStateBody>
</EmptyState>
</FlexItem>
</Flex>
</GridItem>
</Grid>
);
}

const selectedStep = failedSteps[selectedIndex];

return (
<div>
{failedSteps.map((step, i) => (
<Alert
variant="danger"
isInline
key={i}
ouiaId={`task-error-${i}`}
title={__('Step error')}
<Split hasGutter>
<SplitItem style={{ flex: '0 0 min(33%, 20rem)', minWidth: 0 }}>
<Flex
direction={{ default: 'column' }}
fullWidth={{ default: 'fullWidth' }}
role="listbox"
aria-label={__('Failed task errors')}
style={{ minWidth: 0 }}
>
<span>{__('Action')}:</span>
<span>
<pre>{step.action_class}</pre>
</span>
<span>{__('Input')}:</span>
<span>
<pre>{step.input}</pre>
</span>
<span>{__('Output')}:</span>
<span>
<pre>{step.output}</pre>
</span>
{step.error && (
<React.Fragment>
<span>{__('Exception')}:</span>
<span>
<pre>
{step.error.exception_class}: {step.error.message}
</pre>
</span>
<span>{__('Backtrace')}:</span>
<span>
<pre>{(step.error.backtrace || []).join('\n')}</pre>
</span>
</React.Fragment>
)}
</Alert>
))}
</div>
<Stack hasGutter>
{failedSteps.map((step, i) => {
const summary =
step.error?.message || step.action_class || __('Unknown error');
const isStoppedStep = ['skipped', 'skipping'].includes(
String(step.state ?? '')
);
const variant = isStoppedStep
? AlertVariant.warning
: AlertVariant.danger;
const titleKey = isStoppedStep
? __('Stopped task: %s')
: __('Failed task: %s');
const selected = i === selectedIndex;

return (
<StackItem key={`${step.action_class}-${i}`}>
<Flex
direction={{ default: 'column' }}
fullWidth={{ default: 'fullWidth' }}
role="option"
aria-selected={selected}
tabIndex={0}
onClick={() => setSelectedIndex(i)}
onKeyDown={event => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
setSelectedIndex(i);
}
}}
style={{
cursor: 'pointer',
}}
>
<Alert
variant={variant}
isInline
ouiaId={`task-error-${i}`}
title={sprintf(titleKey, summary)}
/>
</Flex>
</StackItem>
);
})}
</Stack>
</Flex>
</SplitItem>
<SplitItem isFilled style={{ minWidth: 0 }}>
<ErrorDetailsPane step={selectedStep} />
</SplitItem>
</Split>
);
};

Expand Down
Loading
Loading