[MM-53990] Support a global retention time of less than 1 day (#25196)

* adding new MessageRetentionHours config

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Ben Cooke
2024-01-09 14:10:22 -05:00
коммит произвёл GitHub
родитель 82b8d4dc07
Коммит bb88b92b4c
18 изменённых файлов: 376 добавлений и 47 удалений

Просмотреть файл

@@ -13,12 +13,16 @@ describe('components/admin_console/data_retention_settings/data_retention_settin
EnableMessageDeletion: true,
EnableFileDeletion: true,
MessageRetentionDays: 100,
MessageRetentionHours: 2400,
FileRetentionDays: 100,
FileRetentionHours: 2400,
DeletionJobStartTime: '00:15',
},
},
customPolicies: {},
customPoliciesCount: 0,
globalMessageRetentionHours: '2400',
globalFileRetentionHours: '2400',
actions: {
getDataRetentionCustomPolicies: jest.fn().mockResolvedValue([]),
createJob: jest.fn(),

Просмотреть файл

@@ -37,6 +37,8 @@ type Props = {
config: DeepPartial<AdminConfig>;
customPolicies: DataRetentionCustomPolicies;
customPoliciesCount: number;
globalMessageRetentionHours: string | undefined;
globalFileRetentionHours: string | undefined;
actions: {
getDataRetentionCustomPolicies: (page: number) => Promise<{ data: DataRetentionCustomPolicies }>;
createJob: (job: JobTypeBase) => Promise<{ data: any }>;
@@ -147,6 +149,52 @@ export default class DataRetentionSettings extends React.PureComponent<Props, St
];
return columns;
};
getGlobalRetentionSetting = (enabled: boolean | undefined, hours: string | undefined): JSX.Element => {
if (!enabled) {
return (
<FormattedMessage
id='admin.data_retention.form.keepForever'
defaultMessage='Keep forever'
/>
);
}
const hoursInt = parseInt(hours || '', 10);
if (hoursInt && hoursInt % 8760 === 0) {
const years = hoursInt / 8760;
return (
<FormattedMessage
id='admin.data_retention.retention_years'
defaultMessage='{count} {count, plural, one {year} other {years}}'
values={{
count: `${years}`,
}}
/>
);
}
if (hoursInt && hoursInt % 24 === 0) {
const days = hoursInt / 24;
return (
<FormattedMessage
id='admin.data_retention.retention_days'
defaultMessage='{count} {count, plural, one {day} other {days}}'
values={{
count: `${days}`,
}}
/>
);
}
return (
<FormattedMessage
id='admin.data_retention.retention_hours'
defaultMessage='{count} {count, plural, one {hour} other {hours}}'
values={{
count: `${hours}`,
}}
/>
);
};
getMessageRetentionSetting = (enabled: boolean | undefined, days: number | undefined): JSX.Element => {
if (!enabled) {
return (
@@ -185,12 +233,12 @@ export default class DataRetentionSettings extends React.PureComponent<Props, St
description: Utils.localizeMessage('admin.data_retention.form.text', 'Applies to all teams and channels, but does not apply to custom retention policies.'),
channel_messages: (
<div data-testid='global_message_retention_cell'>
{this.getMessageRetentionSetting(DataRetentionSettings?.EnableMessageDeletion, DataRetentionSettings?.MessageRetentionDays)}
{this.getGlobalRetentionSetting(DataRetentionSettings?.EnableMessageDeletion, this.props.globalMessageRetentionHours)}
</div>
),
files: (
<div data-testid='global_file_retention_cell'>
{this.getMessageRetentionSetting(DataRetentionSettings?.EnableFileDeletion, DataRetentionSettings?.FileRetentionDays)}
{this.getGlobalRetentionSetting(DataRetentionSettings?.EnableFileDeletion, this.props.globalFileRetentionHours)}
</div>
),
actions: (

Просмотреть файл

@@ -8,6 +8,8 @@ import * as Utils from 'utils/utils';
export const FOREVER = 'FOREVER';
export const YEARS = 'YEARS';
export const DAYS = 'DAYS';
export const HOURS = 'HOURS';
export const keepForeverOption = () => ({value: FOREVER, label: <div><i className='icon icon-infinity option-icon'/><span className='option_forever'>{Utils.localizeMessage('admin.data_retention.form.keepForever', 'Keep forever')}</span></div>});
export const yearsOption = () => ({value: YEARS, label: <span className='option_years'>{Utils.localizeMessage('admin.data_retention.form.years', 'Years')}</span>});
export const daysOption = () => ({value: DAYS, label: <span className='option_days'>{Utils.localizeMessage('admin.data_retention.form.days', 'Days')}</span>});
export const hoursOption = () => ({value: HOURS, label: <span className='option_hours'>{Utils.localizeMessage('admin.data_retention.form.hours', 'Hours')}</span>});

Просмотреть файл

@@ -62,13 +62,21 @@ exports[`components/PluginManagement should match snapshot 1`] = `
}
inputId="channel_message_retention_input"
inputType="number"
inputValue="60"
inputValue="100"
legend="Channel & direct message retention"
name="channel_message_retention"
onDropdownChange={[Function]}
onInputChange={[Function]}
options={
Array [
Object {
"label": <span
className="option_hours"
>
Hours
</span>,
"value": "HOURS",
},
Object {
"label": <span
className="option_days"
@@ -141,13 +149,21 @@ exports[`components/PluginManagement should match snapshot 1`] = `
}
inputId="file_retention_input"
inputType="number"
inputValue="40"
inputValue="100"
legend="File retention"
name="file_retention"
onDropdownChange={[Function]}
onInputChange={[Function]}
options={
Array [
Object {
"label": <span
className="option_hours"
>
Hours
</span>,
"value": "HOURS",
},
Object {
"label": <span
className="option_days"

Просмотреть файл

@@ -48,6 +48,12 @@
background-color: var(--sys-center-channel-bg);
color: rgba(var(--sys-center-channel-color-rgb), 0.64);
}
.alert {
&.alert-warning {
margin-top: 10px;
}
}
}
.CustomPolicy__error {

Просмотреть файл

@@ -12,11 +12,14 @@ describe('components/PluginManagement', () => {
DataRetentionSettings: {
EnableMessageDeletion: true,
EnableFileDeletion: true,
MessageRetentionDays: 60,
FileRetentionDays: 40,
MessageRetentionHours: 1440,
FileRetentionHours: 960,
DeletionJobStartTime: '10:00',
},
},
messageRetentionHours: '2400',
fileRetentionHours: '2400',
environmentConfig: {},
actions: {
updateConfig: jest.fn(),
setNavigationBlocked: jest.fn(),

Просмотреть файл

@@ -4,12 +4,13 @@
import React from 'react';
import {FormattedMessage} from 'react-intl';
import type {AdminConfig} from '@mattermost/types/config';
import type {AdminConfig, EnvironmentConfig} from '@mattermost/types/config';
import type {ServerError} from '@mattermost/types/errors';
import type {DeepPartial} from '@mattermost/types/utilities';
import BlockableLink from 'components/admin_console/blockable_link';
import {keepForeverOption, yearsOption, daysOption, FOREVER, YEARS, DAYS} from 'components/admin_console/data_retention_settings/dropdown_options/dropdown_options';
import {keepForeverOption, yearsOption, daysOption, FOREVER, YEARS, DAYS, hoursOption} from 'components/admin_console/data_retention_settings/dropdown_options/dropdown_options';
import SetByEnv from 'components/admin_console/set_by_env';
import Card from 'components/card/card';
import SaveButton from 'components/save_button';
import AdminHeader from 'components/widgets/admin_console/admin_header';
@@ -26,6 +27,9 @@ type ValueType = {
}
type Props = {
config: DeepPartial<AdminConfig>;
messageRetentionHours: string | undefined;
fileRetentionHours: string | undefined;
environmentConfig: Partial<EnvironmentConfig>;
actions: {
updateConfig: (config: Record<string, any>) => Promise<{ data?: AdminConfig; error?: ServerError }>;
setNavigationBlocked: (blocked: boolean) => void;
@@ -51,30 +55,44 @@ export default class GlobalPolicyForm extends React.PureComponent<Props, State>
saving: false,
serverError: null,
formErrorText: '',
messageRetentionDropdownValue: this.getDefaultDropdownValue(DataRetentionSettings?.EnableMessageDeletion, DataRetentionSettings?.MessageRetentionDays),
messageRetentionInputValue: this.getDefaultInputValue(DataRetentionSettings?.EnableMessageDeletion, DataRetentionSettings?.MessageRetentionDays),
fileRetentionDropdownValue: this.getDefaultDropdownValue(DataRetentionSettings?.EnableFileDeletion, DataRetentionSettings?.FileRetentionDays),
fileRetentionInputValue: this.getDefaultInputValue(DataRetentionSettings?.EnableFileDeletion, DataRetentionSettings?.FileRetentionDays),
messageRetentionDropdownValue: this.getDefaultDropdownValue(DataRetentionSettings?.EnableMessageDeletion, props.messageRetentionHours),
messageRetentionInputValue: this.getDefaultInputValue(DataRetentionSettings?.EnableMessageDeletion, props.messageRetentionHours),
fileRetentionDropdownValue: this.getDefaultDropdownValue(DataRetentionSettings?.EnableFileDeletion, props.fileRetentionHours),
fileRetentionInputValue: this.getDefaultInputValue(DataRetentionSettings?.EnableFileDeletion, props.fileRetentionHours),
};
}
getDefaultInputValue = (isEnabled: boolean | undefined, days: number | undefined): string => {
if (!isEnabled || days === undefined) {
getDefaultInputValue = (isEnabled: boolean | undefined, hours: string | undefined): string => {
if (!isEnabled || hours === undefined) {
return '';
}
if (days % 365 === 0) {
return (days / 365).toString();
const hoursInt = parseInt(hours, 10);
// 8760 hours in a year
if (hoursInt % 8760 === 0) {
return (hoursInt / 8760).toString();
}
return days.toString();
if (hoursInt % 24 === 0) {
return (hoursInt / 24).toString();
}
return hours.toString();
};
getDefaultDropdownValue = (isEnabled: boolean | undefined, days: number | undefined) => {
if (!isEnabled || days === undefined) {
getDefaultDropdownValue = (isEnabled: boolean | undefined, hours: string | undefined) => {
if (!isEnabled || hours === undefined) {
return keepForeverOption();
}
if (days % 365 === 0) {
const hoursInt = parseInt(hours, 10);
// 8760 hours in a year
if (hoursInt % 8760 === 0) {
return yearsOption();
}
return daysOption();
if (hoursInt % 24 === 0) {
return daysOption();
}
return hoursOption();
};
handleSubmit = async () => {
@@ -90,16 +108,16 @@ export default class GlobalPolicyForm extends React.PureComponent<Props, State>
newConfig.DataRetentionSettings.EnableMessageDeletion = this.setDeletionEnabled(messageRetentionDropdownValue.value);
const messageDays = this.setRetentionDays(messageRetentionDropdownValue.value, messageRetentionInputValue);
if (messageDays >= 1) {
newConfig.DataRetentionSettings.MessageRetentionDays = messageDays;
if (!this.isMessageRetentionSetByEnv() && this.setDeletionEnabled(messageRetentionDropdownValue.value)) {
newConfig.DataRetentionSettings.MessageRetentionDays = 0;
newConfig.DataRetentionSettings.MessageRetentionHours = this.setRetentionHours(messageRetentionDropdownValue.value, messageRetentionInputValue);
}
newConfig.DataRetentionSettings.EnableFileDeletion = this.setDeletionEnabled(fileRetentionDropdownValue.value);
const fileDays = this.setRetentionDays(fileRetentionDropdownValue.value, fileRetentionInputValue);
if (fileDays >= 1) {
newConfig.DataRetentionSettings.FileRetentionDays = fileDays;
if (!this.isFileRetentionSetByEnv() && this.setDeletionEnabled(fileRetentionDropdownValue.value)) {
newConfig.DataRetentionSettings.FileRetentionDays = 0;
newConfig.DataRetentionSettings.FileRetentionHours = this.setRetentionHours(fileRetentionDropdownValue.value, fileRetentionInputValue);
}
const {error} = await this.props.actions.updateConfig(newConfig);
@@ -119,16 +137,26 @@ export default class GlobalPolicyForm extends React.PureComponent<Props, State>
return true;
};
setRetentionDays = (dropdownValue: string, value: string): number => {
setRetentionHours = (dropdownValue: string, value: string): number => {
if (dropdownValue === YEARS) {
return parseInt(value, 10) * 365;
return parseInt(value, 10) * 24 * 365;
}
if (dropdownValue === DAYS) {
return parseInt(value, 10);
return parseInt(value, 10) * 24;
}
return parseInt(value, 10);
};
return 0;
isMessageRetentionSetByEnv = () => {
return (this.props.environmentConfig?.DataRetentionSettings?.MessageRetentionDays && this.props.config.DataRetentionSettings?.MessageRetentionDays && this.props.config.DataRetentionSettings.MessageRetentionDays > 0) ||
(this.props.environmentConfig?.DataRetentionSettings?.MessageRetentionHours && this.props.config.DataRetentionSettings?.MessageRetentionHours && this.props.config.DataRetentionSettings.MessageRetentionHours > 0) ||
(this.props.environmentConfig?.DataRetentionSettings?.EnableMessageDeletion && !this.props.config.DataRetentionSettings?.EnableMessageDeletion);
};
isFileRetentionSetByEnv = () => {
return (this.props.environmentConfig?.DataRetentionSettings?.FileRetentionDays && this.props.config.DataRetentionSettings?.FileRetentionDays && this.props.config.DataRetentionSettings.FileRetentionDays > 0) ||
(this.props.environmentConfig?.DataRetentionSettings?.FileRetentionHours && this.props.config.DataRetentionSettings?.FileRetentionHours && this.props.config.DataRetentionSettings.FileRetentionHours > 0) ||
(this.props.environmentConfig?.DataRetentionSettings?.EnableFileDeletion && !this.props.config.DataRetentionSettings?.EnableFileDeletion);
};
render = () => {
@@ -173,8 +201,9 @@ export default class GlobalPolicyForm extends React.PureComponent<Props, State>
inputValue={this.state.messageRetentionInputValue}
width={90}
exceptionToInput={[FOREVER]}
disabled={this.isMessageRetentionSetByEnv()}
defaultValue={keepForeverOption()}
options={[daysOption(), yearsOption(), keepForeverOption()]}
options={[hoursOption(), daysOption(), yearsOption(), keepForeverOption()]}
legend={Utils.localizeMessage('admin.data_retention.form.channelAndDirectMessageRetention', 'Channel & direct message retention')}
placeholder={Utils.localizeMessage('admin.data_retention.form.channelAndDirectMessageRetention', 'Channel & direct message retention')}
name={'channel_message_retention'}
@@ -182,6 +211,7 @@ export default class GlobalPolicyForm extends React.PureComponent<Props, State>
dropdownClassNamePrefix={'channel_message_retention_dropdown'}
inputId={'channel_message_retention_input'}
/>
{this.isMessageRetentionSetByEnv() && <SetByEnv/>}
</div>
<div id='global_file_dropdown'>
<DropdownInputHybrid
@@ -199,8 +229,9 @@ export default class GlobalPolicyForm extends React.PureComponent<Props, State>
inputValue={this.state.fileRetentionInputValue}
width={90}
exceptionToInput={[FOREVER]}
disabled={this.isFileRetentionSetByEnv()}
defaultValue={keepForeverOption()}
options={[daysOption(), yearsOption(), keepForeverOption()]}
options={[hoursOption(), daysOption(), yearsOption(), keepForeverOption()]}
legend={Utils.localizeMessage('admin.data_retention.form.fileRetention', 'File retention')}
placeholder={Utils.localizeMessage('admin.data_retention.form.fileRetention', 'File retention')}
name={'file_retention'}
@@ -208,6 +239,7 @@ export default class GlobalPolicyForm extends React.PureComponent<Props, State>
dropdownClassNamePrefix={'file_retention_dropdown'}
inputId={'file_retention_input'}
/>
{this.isFileRetentionSetByEnv() && <SetByEnv/>}
</div>
</div>

Просмотреть файл

@@ -11,10 +11,14 @@ import type {ServerError} from '@mattermost/types/errors';
import {
updateConfig,
} from 'mattermost-redux/actions/admin';
import {getEnvironmentConfig} from 'mattermost-redux/selectors/entities/admin';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import type {GenericAction, ActionFunc} from 'mattermost-redux/types/actions';
import {setNavigationBlocked} from 'actions/admin_actions.jsx';
import type {GlobalState} from 'types/store';
import GlobalPolicyForm from './global_policy_form';
type Actions = {
@@ -22,6 +26,17 @@ type Actions = {
setNavigationBlocked: (blocked: boolean) => void;
};
function mapStateToProps(state: GlobalState) {
const messageRetentionHours = getConfig(state).DataRetentionMessageRetentionHours;
const fileRetentionHours = getConfig(state).DataRetentionFileRetentionHours;
return {
messageRetentionHours,
fileRetentionHours,
environmentConfig: getEnvironmentConfig(state),
};
}
function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
return {
actions: bindActionCreators<ActionCreatorsMapObject<ActionFunc | GenericAction>, Actions>({
@@ -31,4 +46,4 @@ function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
};
}
export default connect(null, mapDispatchToProps)(GlobalPolicyForm);
export default connect(mapStateToProps, mapDispatchToProps)(GlobalPolicyForm);

Просмотреть файл

@@ -11,6 +11,7 @@ import type {JobTypeBase, JobType} from '@mattermost/types/jobs';
import {getDataRetentionCustomPolicies as fetchDataRetentionCustomPolicies, deleteDataRetentionCustomPolicy, updateConfig} from 'mattermost-redux/actions/admin';
import {createJob, getJobsByType} from 'mattermost-redux/actions/jobs';
import {getDataRetentionCustomPolicies, getDataRetentionCustomPoliciesCount} from 'mattermost-redux/selectors/entities/admin';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import type {GenericAction, ActionFunc, ActionResult} from 'mattermost-redux/types/actions';
import type {GlobalState} from 'types/store';
@@ -28,10 +29,14 @@ type Actions = {
function mapStateToProps(state: GlobalState) {
const customPolicies = getDataRetentionCustomPolicies(state);
const customPoliciesCount = getDataRetentionCustomPoliciesCount(state);
const globalMessageRetentionHours = getConfig(state).DataRetentionMessageRetentionHours;
const globalFileRetentionHours = getConfig(state).DataRetentionFileRetentionHours;
return {
customPolicies,
customPoliciesCount,
globalMessageRetentionHours,
globalFileRetentionHours,
};
}

Просмотреть файл

@@ -44,6 +44,8 @@ const baseStyles = {
boxShadow: 'none',
padding: '0 2px',
cursor: 'pointer',
minHeight: '40px',
borderRadius: '0',
}),
indicatorSeparator: (provided: CSSProperties) => ({
...provided,
@@ -206,6 +208,7 @@ const DropdownInputHybrid = <T extends OptionType = OptionType>(props: Props<T>)
className={classNames('Input form-control')}
ref={inputRef}
id={inputId}
disabled={props.disabled}
/>
</div>
<div
@@ -232,6 +235,7 @@ const DropdownInputHybrid = <T extends OptionType = OptionType>(props: Props<T>)
hideSelectedOptions={true}
isSearchable={false}
menuPortalTarget={document.body}
isDisabled={props.disabled}
{...otherProps}
/>
</div>

Просмотреть файл

@@ -722,6 +722,7 @@
"admin.data_retention.form.channelAndDirectMessageRetention": "Channel & direct message retention",
"admin.data_retention.form.days": "Days",
"admin.data_retention.form.fileRetention": "File retention",
"admin.data_retention.form.hours": "Hours",
"admin.data_retention.form.keepForever": "Keep forever",
"admin.data_retention.form.text": "Applies to all teams and channels, but does not apply to custom retention policies.",
"admin.data_retention.form.years": "Years",
@@ -739,6 +740,7 @@
"admin.data_retention.jobTimeAM": "{time} AM (UTC)",
"admin.data_retention.jobTimePM": "{time} PM (UTC)",
"admin.data_retention.retention_days": "{count} {count, plural, one {day} other {days}}",
"admin.data_retention.retention_hours": "{count} {count, plural, one {hour} other {hours}}",
"admin.data_retention.retention_years": "{count} {count, plural, one {year} other {years}}",
"admin.data_retention.settings.title": "Data Retention Policies",
"admin.data_retention.title": "Data Retention Policy",

Просмотреть файл

@@ -33,8 +33,8 @@ export type ClientConfig = {
CWSMock: string;
DataRetentionEnableFileDeletion: string;
DataRetentionEnableMessageDeletion: string;
DataRetentionFileRetentionDays: string;
DataRetentionMessageRetentionDays: string;
DataRetentionFileRetentionHours: string;
DataRetentionMessageRetentionHours: string;
DefaultClientLocale: string;
DefaultTheme: string;
DiagnosticId: string;
@@ -828,7 +828,9 @@ export type DataRetentionSettings = {
EnableMessageDeletion: boolean;
EnableFileDeletion: boolean;
MessageRetentionDays: number;
MessageRetentionHours: number;
FileRetentionDays: number;
FileRetentionHours: number;
DeletionJobStartTime: string;
BatchSize: number;
EnableBoardsDeletion: boolean,