[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>
This commit is contained in:
Ben Cooke
2024-01-09 14:10:22 -05:00
committed by GitHub
parent 82b8d4dc07
commit bb88b92b4c
18 changed files with 376 additions and 47 deletions

View File

@@ -611,7 +611,9 @@ const defaultServerConfig: AdminConfig = {
EnableFileDeletion: false,
EnableBoardsDeletion: false,
MessageRetentionDays: 365,
MessageRetentionHours: 0,
FileRetentionDays: 365,
FileRetentionHours: 0,
BoardsRetentionDays: 365,
DeletionJobStartTime: '02:00',
BatchSize: 3000,

View File

@@ -122,9 +122,9 @@ func GenerateClientConfig(c *model.Config, telemetryID string, license *model.Li
props["AllowCustomThemes"] = "true"
props["AllowedThemes"] = ""
props["DataRetentionEnableMessageDeletion"] = "false"
props["DataRetentionMessageRetentionDays"] = "0"
props["DataRetentionMessageRetentionHours"] = "0"
props["DataRetentionEnableFileDeletion"] = "false"
props["DataRetentionFileRetentionDays"] = "0"
props["DataRetentionFileRetentionHours"] = "0"
props["CustomUrlSchemes"] = strings.Join(c.DisplaySettings.CustomURLSchemes, ",")
props["MaxMarkdownNodes"] = strconv.FormatInt(int64(*c.DisplaySettings.MaxMarkdownNodes), 10)
@@ -204,9 +204,9 @@ func GenerateClientConfig(c *model.Config, telemetryID string, license *model.Li
if *license.Features.DataRetention {
props["DataRetentionEnableMessageDeletion"] = strconv.FormatBool(*c.DataRetentionSettings.EnableMessageDeletion)
props["DataRetentionMessageRetentionDays"] = strconv.FormatInt(int64(*c.DataRetentionSettings.MessageRetentionDays), 10)
props["DataRetentionMessageRetentionHours"] = strconv.FormatInt(int64(c.DataRetentionSettings.GetMessageRetentionHours()), 10)
props["DataRetentionEnableFileDeletion"] = strconv.FormatBool(*c.DataRetentionSettings.EnableFileDeletion)
props["DataRetentionFileRetentionDays"] = strconv.FormatInt(int64(*c.DataRetentionSettings.FileRetentionDays), 10)
props["DataRetentionFileRetentionHours"] = strconv.FormatInt(int64(c.DataRetentionSettings.GetFileRetentionHours()), 10)
}
if license.HasSharedChannels() {

View File

@@ -8742,13 +8742,37 @@
"id": "model.config.is_valid.data_retention.deletion_job_start_time.app_error",
"translation": "Data retention job start time must be a 24-hour time stamp in the form HH:MM."
},
{
"id": "model.config.is_valid.data_retention.file_retention_both_zero.app_error",
"translation": "File retention days and file retention hours cannot both be 0."
},
{
"id": "model.config.is_valid.data_retention.file_retention_days_too_low.app_error",
"translation": "File retention must be one day or longer."
"translation": "File retention days cannot be less than 0."
},
{
"id": "model.config.is_valid.data_retention.file_retention_hours_too_low.app_error",
"translation": "File retention hours cannot be less than 0"
},
{
"id": "model.config.is_valid.data_retention.file_retention_misconfiguration.app_error",
"translation": "File retention days and file retention hours cannot both be greater than 0."
},
{
"id": "model.config.is_valid.data_retention.message_retention_both_zero.app_error",
"translation": "Message retention days and message retention hours cannot both be 0."
},
{
"id": "model.config.is_valid.data_retention.message_retention_days_too_low.app_error",
"translation": "Message retention must be one day or longer."
"translation": "Message retention days cannot be less than 0."
},
{
"id": "model.config.is_valid.data_retention.message_retention_hours_too_low.app_error",
"translation": "Message retention hours cannot be less than 0"
},
{
"id": "model.config.is_valid.data_retention.message_retention_misconfiguration.app_error",
"translation": "Message retention days and message retention hours cannot both be greater than 0."
},
{
"id": "model.config.is_valid.directory.app_error",

View File

@@ -829,7 +829,9 @@ func (ts *TelemetryService) trackConfig() {
"enable_message_deletion": *cfg.DataRetentionSettings.EnableMessageDeletion,
"enable_file_deletion": *cfg.DataRetentionSettings.EnableFileDeletion,
"message_retention_days": *cfg.DataRetentionSettings.MessageRetentionDays,
"message_retention_hours": *cfg.DataRetentionSettings.MessageRetentionHours,
"file_retention_days": *cfg.DataRetentionSettings.FileRetentionDays,
"file_retention_hours": *cfg.DataRetentionSettings.FileRetentionHours,
"deletion_job_start_time": *cfg.DataRetentionSettings.DeletionJobStartTime,
"batch_size": *cfg.DataRetentionSettings.BatchSize,
"time_between_batches": *cfg.DataRetentionSettings.TimeBetweenBatchesMilliseconds,

View File

@@ -207,7 +207,9 @@ const (
BleveSettingsDefaultBatchSize = 10000
DataRetentionSettingsDefaultMessageRetentionDays = 365
DataRetentionSettingsDefaultMessageRetentionHours = 0
DataRetentionSettingsDefaultFileRetentionDays = 365
DataRetentionSettingsDefaultFileRetentionHours = 0
DataRetentionSettingsDefaultBoardsRetentionDays = 365
DataRetentionSettingsDefaultDeletionJobStartTime = "02:00"
DataRetentionSettingsDefaultBatchSize = 3000
@@ -2911,8 +2913,10 @@ type DataRetentionSettings struct {
EnableMessageDeletion *bool `access:"compliance_data_retention_policy"`
EnableFileDeletion *bool `access:"compliance_data_retention_policy"`
EnableBoardsDeletion *bool `access:"compliance_data_retention_policy"`
MessageRetentionDays *int `access:"compliance_data_retention_policy"`
FileRetentionDays *int `access:"compliance_data_retention_policy"`
MessageRetentionDays *int `access:"compliance_data_retention_policy"` // Deprecated: use `MessageRetentionHours`
MessageRetentionHours *int `access:"compliance_data_retention_policy"`
FileRetentionDays *int `access:"compliance_data_retention_policy"` // Deprecated: use `FileRetentionHours`
FileRetentionHours *int `access:"compliance_data_retention_policy"`
BoardsRetentionDays *int `access:"compliance_data_retention_policy"`
DeletionJobStartTime *string `access:"compliance_data_retention_policy"`
BatchSize *int `access:"compliance_data_retention_policy"`
@@ -2937,10 +2941,18 @@ func (s *DataRetentionSettings) SetDefaults() {
s.MessageRetentionDays = NewInt(DataRetentionSettingsDefaultMessageRetentionDays)
}
if s.MessageRetentionHours == nil {
s.MessageRetentionHours = NewInt(DataRetentionSettingsDefaultMessageRetentionHours)
}
if s.FileRetentionDays == nil {
s.FileRetentionDays = NewInt(DataRetentionSettingsDefaultFileRetentionDays)
}
if s.FileRetentionHours == nil {
s.FileRetentionHours = NewInt(DataRetentionSettingsDefaultFileRetentionHours)
}
if s.BoardsRetentionDays == nil {
s.BoardsRetentionDays = NewInt(DataRetentionSettingsDefaultBoardsRetentionDays)
}
@@ -2961,6 +2973,30 @@ func (s *DataRetentionSettings) SetDefaults() {
}
}
// GetMessageRetentionHours returns the message retention time as an int.
// MessageRetentionHours takes precedence over the deprecated MessageRetentionDays.
func (s *DataRetentionSettings) GetMessageRetentionHours() int {
if s.MessageRetentionHours != nil && *s.MessageRetentionHours > 0 {
return *s.MessageRetentionHours
}
if s.MessageRetentionDays != nil && *s.MessageRetentionDays > 0 {
return *s.MessageRetentionDays * 24
}
return DataRetentionSettingsDefaultMessageRetentionDays * 24
}
// GetFileRetentionHours returns the message retention time as an int.
// FileRetentionHours takes precedence over the deprecated FileRetentionDays.
func (s *DataRetentionSettings) GetFileRetentionHours() int {
if s.FileRetentionHours != nil && *s.FileRetentionHours > 0 {
return *s.FileRetentionHours
}
if s.FileRetentionDays != nil && *s.FileRetentionDays > 0 {
return *s.FileRetentionDays * 24
}
return DataRetentionSettingsDefaultFileRetentionDays * 24
}
type JobSettings struct {
RunJobs *bool `access:"write_restrictable,cloud_restrictable"` // telemetry: none
RunScheduler *bool `access:"write_restrictable,cloud_restrictable"` // telemetry: none
@@ -4141,14 +4177,38 @@ func (bs *BleveSettings) isValid() *AppError {
}
func (s *DataRetentionSettings) isValid() *AppError {
if *s.MessageRetentionDays <= 0 {
if s.MessageRetentionDays == nil || *s.MessageRetentionDays < 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.data_retention.message_retention_days_too_low.app_error", nil, "", http.StatusBadRequest)
}
if *s.FileRetentionDays <= 0 {
if s.MessageRetentionHours == nil || *s.MessageRetentionHours < 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.data_retention.message_retention_hours_too_low.app_error", nil, "", http.StatusBadRequest)
}
if s.FileRetentionDays == nil || *s.FileRetentionDays < 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.data_retention.file_retention_days_too_low.app_error", nil, "", http.StatusBadRequest)
}
if s.FileRetentionHours == nil || *s.FileRetentionHours < 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.data_retention.file_retention_hours_too_low.app_error", nil, "", http.StatusBadRequest)
}
if *s.MessageRetentionDays > 0 && *s.MessageRetentionHours > 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.data_retention.message_retention_misconfiguration.app_error", nil, "", http.StatusBadRequest)
}
if *s.FileRetentionDays > 0 && *s.FileRetentionHours > 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.data_retention.file_retention_misconfiguration.app_error", nil, "", http.StatusBadRequest)
}
if *s.MessageRetentionDays == 0 && *s.MessageRetentionHours == 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.data_retention.message_retention_both_zero.app_error", nil, "", http.StatusBadRequest)
}
if *s.FileRetentionDays == 0 && *s.FileRetentionHours == 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.data_retention.file_retention_both_zero.app_error", nil, "", http.StatusBadRequest)
}
if _, err := time.Parse("15:04", *s.DeletionJobStartTime); err != nil {
return NewAppError("Config.IsValid", "model.config.is_valid.data_retention.deletion_job_start_time.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}

View File

@@ -1647,3 +1647,105 @@ func TestConfigDefaultCallsPluginState(t *testing.T) {
assert.False(t, c1.PluginSettings.PluginStates["com.mattermost.calls"].Enable)
})
}
func TestConfigGetMessageRetentionHours(t *testing.T) {
tests := []struct {
name string
config Config
value int
}{
{
name: "should return MessageRetentionDays config value in hours by default",
config: Config{},
value: 8760,
},
{
name: "should return MessageRetentionHours config value",
config: Config{
DataRetentionSettings: DataRetentionSettings{
MessageRetentionHours: NewInt(48),
},
},
value: 48,
},
{
name: "should return MessageRetentionHours config value",
config: Config{
DataRetentionSettings: DataRetentionSettings{
MessageRetentionDays: NewInt(50),
MessageRetentionHours: NewInt(48),
},
},
value: 48,
},
{
name: "should return MessageRetentionDays config value in hours",
config: Config{
DataRetentionSettings: DataRetentionSettings{
MessageRetentionDays: NewInt(50),
MessageRetentionHours: NewInt(0),
},
},
value: 1200,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
test.config.SetDefaults()
require.Equal(t, test.value, test.config.DataRetentionSettings.GetMessageRetentionHours())
})
}
}
func TestConfigGetFileRetentionHours(t *testing.T) {
tests := []struct {
name string
config Config
value int
}{
{
name: "should return FileRetentionDays config value in hours by default",
config: Config{},
value: 8760,
},
{
name: "should return FileRetentionHours config value",
config: Config{
DataRetentionSettings: DataRetentionSettings{
FileRetentionHours: NewInt(48),
},
},
value: 48,
},
{
name: "should return FileRetentionHours config value",
config: Config{
DataRetentionSettings: DataRetentionSettings{
FileRetentionDays: NewInt(50),
FileRetentionHours: NewInt(48),
},
},
value: 48,
},
{
name: "should return FileRetentionDays config value in hours",
config: Config{
DataRetentionSettings: DataRetentionSettings{
FileRetentionDays: NewInt(50),
FileRetentionHours: NewInt(0),
},
},
value: 1200,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
test.config.SetDefaults()
require.Equal(t, test.value, test.config.DataRetentionSettings.GetFileRetentionHours())
})
}
}

View File

@@ -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(),

View File

@@ -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: (

View File

@@ -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>});

View File

@@ -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"

View File

@@ -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 {

View File

@@ -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(),

View File

@@ -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>

View File

@@ -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);

View File

@@ -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,
};
}

View File

@@ -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>

View File

@@ -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",

View File

@@ -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,