[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",
|
"@mui/styled-engine-sc": "5.11.11",
|
||||||
"@stripe/react-stripe-js": "1.13.0",
|
"@stripe/react-stripe-js": "1.13.0",
|
||||||
"@stripe/stripe-js": "1.41.0",
|
"@stripe/stripe-js": "1.41.0",
|
||||||
|
"@tanstack/react-table": "8.10.7",
|
||||||
"@tippyjs/react": "4.2.6",
|
"@tippyjs/react": "4.2.6",
|
||||||
"@types/color-hash": "1.0.2",
|
"@types/color-hash": "1.0.2",
|
||||||
"@types/turndown": "5.0.1",
|
"@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;
|
id: string;
|
||||||
dateTestId?: string;
|
dateTestId?: string;
|
||||||
'aria-label'?: string;
|
'aria-label'?: string;
|
||||||
|
disabled?: boolean;
|
||||||
class?: string;
|
class?: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
@@ -174,6 +175,7 @@ export function Menu(props: Props) {
|
|||||||
aria-controls={props.menu.id}
|
aria-controls={props.menu.id}
|
||||||
aria-haspopup={true}
|
aria-haspopup={true}
|
||||||
aria-expanded={isMenuOpen}
|
aria-expanded={isMenuOpen}
|
||||||
|
disabled={props.menuButton?.disabled ?? false}
|
||||||
aria-label={props.menuButton?.['aria-label'] ?? ''}
|
aria-label={props.menuButton?.['aria-label'] ?? ''}
|
||||||
className={props.menuButton?.class ?? ''}
|
className={props.menuButton?.class ?? ''}
|
||||||
onClick={handleMenuButtonClick}
|
onClick={handleMenuButtonClick}
|
||||||
|
|||||||
@@ -2446,6 +2446,9 @@
|
|||||||
"admin.system_users.allUsers": "All Users",
|
"admin.system_users.allUsers": "All Users",
|
||||||
"admin.system_users.guest": "Guest",
|
"admin.system_users.guest": "Guest",
|
||||||
"admin.system_users.inactive": "Inactive",
|
"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.noTeams": "No Teams",
|
||||||
"admin.system_users.revoke_all_sessions_button": "Revoke All Sessions",
|
"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?",
|
"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.webserverModeTitle": "Webserver Mode:",
|
||||||
"admin.webserverModeUncompressed": "Uncompressed",
|
"admin.webserverModeUncompressed": "Uncompressed",
|
||||||
"admin.webserverModeUncompressedDescription": "The Mattermost server will serve static files 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.close": "Close",
|
||||||
"air_gapped_modal.description": "To start your trial, please visit {link} and request a trial key.",
|
"air_gapped_modal.description": "To start your trial, please visit {link} and request a trial key.",
|
||||||
"air_gapped_modal.title": "Request a trial key",
|
"air_gapped_modal.title": "Request a trial key",
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
color: #333;
|
color: #333;
|
||||||
grid-area: center;
|
grid-area: center;
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
|
|
||||||
> div {
|
> div {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -30,12 +29,8 @@
|
|||||||
.admin-console__wrapper {
|
.admin-console__wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
-webkit-flex: 1 1 auto;
|
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
margin-right: 5px;
|
|
||||||
scrollbar-color: var(--center-channel-color-32) #fff0;
|
|
||||||
scrollbar-width: thin;
|
|
||||||
|
|
||||||
> .wrapper--admin {
|
> .wrapper--admin {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -43,6 +38,12 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
flex-direction: column;
|
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 {
|
.admin-console__content {
|
||||||
|
|||||||
@@ -74,7 +74,6 @@
|
|||||||
|
|
||||||
@mixin clearfix {
|
@mixin clearfix {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
*zoom: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin pie-clearfix {
|
@mixin pie-clearfix {
|
||||||
@@ -83,8 +82,6 @@
|
|||||||
clear: both;
|
clear: both;
|
||||||
content: "";
|
content: "";
|
||||||
}
|
}
|
||||||
|
|
||||||
*zoom: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin tertiary-button {
|
@mixin tertiary-button {
|
||||||
|
|||||||
32
webapp/package-lock.json
сгенерированный
32
webapp/package-lock.json
сгенерированный
@@ -67,6 +67,7 @@
|
|||||||
"@mui/styled-engine-sc": "5.11.11",
|
"@mui/styled-engine-sc": "5.11.11",
|
||||||
"@stripe/react-stripe-js": "1.13.0",
|
"@stripe/react-stripe-js": "1.13.0",
|
||||||
"@stripe/stripe-js": "1.41.0",
|
"@stripe/stripe-js": "1.41.0",
|
||||||
|
"@tanstack/react-table": "8.10.7",
|
||||||
"@tippyjs/react": "4.2.6",
|
"@tippyjs/react": "4.2.6",
|
||||||
"@types/color-hash": "1.0.2",
|
"@types/color-hash": "1.0.2",
|
||||||
"@types/turndown": "5.0.1",
|
"@types/turndown": "5.0.1",
|
||||||
@@ -4643,6 +4644,37 @@
|
|||||||
"postcss-syntax": ">=0.36.2"
|
"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": {
|
"node_modules/@testing-library/dom": {
|
||||||
"version": "8.20.1",
|
"version": "8.20.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user