github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/web/src/OverviewTable.tsx (about) 1 import { 2 Accordion, 3 AccordionDetails, 4 AccordionSummary, 5 } from "@material-ui/core" 6 import React, { 7 ChangeEvent, 8 MouseEvent, 9 MutableRefObject, 10 useEffect, 11 useMemo, 12 useRef, 13 useState, 14 } from "react" 15 import { 16 HeaderGroup, 17 Row, 18 SortingRule, 19 TableHeaderProps, 20 TableOptions, 21 TableState, 22 usePagination, 23 useSortBy, 24 UseSortByState, 25 useTable, 26 } from "react-table" 27 import styled from "styled-components" 28 import { buildAlerts, runtimeAlerts } from "./alerts" 29 import { AnalyticsType, Tags } from "./analytics" 30 import { ApiButtonType, buttonsForComponent } from "./ApiButton" 31 import { 32 DEFAULT_RESOURCE_LIST_LIMIT, 33 RESOURCE_LIST_MULTIPLIER, 34 } from "./constants" 35 import Features, { Flag, useFeatures } from "./feature" 36 import { Hold } from "./Hold" 37 import { 38 getResourceLabels, 39 GroupByLabelView, 40 orderLabels, 41 TILTFILE_LABEL, 42 UNLABELED_LABEL, 43 } from "./labels" 44 import { LogAlertIndex, useLogAlertIndex } from "./LogStore" 45 import { 46 COLUMNS, 47 ResourceTableHeaderTip, 48 rowIsDisabled, 49 RowValues, 50 } from "./OverviewTableColumns" 51 import { OverviewTableKeyboardShortcuts } from "./OverviewTableKeyboardShortcuts" 52 import { 53 AccordionDetailsStyleResetMixin, 54 AccordionStyleResetMixin, 55 AccordionSummaryStyleResetMixin, 56 ResourceGroupsInfoTip, 57 ResourceGroupSummaryIcon, 58 ResourceGroupSummaryMixin, 59 } from "./ResourceGroups" 60 import { useResourceGroups } from "./ResourceGroupsContext" 61 import { 62 ResourceListOptions, 63 useResourceListOptions, 64 } from "./ResourceListOptionsContext" 65 import { matchesResourceName } from "./ResourceNameFilter" 66 import { useResourceSelection } from "./ResourceSelectionContext" 67 import { resourceIsDisabled, resourceTargetType } from "./ResourceStatus" 68 import { TableGroupStatusSummary } from "./ResourceStatusSummary" 69 import { ShowMoreButton } from "./ShowMoreButton" 70 import { buildStatus, runtimeStatus } from "./status" 71 import { Color, Font, FontSize, SizeUnit } from "./style-helpers" 72 import { isZeroTime, timeDiff } from "./time" 73 import { 74 ResourceName, 75 ResourceStatus, 76 TargetType, 77 TriggerMode, 78 UIButton, 79 UIResource, 80 UIResourceStatus, 81 } from "./types" 82 83 export type OverviewTableProps = { 84 view: Proto.webviewView 85 } 86 87 type TableWrapperProps = { 88 resources?: UIResource[] 89 buttons?: UIButton[] 90 } 91 92 type TableGroupProps = { 93 label: string 94 setGlobalSortBy: (id: string) => void 95 focused: string 96 } & TableOptions<RowValues> 97 98 type TableProps = { 99 setGlobalSortBy?: (id: string) => void 100 focused: string 101 } & TableOptions<RowValues> 102 103 type ResourceTableHeadRowProps = { 104 headerGroup: HeaderGroup<RowValues> 105 setGlobalSortBy?: (id: string) => void 106 } & TableHeaderProps 107 108 // Resource name filter styles 109 export const ResourceResultCount = styled.p` 110 color: ${Color.gray50}; 111 font-size: ${FontSize.small}; 112 margin-top: ${SizeUnit(0.5)}; 113 margin-left: ${SizeUnit(0.5)}; 114 text-transform: uppercase; 115 ` 116 117 export const NoMatchesFound = styled.p` 118 color: ${Color.grayLightest}; 119 margin-left: ${SizeUnit(0.5)}; 120 margin-top: ${SizeUnit(1 / 4)}; 121 ` 122 123 // Table styles 124 const OverviewTableRoot = styled.section` 125 padding-bottom: ${SizeUnit(1 / 2)}; 126 margin-left: auto; 127 margin-right: auto; 128 /* Max and min width are based on fixed table layout and column widths */ 129 max-width: 2000px; 130 min-width: 1400px; 131 132 @media screen and (max-width: 2200px) { 133 margin-left: ${SizeUnit(1 / 2)}; 134 margin-right: ${SizeUnit(1 / 2)}; 135 } 136 ` 137 138 const TableWithoutGroupsRoot = styled.div` 139 box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); 140 border: 1px ${Color.gray40} solid; 141 border-radius: 0px 0px 8px 8px; 142 background-color: ${Color.gray20}; 143 ` 144 145 const ResourceTable = styled.table` 146 table-layout: fixed; 147 width: 100%; 148 border-spacing: 0; 149 border-collapse: collapse; 150 151 td { 152 padding-left: 10px; 153 padding-right: 10px; 154 } 155 156 td:first-child { 157 padding-left: 24px; 158 } 159 160 td:last-child { 161 padding-right: ${SizeUnit(1)}; 162 } 163 ` 164 const ResourceTableHead = styled.thead` 165 & > tr { 166 background-color: ${Color.gray10}; 167 } 168 ` 169 170 export const ResourceTableRow = styled.tr` 171 border-top: 1px solid ${Color.gray40}; 172 font-family: ${Font.monospace}; 173 font-size: ${FontSize.small}; 174 font-style: none; 175 color: ${Color.gray60}; 176 padding-top: 6px; 177 padding-bottom: 6px; 178 padding-left: 4px; 179 180 &.isFocused, 181 &:focus { 182 border-left: 4px solid ${Color.blue}; 183 outline: none; 184 185 td:first-child { 186 padding-left: 22px; 187 } 188 } 189 190 &.isSelected { 191 background-color: ${Color.gray30}; 192 } 193 194 /* For visual consistency on rows */ 195 &.isFixedHeight { 196 height: ${SizeUnit(1.4)}; 197 } 198 ` 199 export const ResourceTableData = styled.td` 200 box-sizing: border-box; 201 202 &.isSorted { 203 background-color: ${Color.gray30}; 204 } 205 206 &.alignRight { 207 text-align: right; 208 } 209 ` 210 211 export const ResourceTableHeader = styled(ResourceTableData)` 212 color: ${Color.gray70}; 213 font-size: ${FontSize.small}; 214 box-sizing: border-box; 215 white-space: nowrap; 216 217 &.isSorted { 218 background-color: ${Color.gray20}; 219 } 220 ` 221 222 const ResourceTableHeaderLabel = styled.div` 223 display: flex; 224 align-items: center; 225 user-select: none; 226 ` 227 228 export const ResourceTableHeaderSortTriangle = styled.div` 229 display: inline-block; 230 margin-left: ${SizeUnit(0.25)}; 231 width: 0; 232 height: 0; 233 border-left: 5px solid transparent; 234 border-right: 5px solid transparent; 235 border-bottom: 6px solid ${Color.gray50}; 236 237 &.is-sorted-asc { 238 border-bottom: 6px solid ${Color.blue}; 239 } 240 &.is-sorted-desc { 241 border-bottom: 6px solid ${Color.blue}; 242 transform: rotate(180deg); 243 } 244 ` 245 246 // Table Group styles 247 export const OverviewGroup = styled(Accordion)` 248 ${AccordionStyleResetMixin} 249 color: ${Color.gray50}; 250 border: 1px ${Color.gray40} solid; 251 background-color: ${Color.gray20}; 252 253 &.MuiAccordion-root, 254 &.MuiAccordion-root.Mui-expanded { 255 margin-top: ${SizeUnit(1 / 2)}; 256 } 257 258 &.Mui-expanded { 259 box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); 260 border-radius: 0px 0px 8px 8px; 261 } 262 ` 263 264 export const OverviewGroupSummary = styled(AccordionSummary)` 265 ${AccordionSummaryStyleResetMixin} 266 ${ResourceGroupSummaryMixin} 267 background-color: ${Color.gray10}; 268 269 .MuiAccordionSummary-content { 270 font-size: ${FontSize.default}; 271 } 272 ` 273 274 export const OverviewGroupName = styled.span` 275 padding: 0 ${SizeUnit(1 / 3)}; 276 ` 277 278 export const OverviewGroupDetails = styled(AccordionDetails)` 279 ${AccordionDetailsStyleResetMixin} 280 ` 281 const TABLE_TYPE_TAGS: Tags = { type: AnalyticsType.Grid } 282 283 const GROUP_INFO_TOOLTIP_ID = "table-groups-info" 284 285 export function TableResourceResultCount(props: { resources?: UIResource[] }) { 286 const { options } = useResourceListOptions() 287 288 if ( 289 props.resources === undefined || 290 options.resourceNameFilter.length === 0 291 ) { 292 return null 293 } 294 295 const count = props.resources.length 296 297 return ( 298 <ResourceResultCount> 299 {count} result{count !== 1 ? "s" : ""} 300 </ResourceResultCount> 301 ) 302 } 303 304 export function TableNoMatchesFound(props: { resources?: UIResource[] }) { 305 const { options } = useResourceListOptions() 306 307 if (props.resources?.length === 0 && options.resourceNameFilter.length > 0) { 308 return <NoMatchesFound>No matching resources</NoMatchesFound> 309 } 310 311 return null 312 } 313 314 const FIRST_SORT_STATE = false 315 const SECOND_SORT_STATE = true 316 317 // This helper function manually implements the toggle sorting 318 // logic used by react-table, so we can keep the sorting state 319 // globally and sort multiple tables by the same column. 320 // Click once to sort by ascending values 321 // Click twice to sort by descending values 322 // Click thrice to remove sort 323 // Note: this does NOT support sorting by multiple columns. 324 function calculateNextSort( 325 id: string, 326 sortByState: SortingRule<RowValues>[] | undefined 327 ): SortingRule<RowValues>[] { 328 if (!sortByState || sortByState.length === 0) { 329 return [{ id, desc: FIRST_SORT_STATE }] 330 } 331 332 // If the current sort is the same column as next sort, 333 // determine its next value 334 const [currentSort] = sortByState 335 if (currentSort.id === id) { 336 const { desc } = currentSort 337 338 if (desc === undefined) { 339 return [{ id, desc: FIRST_SORT_STATE }] 340 } 341 342 if (desc === FIRST_SORT_STATE) { 343 return [{ id, desc: SECOND_SORT_STATE }] 344 } 345 346 if (desc === SECOND_SORT_STATE) { 347 return [] 348 } 349 } 350 351 return [{ id, desc: FIRST_SORT_STATE }] 352 } 353 354 function applyOptionsToResources( 355 resources: UIResource[] | undefined, 356 options: ResourceListOptions, 357 features: Features 358 ): UIResource[] { 359 if (!resources) { 360 return [] 361 } 362 363 const hideDisabledResources = !options.showDisabledResources 364 const resourceNameFilter = options.resourceNameFilter.length > 0 365 366 // If there are no options to apply to the resources, return the un-filtered, sorted list 367 if (!resourceNameFilter && !hideDisabledResources) { 368 return sortByDisableStatus(resources) 369 } 370 371 // Otherwise, apply the options to the resources and sort it 372 const filteredResources = resources.filter((r) => { 373 const resourceDisabled = resourceIsDisabled(r) 374 if (hideDisabledResources && resourceDisabled) { 375 return false 376 } 377 378 if (resourceNameFilter) { 379 return matchesResourceName( 380 r.metadata?.name || "", 381 options.resourceNameFilter 382 ) 383 } 384 385 return true 386 }) 387 388 return sortByDisableStatus(filteredResources) 389 } 390 391 function uiResourceToCell( 392 r: UIResource, 393 allButtons: UIButton[] | undefined, 394 alertIndex: LogAlertIndex 395 ): RowValues { 396 let res = (r.status || {}) as UIResourceStatus 397 let buildHistory = res.buildHistory || [] 398 let lastBuild = buildHistory.length > 0 ? buildHistory[0] : null 399 let lastBuildDur = 400 lastBuild?.startTime && lastBuild?.finishTime 401 ? timeDiff(lastBuild.startTime, lastBuild.finishTime) 402 : null 403 let currentBuildStartTime = res.currentBuild?.startTime ?? "" 404 let isBuilding = !isZeroTime(currentBuildStartTime) 405 let hasBuilt = lastBuild !== null 406 let buttons = buttonsForComponent( 407 allButtons, 408 ApiButtonType.Resource, 409 r.metadata?.name 410 ) 411 let analyticsTags = { target: resourceTargetType(r) } 412 // Consider a resource `selectable` if it can be disabled 413 const selectable = !!buttons.toggleDisable 414 415 return { 416 lastDeployTime: res.lastDeployTime ?? "", 417 trigger: { 418 isBuilding: isBuilding, 419 hasBuilt: hasBuilt, 420 hasPendingChanges: !!res.hasPendingChanges, 421 isQueued: !!res.queued, 422 }, 423 name: r.metadata?.name ?? "", 424 resourceTypeLabel: resourceTypeLabel(r), 425 statusLine: { 426 buildStatus: buildStatus(r, alertIndex), 427 buildAlertCount: buildAlerts(r, alertIndex).length, 428 lastBuildDur: lastBuildDur, 429 runtimeStatus: runtimeStatus(r, alertIndex), 430 runtimeAlertCount: runtimeAlerts(r, alertIndex).length, 431 hold: res.waiting ? new Hold(res.waiting) : null, 432 }, 433 podId: res.k8sResourceInfo?.podName ?? "", 434 endpoints: res.endpointLinks ?? [], 435 mode: res.triggerMode ?? TriggerMode.TriggerModeAuto, 436 buttons: buttons, 437 analyticsTags: analyticsTags, 438 selectable, 439 } 440 } 441 442 function resourceTypeLabel(r: UIResource): string { 443 let res = (r.status || {}) as UIResourceStatus 444 let name = r.metadata?.name 445 if (name == "(Tiltfile)") { 446 return "Tiltfile" 447 } 448 let specs = res.specs ?? [] 449 for (let i = 0; i < specs.length; i++) { 450 let spec = specs[i] 451 if (spec.type === TargetType.K8s) { 452 return "K8s" 453 } else if (spec.type === TargetType.DockerCompose) { 454 return "DCS" 455 } else if (spec.type === TargetType.Local) { 456 return "Local" 457 } 458 } 459 return "Unknown" 460 } 461 462 function sortByDisableStatus(resources: UIResource[] = []) { 463 // Sort by disabled status, so disabled resources appear at the end of each table list. 464 // Note: this initial sort is done here so it doesn't interfere with the sorting 465 // managed by react-table 466 const sorted = [...resources].sort((a, b) => { 467 const resourceAOrder = resourceIsDisabled(a) ? 1 : 0 468 const resourceBOrder = resourceIsDisabled(b) ? 1 : 0 469 470 return resourceAOrder - resourceBOrder 471 }) 472 473 return sorted 474 } 475 476 function onlyEnabledRows(rows: RowValues[]): RowValues[] { 477 return rows.filter( 478 (row) => row.statusLine.runtimeStatus !== ResourceStatus.Disabled 479 ) 480 } 481 function onlyDisabledRows(rows: RowValues[]): RowValues[] { 482 return rows.filter( 483 (row) => row.statusLine.runtimeStatus === ResourceStatus.Disabled 484 ) 485 } 486 function enabledRowsFirst(rows: RowValues[]): RowValues[] { 487 let result = onlyEnabledRows(rows) 488 result.push(...onlyDisabledRows(rows)) 489 return result 490 } 491 492 export function labeledResourcesToTableCells( 493 resources: UIResource[] | undefined, 494 buttons: UIButton[] | undefined, 495 logAlertIndex: LogAlertIndex 496 ): GroupByLabelView<RowValues> { 497 const labelsToResources: { [key: string]: RowValues[] } = {} 498 const unlabeled: RowValues[] = [] 499 const tiltfile: RowValues[] = [] 500 501 if (resources === undefined) { 502 return { labels: [], labelsToResources, tiltfile, unlabeled } 503 } 504 505 resources.forEach((r) => { 506 const labels = getResourceLabels(r) 507 const isTiltfile = r.metadata?.name === ResourceName.tiltfile 508 const tableCell = uiResourceToCell(r, buttons, logAlertIndex) 509 if (labels.length) { 510 labels.forEach((label) => { 511 if (!labelsToResources.hasOwnProperty(label)) { 512 labelsToResources[label] = [] 513 } 514 515 labelsToResources[label].push(tableCell) 516 }) 517 } else if (isTiltfile) { 518 tiltfile.push(tableCell) 519 } else { 520 unlabeled.push(tableCell) 521 } 522 }) 523 524 // Labels are always displayed in sorted order 525 const labels = orderLabels(Object.keys(labelsToResources)) 526 527 return { labels, labelsToResources, tiltfile, unlabeled } 528 } 529 530 export function ResourceTableHeadRow({ 531 headerGroup, 532 setGlobalSortBy, 533 }: ResourceTableHeadRowProps) { 534 const calculateToggleProps = (column: HeaderGroup<RowValues>) => { 535 // If a column header is JSX, fall back on using its id as a descriptive title 536 // and capitalize for consistency 537 const columnHeader = 538 typeof column.Header === "string" 539 ? column.Header 540 : `${column.id[0]?.toUpperCase()}${column.id?.slice(1)}` 541 542 // Warning! Toggle props are not typed or documented well within react-table. 543 // Modify toggle props with caution. 544 // See https://react-table.tanstack.com/docs/api/useSortBy#column-properties 545 const toggleProps: { [key: string]: any } = { 546 title: column.canSort ? `Sort by ${columnHeader}` : columnHeader, 547 } 548 549 if (setGlobalSortBy && column.canSort) { 550 // The sort state is global whenever there are multiple tables, so 551 // pass a click handler to the sort toggle that changes the global state 552 toggleProps.onClick = () => setGlobalSortBy(column.id) 553 } 554 555 return toggleProps 556 } 557 558 const calculateHeaderProps = (column: HeaderGroup<RowValues>) => { 559 const headerProps: Partial<TableHeaderProps> = { 560 style: { width: column.width }, 561 } 562 563 if (column.isSorted) { 564 headerProps.className = "isSorted" 565 } 566 567 return headerProps 568 } 569 570 return ( 571 <ResourceTableRow> 572 {headerGroup.headers.map((column) => ( 573 <ResourceTableHeader 574 {...column.getHeaderProps([ 575 calculateHeaderProps(column), 576 column.getSortByToggleProps(calculateToggleProps(column)), 577 ])} 578 > 579 <ResourceTableHeaderLabel> 580 {column.render("Header")} 581 <ResourceTableHeaderTip id={String(column.id)} /> 582 {column.canSort && ( 583 <ResourceTableHeaderSortTriangle 584 className={ 585 column.isSorted 586 ? column.isSortedDesc 587 ? "is-sorted-desc" 588 : "is-sorted-asc" 589 : "" 590 } 591 /> 592 )} 593 </ResourceTableHeaderLabel> 594 </ResourceTableHeader> 595 ))} 596 </ResourceTableRow> 597 ) 598 } 599 600 function ShowMoreResourcesRow({ 601 colSpan, 602 itemCount, 603 pageSize, 604 onClick, 605 }: { 606 colSpan: number 607 itemCount: number 608 pageSize: number 609 onClick: (e: MouseEvent) => void 610 }) { 611 if (itemCount <= pageSize) { 612 return null 613 } 614 615 return ( 616 <ResourceTableRow className="isFixedHeight"> 617 <ResourceTableData colSpan={colSpan - 2} /> 618 <ResourceTableData className="alignRight" colSpan={2}> 619 <ShowMoreButton 620 itemCount={itemCount} 621 currentListSize={pageSize} 622 onClick={onClick} 623 analyticsTags={TABLE_TYPE_TAGS} 624 /> 625 </ResourceTableData> 626 </ResourceTableRow> 627 ) 628 } 629 630 function TableRow(props: { row: Row<RowValues>; focused: string }) { 631 let { row, focused } = props 632 const { isSelected } = useResourceSelection() 633 let isFocused = row.original.name == focused 634 let rowClasses = 635 (rowIsDisabled(row) ? "isDisabled " : "") + 636 (isSelected(row.original.name) ? "isSelected " : "") + 637 (isFocused ? "isFocused " : "") 638 let ref: MutableRefObject<HTMLTableRowElement | null> = useRef(null) 639 640 useEffect(() => { 641 if (isFocused && ref.current) { 642 ref.current.focus() 643 } 644 }, [isFocused, ref]) 645 646 return ( 647 <ResourceTableRow 648 tabIndex={-1} 649 ref={ref} 650 {...row.getRowProps({ 651 className: rowClasses, 652 })} 653 > 654 {row.cells.map((cell) => ( 655 <ResourceTableData 656 {...cell.getCellProps()} 657 className={cell.column.isSorted ? "isSorted" : ""} 658 > 659 {cell.render("Cell")} 660 </ResourceTableData> 661 ))} 662 </ResourceTableRow> 663 ) 664 } 665 666 export function Table(props: TableProps) { 667 if (props.data.length === 0) { 668 return null 669 } 670 671 const { 672 getTableProps, 673 getTableBodyProps, 674 headerGroups, 675 rows, // Used to calculate the total number of rows 676 page, // Used to render the rows for the current page 677 prepareRow, 678 state: { pageSize }, 679 setPageSize, 680 } = useTable( 681 { 682 columns: props.columns, 683 data: props.data, 684 autoResetSortBy: false, 685 useControlledState: props.useControlledState, 686 initialState: { pageSize: DEFAULT_RESOURCE_LIST_LIMIT }, 687 }, 688 useSortBy, 689 usePagination 690 ) 691 692 const showMoreOnClick = () => setPageSize(pageSize * RESOURCE_LIST_MULTIPLIER) 693 694 // TODO (lizz): Consider adding `aria-sort` markup to table headings 695 return ( 696 <ResourceTable {...getTableProps()}> 697 <ResourceTableHead> 698 {headerGroups.map((headerGroup: HeaderGroup<RowValues>) => ( 699 <ResourceTableHeadRow 700 {...headerGroup.getHeaderGroupProps()} 701 headerGroup={headerGroup} 702 setGlobalSortBy={props.setGlobalSortBy} 703 /> 704 ))} 705 </ResourceTableHead> 706 <tbody {...getTableBodyProps()}> 707 {page.map((row: Row<RowValues>) => { 708 prepareRow(row) 709 return ( 710 <TableRow 711 key={row.original.name} 712 row={row} 713 focused={props.focused} 714 /> 715 ) 716 })} 717 <ShowMoreResourcesRow 718 itemCount={rows.length} 719 pageSize={pageSize} 720 onClick={showMoreOnClick} 721 colSpan={props.columns.length} 722 /> 723 </tbody> 724 </ResourceTable> 725 ) 726 } 727 728 function TableGroup(props: TableGroupProps) { 729 const { label, ...tableProps } = props 730 731 if (tableProps.data.length === 0) { 732 return null 733 } 734 735 const formattedLabel = label === UNLABELED_LABEL ? <em>{label}</em> : label 736 const labelNameId = `tableOverview-${label}` 737 738 const { getGroup, toggleGroupExpanded } = useResourceGroups() 739 const { expanded } = getGroup(label) 740 const handleChange = (_e: ChangeEvent<{}>) => 741 toggleGroupExpanded(label, AnalyticsType.Grid) 742 743 return ( 744 <OverviewGroup expanded={expanded} onChange={handleChange}> 745 <OverviewGroupSummary id={labelNameId}> 746 <ResourceGroupSummaryIcon role="presentation" /> 747 <OverviewGroupName>{formattedLabel}</OverviewGroupName> 748 <TableGroupStatusSummary 749 labelText={`Status summary for ${label} group`} 750 resources={tableProps.data} 751 /> 752 </OverviewGroupSummary> 753 <OverviewGroupDetails> 754 <Table {...tableProps} /> 755 </OverviewGroupDetails> 756 </OverviewGroup> 757 ) 758 } 759 760 export function TableGroupedByLabels({ 761 resources, 762 buttons, 763 }: TableWrapperProps) { 764 const features = useFeatures() 765 const logAlertIndex = useLogAlertIndex() 766 const data = useMemo( 767 () => labeledResourcesToTableCells(resources, buttons, logAlertIndex), 768 [resources, buttons] 769 ) 770 771 const totalOrder = useMemo(() => { 772 let totalOrder = [] 773 data.labels.forEach((label) => 774 totalOrder.push(...enabledRowsFirst(data.labelsToResources[label])) 775 ) 776 totalOrder.push(...enabledRowsFirst(data.unlabeled)) 777 totalOrder.push(...enabledRowsFirst(data.tiltfile)) 778 return totalOrder 779 }, [data]) 780 let [focused, setFocused] = useState("") 781 782 // Global table settings are currently used to sort multiple 783 // tables by the same column 784 // See: https://react-table.tanstack.com/docs/faq#how-can-i-manually-control-the-table-state 785 const [globalTableSettings, setGlobalTableSettings] = 786 useState<UseSortByState<RowValues>>() 787 788 const useControlledState = (state: TableState<RowValues>) => 789 useMemo(() => { 790 return { ...state, ...globalTableSettings } 791 }, [state, globalTableSettings]) 792 793 const setGlobalSortBy = (columnId: string) => { 794 const sortBy = calculateNextSort(columnId, globalTableSettings?.sortBy) 795 setGlobalTableSettings({ sortBy }) 796 } 797 798 return ( 799 <> 800 {data.labels.map((label) => ( 801 <TableGroup 802 key={label} 803 label={label} 804 data={data.labelsToResources[label]} 805 columns={COLUMNS} 806 useControlledState={useControlledState} 807 setGlobalSortBy={setGlobalSortBy} 808 focused={focused} 809 /> 810 ))} 811 <TableGroup 812 label={UNLABELED_LABEL} 813 data={data.unlabeled} 814 columns={COLUMNS} 815 useControlledState={useControlledState} 816 setGlobalSortBy={setGlobalSortBy} 817 focused={focused} 818 /> 819 <TableGroup 820 label={TILTFILE_LABEL} 821 data={data.tiltfile} 822 columns={COLUMNS} 823 useControlledState={useControlledState} 824 setGlobalSortBy={setGlobalSortBy} 825 focused={focused} 826 /> 827 <OverviewTableKeyboardShortcuts 828 focused={focused} 829 setFocused={setFocused} 830 rows={totalOrder} 831 /> 832 </> 833 ) 834 } 835 836 export function TableWithoutGroups({ resources, buttons }: TableWrapperProps) { 837 const features = useFeatures() 838 const logAlertIndex = useLogAlertIndex() 839 const data = useMemo(() => { 840 return ( 841 resources?.map((r) => uiResourceToCell(r, buttons, logAlertIndex)) || [] 842 ) 843 }, [resources, buttons]) 844 845 let totalOrder = useMemo(() => enabledRowsFirst(data), [data]) 846 let [focused, setFocused] = useState("") 847 848 if (resources?.length === 0) { 849 return null 850 } 851 852 return ( 853 <TableWithoutGroupsRoot> 854 <Table columns={COLUMNS} data={data} focused={focused} /> 855 <OverviewTableKeyboardShortcuts 856 focused={focused} 857 setFocused={setFocused} 858 rows={totalOrder} 859 /> 860 </TableWithoutGroupsRoot> 861 ) 862 } 863 864 function OverviewTableContent(props: OverviewTableProps) { 865 const features = useFeatures() 866 const labelsEnabled = features.isEnabled(Flag.Labels) 867 const resourcesHaveLabels = 868 props.view.uiResources?.some((r) => getResourceLabels(r).length > 0) || 869 false 870 871 const { options } = useResourceListOptions() 872 const resourceFilterApplied = options.resourceNameFilter.length > 0 873 874 // Apply any display filters or options to resources, plus sort for initial view 875 const resourcesToDisplay = applyOptionsToResources( 876 props.view.uiResources, 877 options, 878 features 879 ) 880 881 // Table groups are displayed when feature is enabled, resources have labels, 882 // and no resource name filter is applied 883 const displayResourceGroups = 884 labelsEnabled && resourcesHaveLabels && !resourceFilterApplied 885 886 if (displayResourceGroups) { 887 return ( 888 <TableGroupedByLabels 889 resources={resourcesToDisplay} 890 buttons={props.view.uiButtons} 891 /> 892 ) 893 } else { 894 // The label group tip is only displayed if labels are enabled but not used 895 const displayLabelGroupsTip = labelsEnabled && !resourcesHaveLabels 896 897 return ( 898 <> 899 {displayLabelGroupsTip && ( 900 <ResourceGroupsInfoTip idForIcon={GROUP_INFO_TOOLTIP_ID} /> 901 )} 902 <TableResourceResultCount resources={resourcesToDisplay} /> 903 <TableNoMatchesFound resources={resourcesToDisplay} /> 904 <TableWithoutGroups 905 aria-describedby={ 906 displayLabelGroupsTip ? GROUP_INFO_TOOLTIP_ID : undefined 907 } 908 resources={resourcesToDisplay} 909 buttons={props.view.uiButtons} 910 /> 911 </> 912 ) 913 } 914 } 915 916 export default function OverviewTable(props: OverviewTableProps) { 917 return ( 918 <OverviewTableRoot aria-label="Resources overview"> 919 <OverviewTableContent {...props} /> 920 </OverviewTableRoot> 921 ) 922 }