Move useTaskStream, TaskBorder, TaskLogStream and TaskSteps into scaffolder-react
Signed-off-by: Paul Cowan <paul.cowan@cutting.scot>
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed 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 from 'react';
|
||||
import { TaskBorder } from './TaskBorder';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
describe('TaskBorder', () => {
|
||||
it('should render a pending linear progress if the task is not complete', () => {
|
||||
const { getByRole } = render(
|
||||
<TaskBorder isComplete={false} isError={false} />,
|
||||
);
|
||||
|
||||
const progressBar = getByRole('progressbar');
|
||||
|
||||
expect(progressBar).toBeInTheDocument();
|
||||
expect(progressBar).toHaveClass('MuiLinearProgress-indeterminate');
|
||||
});
|
||||
|
||||
it('should render a determinate progress bar if the task is complete', () => {
|
||||
const { getByRole } = render(<TaskBorder isComplete isError />);
|
||||
|
||||
const progressBar = getByRole('progressbar');
|
||||
|
||||
expect(progressBar).toBeInTheDocument();
|
||||
expect(progressBar).toHaveClass('MuiLinearProgress-determinate');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed 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 from 'react';
|
||||
import { LinearProgress, makeStyles } from '@material-ui/core';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
|
||||
const useStyles = makeStyles((theme: BackstageTheme) => ({
|
||||
failed: {
|
||||
backgroundColor: theme.palette.error.main,
|
||||
},
|
||||
success: {
|
||||
backgroundColor: theme.palette.success.main,
|
||||
},
|
||||
}));
|
||||
|
||||
/**
|
||||
* The visual progress of the task event stream
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const TaskBorder = (props: {
|
||||
isComplete: boolean;
|
||||
isError: boolean;
|
||||
}) => {
|
||||
const styles = useStyles();
|
||||
|
||||
if (!props.isComplete) {
|
||||
return <LinearProgress variant="indeterminate" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
classes={{ bar: props.isError ? styles.failed : styles.success }}
|
||||
value={100}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed 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.
|
||||
*/
|
||||
export { TaskBorder } from './TaskBorder';
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed 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 { renderInTestApp } from '@backstage/test-utils';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { TaskLogStream } from './TaskLogStream';
|
||||
|
||||
// The <AutoSizer> inside <LogViewer> needs mocking to render in jsdom
|
||||
jest.mock('react-virtualized-auto-sizer', () => ({
|
||||
__esModule: true,
|
||||
default: (props: {
|
||||
children: (size: { width: number; height: number }) => ReactNode;
|
||||
}) => <>{props.children({ width: 400, height: 200 })}</>,
|
||||
}));
|
||||
|
||||
describe('TaskLogStream', () => {
|
||||
it('should render a log stream with the correct log lines', async () => {
|
||||
const logs = { step: ['line 1', 'line 2'], step2: ['line 3'] };
|
||||
|
||||
const { findByText } = await renderInTestApp(<TaskLogStream logs={logs} />);
|
||||
|
||||
const logLines = Object.values(logs).flat();
|
||||
|
||||
for (const line of logLines) {
|
||||
await expect(findByText(line)).resolves.toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
|
||||
it('should filter out empty log lines', async () => {
|
||||
const logs = { step: ['line 1', 'line 2'], step2: [] };
|
||||
|
||||
const { findAllByRole } = await renderInTestApp(
|
||||
<TaskLogStream logs={logs} />,
|
||||
);
|
||||
|
||||
await expect(findAllByRole('row')).resolves.toHaveLength(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed 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 from 'react';
|
||||
import { LogViewer } from '@backstage/core-components';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* The text of the event stream
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const TaskLogStream = (props: { logs: { [k: string]: string[] } }) => {
|
||||
const styles = useStyles();
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<LogViewer
|
||||
text={Object.values(props.logs)
|
||||
.map(l => l.join('\n'))
|
||||
.filter(Boolean)
|
||||
.join('\n')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed 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.
|
||||
*/
|
||||
export { TaskLogStream } from './TaskLogStream';
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed 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 from 'react';
|
||||
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
import { CircularProgress, makeStyles, StepIconProps } from '@material-ui/core';
|
||||
import RemoveCircleOutline from '@material-ui/icons/RemoveCircleOutline';
|
||||
import PanoramaFishEyeIcon from '@material-ui/icons/PanoramaFishEye';
|
||||
import classNames from 'classnames';
|
||||
import CheckCircleOutline from '@material-ui/icons/CheckCircleOutline';
|
||||
import ErrorOutline from '@material-ui/icons/ErrorOutline';
|
||||
|
||||
const useStepIconStyles = makeStyles((theme: BackstageTheme) => ({
|
||||
root: {
|
||||
color: theme.palette.text.disabled,
|
||||
},
|
||||
completed: {
|
||||
color: theme.palette.status.ok,
|
||||
},
|
||||
error: {
|
||||
color: theme.palette.status.error,
|
||||
},
|
||||
}));
|
||||
|
||||
export const StepIcon = (props: StepIconProps & { skipped: boolean }) => {
|
||||
const classes = useStepIconStyles();
|
||||
const { active, completed, error, skipped } = props;
|
||||
|
||||
const getMiddle = () => {
|
||||
if (active) {
|
||||
return <CircularProgress size="20px" />;
|
||||
}
|
||||
if (completed) {
|
||||
return <CheckCircleOutline />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorOutline />;
|
||||
}
|
||||
|
||||
if (skipped) {
|
||||
return <RemoveCircleOutline />;
|
||||
}
|
||||
|
||||
return <PanoramaFishEyeIcon />;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(classes.root, {
|
||||
[classes.completed]: completed,
|
||||
[classes.error]: error,
|
||||
})}
|
||||
>
|
||||
{getMiddle()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed 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 from 'react';
|
||||
import { StepTime } from './StepTime';
|
||||
import { render, act } from '@testing-library/react';
|
||||
|
||||
describe('StepTime', () => {
|
||||
it('should return empty when there is no start time', () => {
|
||||
const step = {
|
||||
id: 'test',
|
||||
startedAt: undefined,
|
||||
endedAt: undefined,
|
||||
name: 'test',
|
||||
};
|
||||
|
||||
const { container } = render(<StepTime step={step} />);
|
||||
|
||||
expect(container.querySelector('span')?.textContent).toBe('');
|
||||
});
|
||||
|
||||
it('should format the time between the start and end date properly', async () => {
|
||||
const step = {
|
||||
id: 'test',
|
||||
startedAt: '2021-01-01T00:00:00Z',
|
||||
endedAt: '2021-01-01T00:00:01Z',
|
||||
name: 'test',
|
||||
};
|
||||
|
||||
const { findByText } = render(<StepTime step={step} />);
|
||||
|
||||
await expect(findByText('1 second')).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('updates', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers({ now: new Date('2021-01-01T00:00:00Z') });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should update the time every second', async () => {
|
||||
const step = {
|
||||
id: 'test',
|
||||
startedAt: '2021-01-01T00:00:00Z',
|
||||
endedAt: undefined,
|
||||
name: 'test',
|
||||
};
|
||||
|
||||
const { findByText } = render(<StepTime step={step} />);
|
||||
|
||||
await expect(findByText('0 seconds')).resolves.toBeInTheDocument();
|
||||
|
||||
act(() => jest.advanceTimersByTime(1000));
|
||||
|
||||
await expect(findByText('1 second')).resolves.toBeInTheDocument();
|
||||
|
||||
act(() => jest.advanceTimersByTime(1000 * 60));
|
||||
|
||||
await expect(
|
||||
findByText('1 minute, 1 second'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed 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, { useCallback, useState } from 'react';
|
||||
import useInterval from 'react-use/lib/useInterval';
|
||||
import { DateTime, Interval } from 'luxon';
|
||||
import humanizeDuration from 'humanize-duration';
|
||||
import { Typography } from '@material-ui/core';
|
||||
import { useMountEffect } from '@react-hookz/web';
|
||||
|
||||
export const StepTime = (props: {
|
||||
step: {
|
||||
id: string;
|
||||
name: string;
|
||||
startedAt?: string;
|
||||
endedAt?: string;
|
||||
};
|
||||
}) => {
|
||||
const [time, setTime] = useState('');
|
||||
const { step } = props;
|
||||
|
||||
const calculate = useCallback(() => {
|
||||
if (!step.startedAt) {
|
||||
setTime('');
|
||||
return;
|
||||
}
|
||||
|
||||
const end = step.endedAt
|
||||
? DateTime.fromISO(step.endedAt)
|
||||
: DateTime.local();
|
||||
|
||||
const startedAt = DateTime.fromISO(step.startedAt);
|
||||
const formatted = Interval.fromDateTimes(startedAt, end)
|
||||
.toDuration()
|
||||
.valueOf();
|
||||
|
||||
setTime(humanizeDuration(formatted, { round: true }));
|
||||
}, [step.endedAt, step.startedAt]);
|
||||
|
||||
useMountEffect(() => calculate());
|
||||
|
||||
useInterval(() => !step.endedAt && calculate(), 1000);
|
||||
|
||||
return <Typography variant="caption">{time}</Typography>;
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed 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 from 'react';
|
||||
import { TaskSteps } from './TaskSteps';
|
||||
import { ScaffolderTaskStatus } from '@backstage/plugin-scaffolder-react';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
|
||||
describe('TaskSteps', () => {
|
||||
it('should render each of the steps', async () => {
|
||||
const steps = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Step 1',
|
||||
status: 'processing' as ScaffolderTaskStatus,
|
||||
startedAt: Date.now().toLocaleString(),
|
||||
action: 'create',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Step 2',
|
||||
status: 'failed' as ScaffolderTaskStatus,
|
||||
|
||||
startedAt: Date.now().toLocaleString(),
|
||||
endedAt: Date.now().toLocaleString(),
|
||||
action: 'create',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Step 3',
|
||||
status: 'completed' as ScaffolderTaskStatus,
|
||||
startedAt: Date.now().toLocaleString(),
|
||||
endedAt: Date.now().toLocaleString(),
|
||||
action: 'create',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'Step 4',
|
||||
status: 'skipped' as ScaffolderTaskStatus,
|
||||
startedAt: Date.now().toLocaleString(),
|
||||
endedAt: Date.now().toLocaleString(),
|
||||
action: 'create',
|
||||
},
|
||||
];
|
||||
|
||||
const { getByText } = await renderInTestApp(<TaskSteps steps={steps} />);
|
||||
|
||||
for (const step of steps) {
|
||||
expect(getByText(step.name)).toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed 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 from 'react';
|
||||
import {
|
||||
Stepper as MuiStepper,
|
||||
Step as MuiStep,
|
||||
StepButton as MuiStepButton,
|
||||
StepLabel as MuiStepLabel,
|
||||
StepIconProps,
|
||||
Box,
|
||||
} from '@material-ui/core';
|
||||
import { TaskStep } from '@backstage/plugin-scaffolder-common';
|
||||
import { type Step } from '@backstage/plugin-scaffolder-react';
|
||||
import { StepIcon } from './StepIcon';
|
||||
import { StepTime } from './StepTime';
|
||||
|
||||
/**
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface StepperProps {
|
||||
steps: (TaskStep & Step)[];
|
||||
activeStep?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The visual stepper of the task event stream
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const TaskSteps = (props: StepperProps) => {
|
||||
return (
|
||||
<MuiStepper
|
||||
activeStep={props.activeStep}
|
||||
alternativeLabel
|
||||
variant="elevation"
|
||||
>
|
||||
{props.steps.map((step, index) => {
|
||||
const isCompleted = step.status === 'completed';
|
||||
const isFailed = step.status === 'failed';
|
||||
const isActive = step.status === 'processing';
|
||||
const isSkipped = step.status === 'skipped';
|
||||
const stepIconProps: Partial<StepIconProps & { skipped: boolean }> = {
|
||||
completed: isCompleted,
|
||||
error: isFailed,
|
||||
active: isActive,
|
||||
skipped: isSkipped,
|
||||
};
|
||||
|
||||
return (
|
||||
<MuiStep key={index}>
|
||||
<MuiStepButton>
|
||||
<MuiStepLabel
|
||||
StepIconProps={stepIconProps}
|
||||
StepIconComponent={StepIcon}
|
||||
>
|
||||
<Box>{step.name}</Box>
|
||||
<StepTime step={step} />
|
||||
</MuiStepLabel>
|
||||
</MuiStepButton>
|
||||
</MuiStep>
|
||||
);
|
||||
})}
|
||||
</MuiStepper>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed 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.
|
||||
*/
|
||||
export { TaskSteps, type StepperProps } from './TaskSteps';
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed 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.
|
||||
*/
|
||||
export { TaskSteps, type StepperProps } from './TaskSteps';
|
||||
export { TaskBorder } from './TaskBorder';
|
||||
export { TaskLogStream } from './TaskLogStream';
|
||||
@@ -20,3 +20,4 @@ export * from './secrets';
|
||||
export * from './api';
|
||||
export * from './hooks';
|
||||
export * from './layouts';
|
||||
export * from './components';
|
||||
|
||||
Reference in New Issue
Block a user