[MM-55993] Create a composable independent table to be used in Admin reporting (#25611)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
c6b600b186
Коммит
7b49e15824
@@ -21,6 +21,7 @@
|
||||
"@mui/styled-engine-sc": "5.11.11",
|
||||
"@stripe/react-stripe-js": "1.13.0",
|
||||
"@stripe/stripe-js": "1.41.0",
|
||||
"@tanstack/react-table": "8.10.7",
|
||||
"@tippyjs/react": "4.2.6",
|
||||
"@types/color-hash": "1.0.2",
|
||||
"@types/turndown": "5.0.1",
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import moment from 'moment';
|
||||
import React, {useMemo} from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
|
||||
import OverlayTrigger from 'components/overlay_trigger';
|
||||
import Tooltip from 'components/tooltip';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
|
||||
interface Props {
|
||||
date?: number;
|
||||
}
|
||||
|
||||
export function ElapsedDurationCell(props: Props) {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const todaysDate = moment().startOf('day').valueOf();
|
||||
|
||||
const {elapsedDays, exactPassedInDate} = useMemo(() => {
|
||||
const startOfTodayMoment = moment().startOf('day');
|
||||
const passedInDateMoment = moment(props.date);
|
||||
const exactPassedInDate = passedInDateMoment.format(
|
||||
`MMMM DD, Y [${formatMessage({id: 'adminConsole.list.table.exactTime.at', defaultMessage: 'at'})}] hh:mm:ss A`,
|
||||
);
|
||||
|
||||
const startOfPassedInDateMoment = passedInDateMoment.startOf('day');
|
||||
|
||||
// TODO: Use Timestamp component here
|
||||
const elapsedDays = startOfTodayMoment.diff(startOfPassedInDateMoment, 'days');
|
||||
|
||||
return {
|
||||
elapsedDays,
|
||||
exactPassedInDate,
|
||||
};
|
||||
}, [props.date, todaysDate]);
|
||||
|
||||
if (!props.date) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let elapsedDaysText = null;
|
||||
if (elapsedDays < 1) {
|
||||
elapsedDaysText = (
|
||||
<FormattedMessage
|
||||
id='admin.system_users.list.memberSince.today'
|
||||
defaultMessage='Today'
|
||||
/>
|
||||
);
|
||||
} else if (elapsedDays === 1) {
|
||||
elapsedDaysText = (
|
||||
<FormattedMessage
|
||||
id='admin.system_users.list.memberSince.yesterday'
|
||||
defaultMessage='Yesterday'
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
elapsedDaysText = (
|
||||
<FormattedMessage
|
||||
id='admin.system_users.list.memberSince.days'
|
||||
defaultMessage='{days} days'
|
||||
values={{days: elapsedDays}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<OverlayTrigger
|
||||
delayShow={Constants.OVERLAY_TIME_DELAY}
|
||||
placement='bottom'
|
||||
overlay={
|
||||
<Tooltip id='system-users-cell-elapsed-duration-tooltip'>
|
||||
{exactPassedInDate}
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<span>{elapsedDaysText}</span>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
}
|
||||
10
webapp/channels/src/components/admin_console/list_table/index.ts
Обычный файл
10
webapp/channels/src/components/admin_console/list_table/index.ts
Обычный файл
@@ -0,0 +1,10 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export {useReactTable, getCoreRowModel, getSortedRowModel, createColumnHelper} from '@tanstack/react-table';
|
||||
export type {CellContext, PaginationState, SortingState, OnChangeFn, ColumnDef} from '@tanstack/react-table';
|
||||
|
||||
export {ListTable as AdminConsoleListTable, PAGE_SIZES} from './list_table';
|
||||
export type {TableMeta, PageSizeOption} from './list_table';
|
||||
|
||||
export {ElapsedDurationCell} from './elapsed_duration_cell';
|
||||
@@ -0,0 +1,263 @@
|
||||
table.adminConsoleListTable {
|
||||
// This is rgba(var(--sys-center-channel-color-rgb), 0.08) but since we cannot use opacity
|
||||
// due to sticky row which shows the background thus a defined color is used
|
||||
$sysCenterChannelColorWith8Alpha: #f0f0f0;
|
||||
|
||||
width: 100%;
|
||||
|
||||
thead {
|
||||
border-top: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
|
||||
|
||||
tr {
|
||||
color: rgba(var(--sys-center-channel-color-rgb), 0.64);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
text-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
|
||||
th {
|
||||
max-width: 50rem;
|
||||
background-color: var(--sys-center-channel-bg);
|
||||
padding-block-end: 12px;
|
||||
padding-block-start: 12px;
|
||||
|
||||
&:first-child {
|
||||
padding-inline-end: 12px;
|
||||
padding-inline-start: 24px;
|
||||
}
|
||||
|
||||
&:not(:first-child):not(:last-child) {
|
||||
padding-inline-end: 12px;
|
||||
padding-inline-start: 12px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-inline-end: 24px;
|
||||
padding-inline-start: 12px;
|
||||
}
|
||||
|
||||
&.pinned {
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
right: 0;
|
||||
left: 0;
|
||||
min-width: 20rem;
|
||||
background-color: var(--sys-center-channel-bg);
|
||||
|
||||
&:first-child {
|
||||
left: 0;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background-color: rgba(var(--center-channel-color-rgb), 0.08);
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
right: 0;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 1px;
|
||||
background-color: rgba(var(--center-channel-color-rgb), 0.08);
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
|
||||
& > span {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
&.sortable {
|
||||
vertical-align: middle;
|
||||
|
||||
&:hover {
|
||||
background-color: $sysCenterChannelColorWith8Alpha;
|
||||
cursor: pointer;
|
||||
|
||||
& > span.hoverSortingIcon {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
& > span {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 14px;
|
||||
|
||||
&.hoverSortingIcon {
|
||||
color: rgba(var(--sys-center-channel-color-rgb), 0.32);
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.sortable.pinned {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
& > span {
|
||||
&:first-child {
|
||||
flex-grow: 1;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.pinned:not(.sortable) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
border-top: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
|
||||
background-color: var(--sys-center-channel-bg);
|
||||
color: var(--sys-center-channel-color);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
|
||||
&:hover {
|
||||
background-color: $sysCenterChannelColorWith8Alpha;
|
||||
cursor: pointer;
|
||||
|
||||
// Since the sticky element creates a new stacking context and doesnt automatically inherit the background color
|
||||
// set the background color of the sticky element to the same as the on hover
|
||||
td.pinned {
|
||||
background-color: $sysCenterChannelColorWith8Alpha;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
overflow: hidden;
|
||||
padding-block-end: 16px;
|
||||
padding-block-start: 16px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&:first-child {
|
||||
padding-inline-end: 12px;
|
||||
padding-inline-start: 24px;
|
||||
}
|
||||
|
||||
&:not(:first-child):not(:last-child) {
|
||||
padding-inline-end: 12px;
|
||||
padding-inline-start: 12px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-inline-end: 24px;
|
||||
padding-inline-start: 12px;
|
||||
}
|
||||
|
||||
// Pinning of columns can only be done on the first and/or last column
|
||||
&.pinned {
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
overflow: visible;
|
||||
background-color: var(--sys-center-channel-bg);
|
||||
|
||||
&:first-child {
|
||||
left: 0;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background-color: rgba(var(--center-channel-color-rgb), 0.08);
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
right: 0;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 1px;
|
||||
background-color: rgba(var(--center-channel-color-rgb), 0.08);
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tfoot {
|
||||
border-top: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.adminConsoleListTabletOptionalFoot,
|
||||
.adminConsoleListTabletOptionalHead {
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
left: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
color: rgba(var(--sys-center-channel-color-rgb), 0.6);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
padding-block-end: 12px;
|
||||
padding-block-start: 12px;
|
||||
padding-inline-end: 24px;
|
||||
padding-inline-start: 24px;
|
||||
|
||||
.adminConsoleListTablePageSize {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
& > .react-select {
|
||||
min-width: 64px;
|
||||
margin: 0 6px;
|
||||
|
||||
.react-select__value-container {
|
||||
color: rgba(var(--sys-center-channel-color-rgb), 1);
|
||||
}
|
||||
|
||||
.react-select__menu-list {
|
||||
color: rgba(var(--sys-center-channel-color-rgb), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.paginationButtons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
& > span {
|
||||
padding-inline-end: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {SortDirection, Table} from '@tanstack/react-table';
|
||||
import {flexRender} from '@tanstack/react-table';
|
||||
import classNames from 'classnames';
|
||||
import React, {useMemo} from 'react';
|
||||
import type {AriaAttributes, MouseEvent, ReactNode} from 'react';
|
||||
import {FormattedMessage, defineMessages, useIntl} from 'react-intl';
|
||||
import ReactSelect, {components} from 'react-select';
|
||||
import type {IndicatorContainerProps, ValueType} from 'react-select';
|
||||
|
||||
import OverlayTrigger from 'components/overlay_trigger';
|
||||
import Tooltip from 'components/tooltip';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
|
||||
import {Pagination} from './pagination';
|
||||
|
||||
import './list_table.scss';
|
||||
|
||||
const SORTABLE_CLASS = 'sortable';
|
||||
const PINNED_CLASS = 'pinned';
|
||||
|
||||
export const PAGE_SIZES = [10, 20, 50, 100];
|
||||
const PageSizes = defineMessages<number>({
|
||||
10: {
|
||||
id: 'adminConsole.list.table.rowsCount.10',
|
||||
defaultMessage: '10',
|
||||
},
|
||||
20: {
|
||||
id: 'adminConsole.list.table.rowsCount.20',
|
||||
defaultMessage: '20',
|
||||
},
|
||||
50: {
|
||||
id: 'adminConsole.list.table.rowsCount.50',
|
||||
defaultMessage: '50',
|
||||
},
|
||||
100: {
|
||||
id: 'adminConsole.list.table.rowsCount.100',
|
||||
defaultMessage: '100',
|
||||
},
|
||||
});
|
||||
|
||||
export type PageSizeOption = {
|
||||
label: string;
|
||||
value: number;
|
||||
};
|
||||
|
||||
export type TableMeta = {
|
||||
tableId: string;
|
||||
tableCaption?: string;
|
||||
isLoading?: boolean;
|
||||
onRowClick?: (row: string) => void;
|
||||
disablePrevPage?: boolean;
|
||||
disableNextPage?: boolean;
|
||||
onPreviousPageClick?: () => void;
|
||||
onNextPageClick?: () => void;
|
||||
paginationInfo?: ReactNode;
|
||||
totalRowInfo?: ReactNode;
|
||||
hasAdditionalPaginationAtTop?: boolean;
|
||||
};
|
||||
|
||||
interface TableMandatoryTypes {
|
||||
id: string;
|
||||
}
|
||||
|
||||
type Props<TableType extends TableMandatoryTypes> = {
|
||||
table: Table<TableType>;
|
||||
};
|
||||
|
||||
/**
|
||||
* A wrapper around the react-table component that provides a consistent look and feel for the admin console list tables.
|
||||
* It also provides a default pagination component. This table is not meant to be used outside of the admin console since it relies on the admin console styles.
|
||||
*
|
||||
* @param {Table} table - See https://tanstack.com/table/v8/docs/api/core/table/ for more details
|
||||
*/
|
||||
export function ListTable<TableType extends TableMandatoryTypes>(
|
||||
props: Props<TableType>,
|
||||
) {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const tableMeta = props.table.options.meta as TableMeta;
|
||||
const headerIdPrefix = `${tableMeta.tableId}-header-`;
|
||||
const rowIdPrefix = `${tableMeta.tableId}-row-`;
|
||||
const cellIdPrefix = `${tableMeta.tableId}-cell-`;
|
||||
|
||||
const pageSizeOptions = useMemo(() => {
|
||||
return PAGE_SIZES.map((size) => {
|
||||
return {
|
||||
label: formatMessage(PageSizes[size]),
|
||||
value: size,
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const selectedPageSize = pageSizeOptions.find((option) => option.value === props.table.getState().pagination.pageSize) || pageSizeOptions[0];
|
||||
|
||||
function handlePageSizeChange(selectedOption: ValueType<PageSizeOption>) {
|
||||
const {value} = selectedOption as PageSizeOption;
|
||||
props.table.setPageSize(Number(value));
|
||||
}
|
||||
|
||||
function handleRowClick(event: MouseEvent<HTMLTableRowElement>) {
|
||||
const {currentTarget: {id = ''}} = event;
|
||||
const rowOriginalId = id.replace(rowIdPrefix, '');
|
||||
|
||||
if (tableMeta.onRowClick && rowOriginalId.length > 0) {
|
||||
event.preventDefault();
|
||||
tableMeta.onRowClick(rowOriginalId);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='adminConsoleListTabletOptionalHead'>
|
||||
{tableMeta.totalRowInfo}
|
||||
{tableMeta.hasAdditionalPaginationAtTop && (
|
||||
<Pagination
|
||||
disablePrevPage={tableMeta.disablePrevPage}
|
||||
disableNextPage={tableMeta.disableNextPage}
|
||||
isLoading={tableMeta.isLoading}
|
||||
onPreviousPageClick={tableMeta.onPreviousPageClick}
|
||||
onNextPageClick={tableMeta.onNextPageClick}
|
||||
paginationInfo={tableMeta.paginationInfo}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<table
|
||||
id={tableMeta.tableId}
|
||||
aria-colcount={props.table.getAllColumns().length}
|
||||
aria-describedby={`${tableMeta.tableId}-headerId`} // Set this id to the table header so that the title describes the table
|
||||
className={classNames(
|
||||
'adminConsoleListTable',
|
||||
tableMeta.tableId,
|
||||
)}
|
||||
>
|
||||
<caption className='sr-only'>{tableMeta.tableCaption}</caption>
|
||||
<thead>
|
||||
{props.table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<th
|
||||
key={header.id}
|
||||
id={`${headerIdPrefix}${header.id}`}
|
||||
colSpan={header.colSpan}
|
||||
scope='col'
|
||||
role='columnheader'
|
||||
aria-sort={getAriaSortForTableHeader(header.column.getCanSort(), header.column.getIsSorted())}
|
||||
className={classNames(`${header.id}`, {
|
||||
[SORTABLE_CLASS]: header.column.getCanSort(),
|
||||
[PINNED_CLASS]: header.column.getCanPin(),
|
||||
})}
|
||||
onClick={header.column.getToggleSortingHandler()}
|
||||
>
|
||||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||
|
||||
{/* Sort Icons */}
|
||||
{header.column.getIsSorted() === 'asc' && (
|
||||
<span
|
||||
aria-hidden='true'
|
||||
className='icon icon-arrow-up'
|
||||
/>
|
||||
)}
|
||||
{header.column.getIsSorted() === 'desc' && (
|
||||
<span
|
||||
aria-hidden='true'
|
||||
className='icon icon-arrow-down'
|
||||
/>
|
||||
)}
|
||||
{header.column.getCanSort() &&
|
||||
header.column.getIsSorted() !== 'asc' &&
|
||||
header.column.getIsSorted() !== 'desc' && (
|
||||
<span
|
||||
aria-hidden='true'
|
||||
className='icon icon-arrow-up hoverSortingIcon'
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Pinned Icon */}
|
||||
{header.column.getCanPin() && (
|
||||
<OverlayTrigger
|
||||
delayShow={Constants.OVERLAY_TIME_DELAY}
|
||||
placement={
|
||||
header.index === 0 ? 'bottom' : 'left'
|
||||
}
|
||||
overlay={
|
||||
<Tooltip id='system-users-column-pinned-tooltip'>
|
||||
<FormattedMessage
|
||||
id='adminConsole.list.table.pinnedColumn'
|
||||
defaultMessage='This column is pinned'
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<span
|
||||
aria-hidden='true'
|
||||
className='icon icon-pin-outline'
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody>
|
||||
{props.table.getRowModel().rows.map((row) => (
|
||||
<tr
|
||||
id={`${rowIdPrefix}${row.original.id}`}
|
||||
key={row.id}
|
||||
role='row'
|
||||
onClick={handleRowClick}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<td
|
||||
key={cell.id}
|
||||
id={`${cellIdPrefix}${cell.id}`}
|
||||
role='cell'
|
||||
headers={`${headerIdPrefix}${cell.column.id}`}
|
||||
className={classNames(`${cell.column.id}`, {
|
||||
[PINNED_CLASS]: cell.column.getCanPin(),
|
||||
})}
|
||||
>
|
||||
{cell.getIsPlaceholder() ? null : flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
{props.table.getFooterGroups().map((footerGroup) => (
|
||||
<tr key={footerGroup.id}>
|
||||
{footerGroup.headers.map((footer) => (
|
||||
<th
|
||||
key={footer.id}
|
||||
colSpan={footer.colSpan}
|
||||
className={classNames({
|
||||
[PINNED_CLASS]: footer.column.getCanPin(),
|
||||
})}
|
||||
>
|
||||
{footer.isPlaceholder ? null : flexRender(footer.column.columnDef.footer, footer.getContext())}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tfoot>
|
||||
</table>
|
||||
<div className='adminConsoleListTabletOptionalFoot'>
|
||||
{handlePageSizeChange && (
|
||||
<div
|
||||
className='adminConsoleListTablePageSize'
|
||||
aria-label={formatMessage(
|
||||
{
|
||||
id: 'adminConsole.list.table.rowCount.label',
|
||||
defaultMessage: 'Show {count} rows per page',
|
||||
},
|
||||
{count: selectedPageSize.label},
|
||||
)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='adminConsole.list.table.rowsCount.(show)rowsPerPage'
|
||||
defaultMessage='Show'
|
||||
/>
|
||||
<ReactSelect
|
||||
className='react-select'
|
||||
classNamePrefix='react-select'
|
||||
autoFocus={false}
|
||||
isClearable={false}
|
||||
isMulti={false}
|
||||
isSearchable={false}
|
||||
menuPlacement='top'
|
||||
options={pageSizeOptions}
|
||||
value={selectedPageSize}
|
||||
onChange={handlePageSizeChange}
|
||||
isDisabled={tableMeta.isLoading}
|
||||
components={{
|
||||
IndicatorSeparator: null,
|
||||
IndicatorsContainer,
|
||||
}}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='adminConsole.list.table.rowsCount.show(rowsPerPage)'
|
||||
defaultMessage='rows per page'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Pagination
|
||||
disablePrevPage={tableMeta.disablePrevPage}
|
||||
disableNextPage={tableMeta.disableNextPage}
|
||||
isLoading={tableMeta.isLoading}
|
||||
onPreviousPageClick={tableMeta.onPreviousPageClick}
|
||||
onNextPageClick={tableMeta.onNextPageClick}
|
||||
paginationInfo={tableMeta.paginationInfo}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function IndicatorsContainer(props: IndicatorContainerProps<PageSizeOption>) {
|
||||
return (
|
||||
<components.IndicatorsContainer {...props}>
|
||||
<i className='icon icon-chevron-down'/>
|
||||
</components.IndicatorsContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function getAriaSortForTableHeader(
|
||||
canSort: boolean,
|
||||
sortDirection: boolean | SortDirection,
|
||||
): AriaAttributes['aria-sort'] {
|
||||
if (!canSort) {
|
||||
return 'none';
|
||||
}
|
||||
|
||||
if (sortDirection === 'asc') {
|
||||
return 'ascending';
|
||||
}
|
||||
|
||||
if (sortDirection === 'desc') {
|
||||
return 'descending';
|
||||
}
|
||||
|
||||
return 'none';
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import type {TableMeta} from './list_table';
|
||||
|
||||
type Props = Pick<
|
||||
TableMeta,
|
||||
| 'onPreviousPageClick'
|
||||
| 'onNextPageClick'
|
||||
| 'disablePrevPage'
|
||||
| 'disableNextPage'
|
||||
| 'isLoading'
|
||||
| 'paginationInfo'
|
||||
>;
|
||||
|
||||
export function Pagination(props: Props) {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
return (
|
||||
<div className='paginationButtons'>
|
||||
{props.paginationInfo}
|
||||
{props.onPreviousPageClick && (
|
||||
<button
|
||||
className='btn btn-icon btn-sm'
|
||||
disabled={props.disablePrevPage || props.isLoading}
|
||||
onClick={props.onPreviousPageClick}
|
||||
aria-label={formatMessage({id: 'adminConsole.list.table.pagination.previous', defaultMessage: 'Go to previous page'})}
|
||||
>
|
||||
<i
|
||||
className='icon icon-chevron-left'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
{props.onNextPageClick && (
|
||||
<button
|
||||
className='btn btn-icon btn-sm'
|
||||
disabled={props.disableNextPage || props.isLoading}
|
||||
onClick={props.onNextPageClick}
|
||||
aria-label={formatMessage({id: 'adminConsole.list.table.pagination.next', defaultMessage: 'Go to next page'})}
|
||||
>
|
||||
<i
|
||||
className='icon icon-chevron-right'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -40,6 +40,7 @@ type MenuButtonProps = {
|
||||
id: string;
|
||||
dateTestId?: string;
|
||||
'aria-label'?: string;
|
||||
disabled?: boolean;
|
||||
class?: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
@@ -174,6 +175,7 @@ export function Menu(props: Props) {
|
||||
aria-controls={props.menu.id}
|
||||
aria-haspopup={true}
|
||||
aria-expanded={isMenuOpen}
|
||||
disabled={props.menuButton?.disabled ?? false}
|
||||
aria-label={props.menuButton?.['aria-label'] ?? ''}
|
||||
className={props.menuButton?.class ?? ''}
|
||||
onClick={handleMenuButtonClick}
|
||||
|
||||
@@ -2446,6 +2446,9 @@
|
||||
"admin.system_users.allUsers": "All Users",
|
||||
"admin.system_users.guest": "Guest",
|
||||
"admin.system_users.inactive": "Inactive",
|
||||
"admin.system_users.list.memberSince.days": "{days} days",
|
||||
"admin.system_users.list.memberSince.today": "Today",
|
||||
"admin.system_users.list.memberSince.yesterday": "Yesterday",
|
||||
"admin.system_users.noTeams": "No Teams",
|
||||
"admin.system_users.revoke_all_sessions_button": "Revoke All Sessions",
|
||||
"admin.system_users.revoke_all_sessions_modal_message": "This action revokes all sessions in the system. All users will be logged out from all devices, including your session. Are you sure you want to revoke all sessions?",
|
||||
@@ -2670,6 +2673,17 @@
|
||||
"admin.webserverModeTitle": "Webserver Mode:",
|
||||
"admin.webserverModeUncompressed": "Uncompressed",
|
||||
"admin.webserverModeUncompressedDescription": "The Mattermost server will serve static files uncompressed.",
|
||||
"adminConsole.list.table.exactTime.at": "at",
|
||||
"adminConsole.list.table.pagination.next": "Go to next page",
|
||||
"adminConsole.list.table.pagination.previous": "Go to previous page",
|
||||
"adminConsole.list.table.pinnedColumn": "This column is pinned",
|
||||
"adminConsole.list.table.rowCount.label": "Show {count} rows per page",
|
||||
"adminConsole.list.table.rowsCount.(show)rowsPerPage": "Show",
|
||||
"adminConsole.list.table.rowsCount.10": "10",
|
||||
"adminConsole.list.table.rowsCount.100": "100",
|
||||
"adminConsole.list.table.rowsCount.20": "20",
|
||||
"adminConsole.list.table.rowsCount.50": "50",
|
||||
"adminConsole.list.table.rowsCount.show(rowsPerPage)": "rows per page",
|
||||
"air_gapped_modal.close": "Close",
|
||||
"air_gapped_modal.description": "To start your trial, please visit {link} and request a trial key.",
|
||||
"air_gapped_modal.title": "Request a trial key",
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
height: 100%;
|
||||
color: #333;
|
||||
grid-area: center;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
> div {
|
||||
width: 100%;
|
||||
@@ -30,12 +29,8 @@
|
||||
.admin-console__wrapper {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
-webkit-flex: 1 1 auto;
|
||||
flex: 1 1 auto;
|
||||
padding: 20px;
|
||||
margin-right: 5px;
|
||||
scrollbar-color: var(--center-channel-color-32) #fff0;
|
||||
scrollbar-width: thin;
|
||||
|
||||
> .wrapper--admin {
|
||||
display: flex;
|
||||
@@ -43,6 +38,12 @@
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
& > .admin-console__container {
|
||||
overflow: auto;
|
||||
background-color: var(--sys-center-channel-bg);
|
||||
box-shadow: 0 2px 3px 0 alpha-color($black, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.admin-console__content {
|
||||
|
||||
@@ -74,7 +74,6 @@
|
||||
|
||||
@mixin clearfix {
|
||||
overflow: hidden;
|
||||
*zoom: 1;
|
||||
}
|
||||
|
||||
@mixin pie-clearfix {
|
||||
@@ -83,8 +82,6 @@
|
||||
clear: both;
|
||||
content: "";
|
||||
}
|
||||
|
||||
*zoom: 1;
|
||||
}
|
||||
|
||||
@mixin tertiary-button {
|
||||
|
||||
32
webapp/package-lock.json
сгенерированный
32
webapp/package-lock.json
сгенерированный
@@ -67,6 +67,7 @@
|
||||
"@mui/styled-engine-sc": "5.11.11",
|
||||
"@stripe/react-stripe-js": "1.13.0",
|
||||
"@stripe/stripe-js": "1.41.0",
|
||||
"@tanstack/react-table": "8.10.7",
|
||||
"@tippyjs/react": "4.2.6",
|
||||
"@types/color-hash": "1.0.2",
|
||||
"@types/turndown": "5.0.1",
|
||||
@@ -4643,6 +4644,37 @@
|
||||
"postcss-syntax": ">=0.36.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-table": {
|
||||
"version": "8.10.7",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.10.7.tgz",
|
||||
"integrity": "sha512-bXhjA7xsTcsW8JPTTYlUg/FuBpn8MNjiEPhkNhIGCUR6iRQM2+WEco4OBpvDeVcR9SE+bmWLzdfiY7bCbCSVuA==",
|
||||
"dependencies": {
|
||||
"@tanstack/table-core": "8.10.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16",
|
||||
"react-dom": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/table-core": {
|
||||
"version": "8.10.7",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.10.7.tgz",
|
||||
"integrity": "sha512-KQk5OMg5OH6rmbHZxuNROvdI+hKDIUxANaHlV+dPlNN7ED3qYQ/WkpY2qlXww1SIdeMlkIhpN/2L00rof0fXFw==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/dom": {
|
||||
"version": "8.20.1",
|
||||
"dev": true,
|
||||
|
||||
Ссылка в новой задаче
Block a user