Skip to content
Merged
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
2 changes: 1 addition & 1 deletion plugins/harness-iacm/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@harnessio/backstage-plugin-harness-iacm",
"version": "0.3.0",
"version": "0.4.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
Expand Down
72 changes: 72 additions & 0 deletions plugins/harness-iacm/src/components/TableEmptyState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React from 'react';
import {
CircularProgress,
Typography,
Box,
makeStyles,
} from '@material-ui/core';
import { AsyncStatus } from '../types';
import { ClassNameMap } from '@material-ui/core/styles/withStyles';

const useStyles = makeStyles(theme => ({
emptyContainer: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: theme.spacing(8, 2),
minHeight: 200,
},
emptyText: {
color: theme.palette.text.secondary,
marginTop: theme.spacing(2),
},
loadingContainer: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
padding: theme.spacing(4),
minHeight: 200,
},
}));

interface TableEmptyStateProps {
status?: AsyncStatus;
hasData: boolean;
classes: ClassNameMap<'empty'>;
}

const TableEmptyState: React.FC<TableEmptyStateProps> = ({
status,
hasData,
}) => {
const localClasses = useStyles();
const isLoading =
status === AsyncStatus.Init || status === AsyncStatus.Loading;
const isEmpty = status === AsyncStatus.Success && !hasData;

if (isLoading) {
return (
<div className={localClasses.loadingContainer}>
<CircularProgress />
</div>
);
}

if (isEmpty) {
return (
<Box className={localClasses.emptyContainer}>
<Typography variant="h6" color="textSecondary">
No data available
</Typography>
<Typography variant="body2" className={localClasses.emptyText}>
There are no items to display in this table.
</Typography>
</Box>
);
}

return null;
};

export default TableEmptyState;
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React from 'react';
import { Typography, Box } from '@material-ui/core';
import DeleteIcon from '@material-ui/icons/Delete';
import { useStyles } from './styles';
import { getDriftIcon } from './DriftIcon';
import ValueDisplay from './ValueDisplay';
import { AttributeListProps } from './types';

const AttributeList: React.FC<AttributeListProps> = ({
attributes,
driftStatus,
isDeleted,
allDeleted,
}) => {
const classes = useStyles();

if (attributes.length === 0) {
return (
<Typography variant="body2" color="textSecondary">
No attributes found
</Typography>
);
}

return (
<>
{attributes.map((item, index) => {
const hasDrift = item.hasDrift;
const driftValue = item.driftValue;
const attributeIcon =
hasDrift && driftStatus ? getDriftIcon(driftStatus) : null;

return (
<Box
key={`${item.key}-${index}`}
className={
hasDrift
? `${classes.attributeRow} ${classes.attributeRowDrift}`
: classes.attributeRow
}
>
{/* Key/Label */}
<Typography component="div" className={classes.attributeKey}>
{attributeIcon && (
<span className={classes.iconContainerInline}>
{attributeIcon}
</span>
)}
{item.key}
{(isDeleted || (allDeleted && hasDrift)) && (
<span className={classes.deletedBadge}>
<DeleteIcon style={{ fontSize: 12 }} />
DELETED
</span>
)}
</Typography>

{/* Value with Copy Button */}
{hasDrift && driftValue ? (
<Box className={classes.valueComparison}>
<ValueDisplay
value={driftValue}
isDrift
label="Actual Value:"
copyTopOffset="25px"
/>
<ValueDisplay
value={item.value}
isDrift
label="Expected Value:"
copyTopOffset="25px"
/>
</Box>
) : (
<ValueDisplay value={item.value} />
)}
</Box>
);
})}
</>
);
};

export default AttributeList;
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react';
import ReplayIcon from '@material-ui/icons/Replay';
import DeleteIcon from '@material-ui/icons/Delete';
import LensIcon from '@material-ui/icons/Lens';

export const getDriftIcon = (driftStatus?: string): React.ReactNode => {
switch (driftStatus?.toLowerCase()) {
case 'drifted':
return <ReplayIcon style={{ fontSize: 18, color: '#ff9800' }} />;
case 'changed':
return <ReplayIcon style={{ fontSize: 18, color: '#ff9800' }} />;
case 'deleted':
return <DeleteIcon style={{ fontSize: 18, color: '#9e9e9e' }} />;
case 'unchanged':
return <LensIcon style={{ fontSize: 18, color: '#9e9e9e' }} />;
default:
return null;
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react';
import { Box, Typography, IconButton } from '@material-ui/core';
import CloseIcon from '@material-ui/icons/Close';
import { useStyles } from './styles';

interface HeaderProps {
title: string;
icon?: React.ReactNode;
onClose: () => void;
}

const Header: React.FC<HeaderProps> = ({ title, icon, onClose }) => {
const classes = useStyles();

return (
<Box className={classes.drawerHeader}>
<Box display="flex" alignItems="center">
{icon && <Box className={classes.iconContainer}>{icon}</Box>}
<Typography variant="h6" className={classes.drawerTitle}>
{title}
</Typography>
</Box>
<IconButton
onClick={onClose}
aria-label="close drawer"
edge="end"
style={{ padding: 0 }}
>
<CloseIcon />
</IconButton>
</Box>
);
};

export default Header;
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react';
import { TextField, InputAdornment, Box, IconButton } from '@material-ui/core';
import SearchIcon from '@material-ui/icons/Search';
import CloseIcon from '@material-ui/icons/Close';
import { useStyles } from './styles';

interface SearchFieldProps {
value: string;
onChange: (value: string) => void;
sticky?: boolean;
}

const SearchField: React.FC<SearchFieldProps> = ({ value, onChange }) => {
const classes = useStyles();

const handleClear = () => {
onChange('');
};

return (
<Box className={classes.searchFieldSticky}>
<TextField
placeholder="Search"
variant="outlined"
size="small"
fullWidth
value={value}
onChange={e => onChange(e.target.value)}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon fontSize="small" />
</InputAdornment>
),
endAdornment: value && (
<InputAdornment position="end">
<IconButton
size="small"
onClick={handleClear}
aria-label="clear search"
edge="end"
>
<CloseIcon fontSize="small" />
</IconButton>
</InputAdornment>
),
}}
/>
</Box>
);
};

export default SearchField;
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from 'react';
import { Box, Typography, Divider } from '@material-ui/core';
import { useStyles } from './styles';

interface SubHeaderProps {
name?: string;
provider?: string;
module?: string;
}

const SubHeader: React.FC<SubHeaderProps> = ({ name, provider, module }) => {
const classes = useStyles();

if (!name && !provider && !module) return null;

return (
<Box className={classes.drawerSubHeader}>
<Box className={classes.subHeaderItem}>
<Typography className={classes.subHeaderLabel}>Name:</Typography>
<Typography className={classes.subHeaderValue}>
{name || '-'}
</Typography>
</Box>
<Divider
orientation="vertical"
className={classes.subHeaderDivider}
flexItem
/>
<Box className={classes.subHeaderItem}>
<Typography className={classes.subHeaderLabel}>Provider:</Typography>
<Typography className={classes.subHeaderValue}>
{provider || '-'}
</Typography>
</Box>
<Divider
orientation="vertical"
className={classes.subHeaderDivider}
flexItem
/>
<Box className={classes.subHeaderItem}>
<Typography className={classes.subHeaderLabel}>Module:</Typography>
<Typography className={classes.subHeaderValue}>
{module || '-'}
</Typography>
</Box>
</Box>
);
};

export default SubHeader;
Loading