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