diff --git a/server_manager/www/ui_components/outline_data_table.ts b/server_manager/www/ui_components/outline_data_table.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/server_manager/www/views/server_view/server_data_table/index.ts b/server_manager/www/views/server_view/server_data_table/index.ts new file mode 100644 index 0000000000..210f78e1b4 --- /dev/null +++ b/server_manager/www/views/server_view/server_data_table/index.ts @@ -0,0 +1,203 @@ +/** + * Copyright 2024 The Outline Authors + * + * 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 {css, html, LitElement, TemplateResult, nothing} from 'lit'; +import {customElement, property} from 'lit/decorators.js'; +import {classMap} from 'lit/directives/class-map.js'; + +const DEFAULT_COMPARATOR = (value1: string, value2: string): -1 | 0 | 1 => { + if (value1 === value2) return 0; + if (value1 < value2) return -1; + if (value1 > value2) return 1; +}; + +const DEFAULT_RENDER = (value: string): TemplateResult<1> => { + return html`${value}`; +}; + +@customElement('server-data-table') +export class ServerDataTable extends LitElement { + @property({type: Array}) columns: Map< + string, + { + comparator?: (_value1: string, _value2: string) => -1 | 0 | 1; + render?: (_value: string) => TemplateResult<1>; + } + >; + @property({type: Array}) data: {[columnName: string]: string}[]; + + @property({type: String}) sortColumn?: string; + @property({type: String}) sortDescending?: boolean; + + static styles = css` + .table-container { + container-type: size; + } + + .table { + display: grid; + grid-template-columns: repeat(var(--server-data-table-columns), auto); + } + + .table-header { + font-weight: bold; + color: white; + background-color: hsl(200, 19%, 18%); + position: sticky; + top: 0; + z-index: 1; + } + + .table-row { + background-color: white; + } + + .table-row-shaded { + background-color: hsl(0, 0%, 95%); + } + + .table-header, + .table-row { + box-sizing: border-box; + content-visibility: auto; + font-family: Roboto, system-ui; + padding: 1rem; + } + + .table-row-label { + display: none; + font-weight: bold; + text-transform: uppercase; + font-size: 0.75rem; + margin: 0.25rem 0; + } + + @container (max-width: 540px) { + .table { + grid-template-columns: auto; + } + + .table-header { + display: none; + } + + .table-row { + padding: 0.25rem 1rem; + } + + .table-row-empty { + padding-top: 0; + padding-bottom: 0; + } + + .table-row-start { + padding-top: 1rem; + } + + .table-row-end { + padding-bottom: 1rem; + } + + .table-row-label { + display: block; + } + } + `; + + private get columnNames() { + return [...this.columns.keys()]; + } + + private get transformedData() { + if (this.sortColumn) { + const comparator = + this.columns.get(this.sortColumn)?.comparator ?? DEFAULT_COMPARATOR; + + return this.data.sort((row1, row2) => { + const [value1, value2] = [row1[this.sortColumn], row2[this.sortColumn]]; + + if (this.sortDescending) { + return comparator(value2, value1); + } + + return comparator(value1, value2); + }); + } + + return this.data; + } + + render() { + return html` +
+ +
+ ${this.columnNames.map((columnName: string) => { + return this.renderTableHeaderCell(columnName); + })} + ${this.transformedData.flatMap((row, rowIndex) => { + return this.columnNames.map((columnName, columnIndex) => { + return this.renderTableDataCell( + columnName, + columnIndex, + row[columnName], + rowIndex + ); + }); + })} +
+
+ `; + } + + renderTableHeaderCell(columnName: string) { + if (this.sortColumn === columnName) { + return html`
+ ${columnName} ${this.sortDescending ? '↑' : '↓'} +
`; + } + + return html`
${columnName}
`; + } + + renderTableDataCell( + columnName: string, + columnIndex: number, + rowValue: string, + rowIndex: number + ) { + const dataCellContents = rowValue + ? html`
${columnName}
+ ${(this.columns.get(columnName)?.render ?? DEFAULT_RENDER)(rowValue)}` + : nothing; + + return html`
+ ${dataCellContents} +
`; + } +} diff --git a/server_manager/www/views/server_view/server_data_table/stories.ts b/server_manager/www/views/server_view/server_data_table/stories.ts new file mode 100644 index 0000000000..a5c9ef34d3 --- /dev/null +++ b/server_manager/www/views/server_view/server_data_table/stories.ts @@ -0,0 +1,130 @@ +import {html} from 'lit'; + +import './index'; +import {ServerDataTable} from './index'; + +export default { + title: 'Manager/Server View/Server Data Table', + component: 'server-data-table', + args: { + columns: [ + ['id', {}], + ['value', {}], + ], + data: [ + {id: '0', value: 'value-0'}, + {id: '1', value: 'value-1'}, + {id: '2', value: 'value-2'}, + ], + sortColumn: 'id', + sortDescending: true, + }, +}; + +export const BasicExample = ({ + columns, + data, + sortColumn, + sortDescending, +}: ServerDataTable) => { + return html``; +}; + +export const RenderExample = () => { + return html` + html`${tag}` + )}`; + }, + }, + ], + ])} + .data=${[ + { + 'Employee Name': 'Vini', + Tags: 'Lead,IC,Manager', + }, + { + 'Employee Name': 'Sander', + Tags: 'Lead,IC', + }, + { + 'Employee Name': 'Jyyi', + Tags: 'IC', + }, + { + 'Employee Name': 'Daniel', + Tags: 'IC', + }, + ]} + />`; +}; + +export const ComparatorExample = () => { + return html``; +}; + +export const HeavyDataExample = () => { + const data = []; + + let index = 1000; + while (index--) { + data.push({ + id: String(index), + value: String(index), + }); + } + + return html``; +};