mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
[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:
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user