Skip to content

Commit

Permalink
feat: Drill-down for ECS tasks (#180)
Browse files Browse the repository at this point in the history
Co-authored-by: Fabio Torchetti <fabbari@amazon.com>
  • Loading branch information
niallthomson and fabbazon authored Jul 9, 2024
1 parent c1f5b50 commit 58676ee
Show file tree
Hide file tree
Showing 6 changed files with 417 additions and 39 deletions.
157 changes: 157 additions & 0 deletions plugins/ecs/frontend/src/components/EcsDrawer/EcsDrawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/**
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* 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, { ChangeEvent, useState } from 'react';

import Drawer from '@material-ui/core/Drawer';
import Grid from '@material-ui/core/Grid';
import IconButton from '@material-ui/core/IconButton';
import Typography from '@material-ui/core/Typography';
import Button from '@material-ui/core/Button';
import {
createStyles,
makeStyles,
Theme,
withStyles,
} from '@material-ui/core/styles';
import CloseIcon from '@material-ui/icons/Close';

const useDrawerContentStyles = makeStyles((_theme: Theme) =>
createStyles({
header: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
},
icon: {
fontSize: 20,
},
}),
);

interface EcsDrawerContentProps {
close: () => void;
title: string;
header?: React.ReactNode;
children?: React.ReactNode;
}

const EcsDrawerContent = ({
children,
header,
title,
close,
}: EcsDrawerContentProps) => {
const classes = useDrawerContentStyles();

return (
<>
<div className={classes.header}>
<Grid container justifyContent="flex-start" alignItems="flex-start">
<Grid item xs={11}>
<Typography variant="h5">{title}</Typography>
</Grid>
<Grid item xs={1}>
<IconButton
key="dismiss"
title="Close the drawer"
onClick={() => close()}
color="inherit"
>
<CloseIcon className={classes.icon} />
</IconButton>
</Grid>
<Grid item xs={12}>
{header}
</Grid>
</Grid>
</div>
<div>{children}</div>
</>
);
};

export interface EcsDrawerProps {
open?: boolean;
title: string;
label: React.ReactNode;
drawerContentsHeader?: React.ReactNode;
children?: React.ReactNode;
}

const useDrawerStyles = makeStyles((theme: Theme) =>
createStyles({
paper: {
width: '50%',
justifyContent: 'space-between',
padding: theme.spacing(2.5),
},
}),
);

const DrawerButton = withStyles({
root: {
padding: '6px 5px',
},
label: {
textTransform: 'none',
},
})(Button);

export const EcsDrawer = ({
open,
label,
drawerContentsHeader,
title,
children,
}: EcsDrawerProps) => {
const classes = useDrawerStyles();
const [isOpen, setIsOpen] = useState<boolean>(open ?? false);

const toggleDrawer = (e: ChangeEvent<{}>, newValue: boolean) => {
e.stopPropagation();
setIsOpen(newValue);
};

return (
<>
<DrawerButton
onClick={event => {
setIsOpen(true);
event.stopPropagation();
}}
style={{ width: '100%', textAlign: 'left', justifyContent: 'left' }}
>
{label}
</DrawerButton>
<Drawer
classes={{
paper: classes.paper,
}}
anchor="right"
open={isOpen}
onClose={(e: any) => toggleDrawer(e, false)}
onClick={event => event.stopPropagation()}
>
{isOpen && (
<EcsDrawerContent
header={drawerContentsHeader}
title={title}
children={children}
close={() => setIsOpen(false)}
/>
)}
</Drawer>
</>
);
};
128 changes: 128 additions & 0 deletions plugins/ecs/frontend/src/components/EcsDrawer/EcsTaskDrawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/**
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* 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 { KeyValuePair, Task } from '@aws-sdk/client-ecs';
import Grid from '@material-ui/core/Grid';
import Typography from '@material-ui/core/Typography';
import React from 'react';
import {
formatTime,
getTaskDefinition,
getTaskId,
stringOrDefault,
} from '../../util';
import { StructuredMetadataTable } from '@backstage/core-components';
import { TaskStatus } from '../EcsServices/EcsServices';
import { Box } from '@material-ui/core';
import { EcsContainer } from '../EcsServices/EcsContainer';
import { EcsDrawer } from './EcsDrawer';

const formatTaskOverview = (task: Task) => {
return {
ID: getTaskId(task.taskArn),
lastStatus: <TaskStatus status={task.lastStatus} />,
desiredStatus: <TaskStatus status={task.desiredStatus} />,
createdAt: formatTime(task.createdAt),
};
};

const formatTaskConfiguration = (task: Task) => {
let attachmentDetails: KeyValuePair[] | undefined;

if (task.attachments) {
if (task.attachments.length > 0) {
attachmentDetails = task.attachments[0].details;
}
}

return {
CPU: stringOrDefault(task.cpu),
memory: stringOrDefault(task.memory),
platformVersion: task.platformVersion,
capacityProvider: stringOrDefault(task.capacityProviderName),
launchType: stringOrDefault(task.launchType),
containerInstanceID: stringOrDefault(
task.containerInstanceArn?.split('/')[1],
),
taskDefinition: stringOrDefault(getTaskDefinition(task.taskDefinitionArn)),
taskGroup: stringOrDefault(task.group),
ENI_ID: stringOrDefault(
attachmentDetails?.find(e => e.name === 'networkInterfaceId')?.value,
),
subnetID: stringOrDefault(
attachmentDetails?.find(e => e.name === 'subnetId')?.value,
),
privateIPAddress: stringOrDefault(
attachmentDetails?.find(e => e.name === 'privateIPv4Address')?.value,
),
};
};

export const EcsTaskDrawer = ({ task }: { task: Task }) => {
if (!task) {
return null;
}

const taskId = getTaskId(task.taskArn);

return (
<EcsDrawer label={<div>{taskId}</div>} title={`Task ID ${taskId}`}>
<Grid
container
direction="column"
justifyContent="space-between"
alignItems="flex-start"
spacing={2}
style={{ position: 'relative' }}
>
<Grid item xs={12} spacing={0}>
<Typography>
<Box component="span" fontWeight="fontWeightMedium">
Overview
</Box>
</Typography>
</Grid>
<Grid container item xs={12} spacing={0}>
<StructuredMetadataTable metadata={formatTaskOverview(task)} />
</Grid>
<Grid item xs={12} spacing={0}>
<Typography>
<Box component="span" fontWeight="fontWeightMedium">
Configuration
</Box>
</Typography>
</Grid>
<Grid container item xs={12} spacing={0}>
<StructuredMetadataTable metadata={formatTaskConfiguration(task)} />
</Grid>
<Grid item xs={12}>
<Typography variant="h5">Containers</Typography>
</Grid>
{task.containers?.map(container => (
<>
<Grid item xs={12} spacing={0}>
<Typography>
<Box component="span" fontWeight="fontWeightMedium">
{container.name}
</Box>
</Typography>
</Grid>
<Grid item xs={12}>
<EcsContainer container={container} />
</Grid>
</>
))}
</Grid>
</EcsDrawer>
);
};
14 changes: 14 additions & 0 deletions plugins/ecs/frontend/src/components/EcsDrawer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* 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 * from './EcsDrawer';
52 changes: 52 additions & 0 deletions plugins/ecs/frontend/src/components/EcsServices/EcsContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* 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 { Container } from '@aws-sdk/client-ecs';
import {
OverflowTooltip,
StructuredMetadataTable,
} from '@backstage/core-components';
import React from 'react';
import { TaskHealthStatus, TaskStatus } from '.';
import { Box } from '@material-ui/core';
import { stringOrDefault } from '../../util';

const overflowMaxWidth = '250px';

const formatContainer = (container: Container) => {
return {
ID: stringOrDefault(container.containerArn?.split('/')[3]),
status: <TaskStatus status={container.lastStatus} />,
healthStatus: <TaskHealthStatus status={container.healthStatus} />,
CPU: stringOrDefault(container.cpu),
memory: stringOrDefault(container.memoryReservation),
imageURI: (
<Box maxWidth={overflowMaxWidth}>
<OverflowTooltip text={stringOrDefault(container.image)} />
</Box>
),
imageDigest: (
<Box maxWidth={overflowMaxWidth}>
<OverflowTooltip text={stringOrDefault(container.imageDigest)} />
</Box>
),
};
};

type EcsContainerProps = {
container: Container;
};

export const EcsContainer = ({ container }: EcsContainerProps) => {
return <StructuredMetadataTable metadata={formatContainer(container)} />;
};
Loading

0 comments on commit 58676ee

Please sign in to comment.