Files
2024-02-15 17:01:50 +00:00

321 lines
8.0 KiB
TypeScript

import styled from '@emotion/styled';
import Panel from 'sentry/components/panels/panel';
import PanelBody from 'sentry/components/panels/panelBody';
import {space} from 'sentry/styles/space';
export const GRID_HEAD_ROW_HEIGHT = 45;
export const GRID_BODY_ROW_HEIGHT = 40;
export const GRID_STATUS_MESSAGE_HEIGHT = GRID_BODY_ROW_HEIGHT * 4;
/**
* Local z-index stacking context
* https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context
*/
// Parent context is Panel
const Z_INDEX_PANEL = 1;
const Z_INDEX_GRID_STATUS = -1;
const Z_INDEX_GRID = 5;
// Parent context is GridHeadCell
const Z_INDEX_GRID_RESIZER = 1;
export const Header = styled('div')`
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: ${space(1)};
`;
export const HeaderTitle = styled('h4')`
margin: 0;
font-size: ${p => p.theme.fontSizeMedium};
color: ${p => p.theme.subText};
`;
export const HeaderButtonContainer = styled('div')`
display: grid;
gap: ${space(1)};
grid-auto-flow: column;
grid-auto-columns: auto;
justify-items: end;
/* Hovercard anchor element when features are disabled. */
& > span {
display: flex;
flex-direction: row;
}
`;
export const Body = styled(({children, ...props}) => (
<Panel {...props}>
<PanelBody>{children}</PanelBody>
</Panel>
))`
overflow-x: auto;
z-index: ${Z_INDEX_PANEL};
`;
/**
* Grid is the parent element for the tableResizable component.
*
* On newer browsers, it will use CSS Grids to implement its layout.
*
* However, it is based on <table>, which has a distinction between header/body
* HTML elements, which allows CSS selectors to its full potential. This has
* the added advantage that older browsers will still have a chance of
* displaying the data correctly (but this is untested).
*
* <thead>, <tbody>, <tr> are ignored by CSS Grid.
* The entire layout is determined by the usage of <th> and <td>.
*/
export const Grid = styled('table')<{height?: string | number; scrollable?: boolean}>`
position: inherit;
display: grid;
/* Overwritten by GridEditable.setGridTemplateColumns */
grid-template-columns: repeat(auto-fill, minmax(50px, auto));
box-sizing: border-box;
border-collapse: collapse;
margin: 0;
z-index: ${Z_INDEX_GRID};
${p =>
p.scrollable &&
`
overflow-x: auto;
overflow-y: scroll;
`}
${p =>
p.height
? `
height: 100%;
max-height: ${typeof p.height === 'number' ? p.height + 'px' : p.height}
`
: ''}
`;
export const GridRow = styled('tr')`
display: contents;
&:last-child,
&:last-child > td:first-child,
&:last-child > td:last-child {
border-bottom-left-radius: ${p => p.theme.borderRadius};
border-bottom-right-radius: ${p => p.theme.borderRadius};
}
`;
/**
* GridHead is the collection of elements that builds the header section of the
* Grid. As the entirety of the add/remove/resize actions are performed on the
* header, most of the elements behave different for each stage.
*/
export const GridHead = styled('thead')`
display: contents;
`;
export const GridHeadCell = styled('th')<{isFirst: boolean; sticky?: boolean}>`
/* By default, a grid item cannot be smaller than the size of its content.
We override this by setting min-width to be 0. */
position: relative; /* Used by GridResizer */
height: ${GRID_HEAD_ROW_HEIGHT}px;
display: flex;
align-items: center;
min-width: 24px;
padding: 0 ${space(2)};
border-right: 1px solid transparent;
border-left: 1px solid transparent;
background-color: ${p => p.theme.backgroundSecondary};
color: ${p => p.theme.subText};
font-size: ${p => p.theme.fontSizeSmall};
font-weight: 600;
text-transform: uppercase;
user-select: none;
${p => (p.sticky ? `position: sticky; top: 0;` : '')}
a,
div,
span {
line-height: 1.1;
color: inherit;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
&:first-child {
border-top-left-radius: ${p => p.theme.borderRadius};
}
&:last-child {
border-top-right-radius: ${p => p.theme.borderRadius};
border-right: none;
}
&:hover {
border-left-color: ${p => (p.isFirst ? 'transparent' : p.theme.border)};
border-right-color: ${p => p.theme.border};
}
`;
/**
* Create spacing/padding similar to GridHeadCellWrapper but
* without interactive aspects.
*/
export const GridHeadCellStatic = styled('th')`
height: ${GRID_HEAD_ROW_HEIGHT}px;
display: flex;
align-items: center;
padding: 0 ${space(2)};
background-color: ${p => p.theme.backgroundSecondary};
font-size: ${p => p.theme.fontSizeSmall};
font-weight: 600;
line-height: 1;
text-transform: uppercase;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
&:first-child {
border-top-left-radius: ${p => p.theme.borderRadius};
padding: ${space(1)} 0 ${space(1)} ${space(3)};
}
`;
/**
* GridBody are the collection of elements that contains and display the data
* of the Grid. They are rather simple.
*/
export const GridBody = styled('tbody')`
display: contents;
> tr:first-child td {
border-top: 1px solid ${p => p.theme.border};
}
`;
export const GridBodyCell = styled('td')`
/* By default, a grid item cannot be smaller than the size of its content.
We override this by setting min-width to be 0. */
min-width: 0;
/* Locking in the height makes calculation for resizer to be easier.
min-height is used to allow a cell to expand and this is used to display
feedback during empty/error state */
min-height: ${GRID_BODY_ROW_HEIGHT}px;
padding: ${space(1)} ${space(2)};
background-color: ${p => p.theme.background};
border-top: 1px solid ${p => p.theme.innerBorder};
display: flex;
flex-direction: column;
justify-content: center;
font-size: ${p => p.theme.fontSizeMedium};
&:first-child {
padding: ${space(1)} 0 ${space(1)} ${space(3)};
}
&:last-child {
border-right: none;
padding: ${space(1)} ${space(2)};
}
`;
const GridStatusWrapper = styled(GridBodyCell)`
grid-column: 1 / -1;
width: 100%;
height: ${GRID_STATUS_MESSAGE_HEIGHT}px;
background-color: transparent;
`;
const GridStatusFloat = styled('div')`
position: absolute;
top: 45px;
left: 0;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: ${GRID_STATUS_MESSAGE_HEIGHT}px;
overflow: hidden;
z-index: ${Z_INDEX_GRID_STATUS};
background: ${p => p.theme.background};
`;
export function GridBodyCellStatus(props) {
return (
<GridStatusWrapper>
<GridStatusFloat>{props.children}</GridStatusFloat>
</GridStatusWrapper>
);
}
/**
* We have a fat GridResizer and we use the ::after pseudo-element to draw
* a thin 1px border.
*
* The right most cell does not have a resizer as resizing from that side does strange things.
*/
export const GridResizer = styled('div')<{dataRows: number}>`
position: absolute;
top: 0px;
right: -6px;
width: 11px;
height: ${p => {
const numOfRows = p.dataRows;
const height = GRID_HEAD_ROW_HEIGHT + numOfRows * GRID_BODY_ROW_HEIGHT;
return height;
}}px;
padding-left: 5px;
padding-right: 5px;
cursor: col-resize;
z-index: ${Z_INDEX_GRID_RESIZER};
/**
* This element allows us to have a fat GridResizer that is easy to hover and
* drag, but still draws an appealing thin line for the border
*/
&::after {
content: ' ';
display: block;
width: 100%; /* Equivalent to 1px */
height: 100%;
}
&:hover::after {
background-color: ${p => p.theme.gray200};
}
/**
* Ensure that this rule is after :hover, otherwise it will flicker when
* the GridResizer is dragged
*/
&:active::after,
&:focus::after {
background-color: ${p => p.theme.purple300};
}
/**
* This element gives the resize handle a more visible knob to grab
*/
&:hover::before {
position: absolute;
top: 0;
left: 2px;
content: ' ';
display: block;
width: 7px;
height: ${GRID_HEAD_ROW_HEIGHT}px;
background-color: ${p => p.theme.purple300};
opacity: 0.4;
}
`;