mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
[MM-57418] Implement support for defining plugin settings sections (#27654)
* Implement support for defining plugin settings sections * Implement custom plugin configuration sections * Tests * Update test * Improvements
This commit is contained in:
@@ -91,6 +91,29 @@ type PluginSetting struct {
|
||||
Hosting string `json:"hosting"`
|
||||
}
|
||||
|
||||
type PluginSettingsSection struct {
|
||||
// A unique identifier for this section.
|
||||
Key string `json:"key" yaml:"key"`
|
||||
|
||||
// Optional text to display as section title.
|
||||
Title string `json:"title" yaml:"title"`
|
||||
|
||||
// Optional text to display as section subtitle.
|
||||
Subtitle string `json:"subtitle" yaml:"subtitle"`
|
||||
|
||||
// A list of setting definitions to display inside the section.
|
||||
Settings []*PluginSetting `json:"settings" yaml:"settings"`
|
||||
|
||||
// Optional text to display above the settings. Supports Markdown formatting.
|
||||
Header string `json:"header" yaml:"header"`
|
||||
|
||||
// Optional text to display below the settings. Supports Markdown formatting.
|
||||
Footer string `json:"footer" yaml:"footer"`
|
||||
|
||||
// If true, the section will load the custom component registered using `registry.registerAdminConsoleCustomSection`
|
||||
Custom bool `json:"custom" yaml:"custom"`
|
||||
}
|
||||
|
||||
type PluginSettingsSchema struct {
|
||||
// Optional text to display above the settings. Supports Markdown formatting.
|
||||
Header string `json:"header" yaml:"header"`
|
||||
@@ -100,6 +123,9 @@ type PluginSettingsSchema struct {
|
||||
|
||||
// A list of setting definitions.
|
||||
Settings []*PluginSetting `json:"settings" yaml:"settings"`
|
||||
|
||||
// A list of settings section definitions.
|
||||
Sections []*PluginSettingsSection `json:"sections" yaml:"sections"`
|
||||
}
|
||||
|
||||
// The plugin manifest defines the metadata required to load and present your plugin. The manifest
|
||||
@@ -330,6 +356,27 @@ func (s *PluginSettingsSchema) isValid() error {
|
||||
}
|
||||
}
|
||||
|
||||
for _, section := range s.Sections {
|
||||
if err := section.IsValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PluginSettingsSection) IsValid() error {
|
||||
if s.Key == "" {
|
||||
return errors.New("invalid empty Key")
|
||||
}
|
||||
|
||||
for _, setting := range s.Settings {
|
||||
err := setting.isValid()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -65,6 +65,37 @@ func TestIsValid(t *testing.T) {
|
||||
Default: "thedefault",
|
||||
},
|
||||
},
|
||||
Sections: []*PluginSettingsSection{
|
||||
{
|
||||
Key: "section1",
|
||||
Title: "section title",
|
||||
Subtitle: "section subtitle",
|
||||
Settings: []*PluginSetting{
|
||||
{
|
||||
Key: "section1setting1",
|
||||
DisplayName: "thedisplayname",
|
||||
Type: "custom",
|
||||
},
|
||||
{
|
||||
Key: "section1setting2",
|
||||
DisplayName: "thedisplayname",
|
||||
Type: "custom",
|
||||
},
|
||||
},
|
||||
Header: "section header",
|
||||
Footer: "section footer",
|
||||
},
|
||||
{
|
||||
Key: "section2",
|
||||
Settings: []*PluginSetting{
|
||||
{
|
||||
Key: "section2setting1",
|
||||
DisplayName: "thedisplayname",
|
||||
Type: "custom",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, false},
|
||||
}
|
||||
@@ -103,6 +134,62 @@ func TestIsValidSettingsSchema(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginSettingsSectionIsValid(t *testing.T) {
|
||||
for name, test := range map[string]struct {
|
||||
Section PluginSettingsSection
|
||||
ExpectedError string
|
||||
}{
|
||||
"missing key": {
|
||||
Section: PluginSettingsSection{
|
||||
Settings: []*PluginSetting{
|
||||
{
|
||||
Type: "custom",
|
||||
Placeholder: "some Text",
|
||||
},
|
||||
},
|
||||
},
|
||||
ExpectedError: "invalid empty Key",
|
||||
},
|
||||
"invalid setting": {
|
||||
Section: PluginSettingsSection{
|
||||
Key: "sectionKey",
|
||||
Settings: []*PluginSetting{
|
||||
{
|
||||
Type: "invalid",
|
||||
},
|
||||
},
|
||||
},
|
||||
ExpectedError: "invalid setting type: invalid",
|
||||
},
|
||||
"valid empty": {
|
||||
Section: PluginSettingsSection{
|
||||
Key: "sectionKey",
|
||||
Settings: []*PluginSetting{},
|
||||
},
|
||||
},
|
||||
"valid": {
|
||||
Section: PluginSettingsSection{
|
||||
Key: "sectionKey",
|
||||
Settings: []*PluginSetting{
|
||||
{
|
||||
Type: "custom",
|
||||
Placeholder: "some Text",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
err := test.Section.IsValid()
|
||||
if test.ExpectedError != "" {
|
||||
assert.EqualError(t, err, test.ExpectedError)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSettingIsValid(t *testing.T) {
|
||||
for name, test := range map[string]struct {
|
||||
Setting PluginSetting
|
||||
|
||||
@@ -464,6 +464,19 @@ export function registerAdminConsoleCustomSetting(pluginId, key, component, {sho
|
||||
};
|
||||
}
|
||||
|
||||
export function registerAdminConsoleCustomSection(pluginId, key, component) {
|
||||
return (storeDispatch) => {
|
||||
storeDispatch({
|
||||
type: ActionTypes.RECEIVED_ADMIN_CONSOLE_CUSTOM_SECTION,
|
||||
data: {
|
||||
pluginId,
|
||||
key,
|
||||
component,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export async function getSamlMetadataFromIdp(success, error, samlMetadataURL) {
|
||||
const {data, error: err} = await dispatch(AdminActions.getSamlMetadataFromIdp(samlMetadataURL));
|
||||
if (data && success) {
|
||||
|
||||
@@ -54,4 +54,17 @@ describe('Actions.Admin', () => {
|
||||
},
|
||||
}}});
|
||||
});
|
||||
|
||||
test('Register a custom plugin section adds the component to the state', async () => {
|
||||
expect(store.getState().plugins.adminConsoleCustomSections).toEqual({});
|
||||
|
||||
store.dispatch(Actions.registerAdminConsoleCustomSection('plugin-id', 'sectionA', React.Component));
|
||||
expect(store.getState().plugins.adminConsoleCustomSections).toEqual(
|
||||
{'plugin-id': {
|
||||
sectiona: {
|
||||
key: 'sectionA',
|
||||
pluginId: 'plugin-id',
|
||||
component: React.Component,
|
||||
}}});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3220,6 +3220,7 @@ const AdminDefinition: AdminDefinitionType = {
|
||||
name: defineMessage({id: 'admin.authentication.ldap', defaultMessage: 'AD/LDAP'}),
|
||||
sections: [
|
||||
{
|
||||
key: 'admin.authentication.ldap.connection',
|
||||
title: 'Connection',
|
||||
subtitle: 'Connection and security level to your AD/LDAP server.',
|
||||
settings: [
|
||||
@@ -3385,6 +3386,7 @@ const AdminDefinition: AdminDefinitionType = {
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'admin.authentication.ldap.dn_and_filters',
|
||||
title: 'Base DN & Filters',
|
||||
settings: [
|
||||
{
|
||||
@@ -3476,6 +3478,7 @@ const AdminDefinition: AdminDefinitionType = {
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'admin.authentication.ldap.account_synchronization',
|
||||
title: 'Account Synchronization',
|
||||
settings: [
|
||||
{
|
||||
@@ -3631,6 +3634,7 @@ const AdminDefinition: AdminDefinitionType = {
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'admin.authentication.ldap.group_synchronization',
|
||||
title: 'Group Synchronization',
|
||||
settings: [
|
||||
{
|
||||
@@ -3661,6 +3665,7 @@ const AdminDefinition: AdminDefinitionType = {
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'admin.authentication.ldap.synchronization_performance',
|
||||
title: 'Synchronization Performance',
|
||||
settings: [
|
||||
{
|
||||
@@ -3734,6 +3739,7 @@ const AdminDefinition: AdminDefinitionType = {
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'admin.authentication.ldap.synchronization_history',
|
||||
title: 'Synchronization History',
|
||||
subtitle: 'See the table below for the status of each synchronization',
|
||||
settings: [
|
||||
|
||||
@@ -11,6 +11,7 @@ import CustomPluginSettings from 'components/admin_console/custom_plugin_setting
|
||||
import {escapePathPart} from 'components/admin_console/schema_admin_settings';
|
||||
|
||||
import {shallowWithIntl} from 'tests/helpers/intl-test-helper';
|
||||
import {screen, renderWithContext} from 'tests/react_testing_utils';
|
||||
|
||||
import type {AdminDefinitionSetting} from '../types';
|
||||
|
||||
@@ -136,7 +137,7 @@ describe('components/admin_console/CustomPluginSettings', () => {
|
||||
<CustomPluginSettings
|
||||
{...baseProps}
|
||||
config={config}
|
||||
schema={{...plugin.settings_schema, id: plugin.id, name: plugin.name, settings}}
|
||||
schema={{...plugin.settings_schema, id: plugin.id, name: plugin.name, settings, sections: undefined}}
|
||||
patchConfig={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
@@ -170,10 +171,192 @@ describe('components/admin_console/CustomPluginSettings', () => {
|
||||
Plugins: {},
|
||||
} as PluginSettings,
|
||||
}}
|
||||
schema={{...plugin.settings_schema, id: plugin.id, name: plugin.name, settings}}
|
||||
schema={{...plugin.settings_schema, id: plugin.id, name: plugin.name, settings, sections: undefined}}
|
||||
patchConfig={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
function CustomSection(props: {settingsList: React.ReactNode[]}) {
|
||||
return (<div>{'Custom Section'} {props.settingsList}</div>);
|
||||
}
|
||||
|
||||
function CustomSetting() {
|
||||
return (<div>{'Custom Setting'}</div>);
|
||||
}
|
||||
|
||||
describe('custom plugin sections', () => {
|
||||
let config: {PluginSettings: PluginSettings};
|
||||
|
||||
const baseProps = {
|
||||
isDisabled: false,
|
||||
environmentConfig: {},
|
||||
setNavigationBlocked: jest.fn(),
|
||||
roles: {},
|
||||
cloud: {} as CloudState,
|
||||
license: {},
|
||||
editRole: jest.fn(),
|
||||
consoleAccess: {read: {}, write: {}},
|
||||
isCurrentUserSystemAdmin: false,
|
||||
enterpriseReady: false,
|
||||
};
|
||||
|
||||
it('empty sections', () => {
|
||||
const schema = {
|
||||
id: 'testplugin',
|
||||
name: 'testplugin',
|
||||
description: '',
|
||||
version: '',
|
||||
active: true,
|
||||
webapp: {
|
||||
bundle_path: '/static/testplugin_bundle.js',
|
||||
},
|
||||
sections: [],
|
||||
};
|
||||
|
||||
renderWithContext(
|
||||
<CustomPluginSettings
|
||||
{...baseProps}
|
||||
config={config}
|
||||
schema={schema}
|
||||
patchConfig={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText('testplugin')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('render sections', () => {
|
||||
const schema = {
|
||||
id: 'testplugin',
|
||||
name: 'testplugin',
|
||||
description: '',
|
||||
version: '',
|
||||
active: true,
|
||||
webapp: {
|
||||
bundle_path: '/static/testplugin_bundle.js',
|
||||
},
|
||||
sections: [
|
||||
{
|
||||
key: 'section1',
|
||||
title: 'Section 1',
|
||||
settings: [],
|
||||
header: 'Section1 Header',
|
||||
footer: 'Section1 Footer',
|
||||
},
|
||||
{
|
||||
key: 'section2',
|
||||
title: 'Section 2',
|
||||
settings: [
|
||||
{
|
||||
key: 'section2setting1',
|
||||
label: 'Section 2 Setting 1',
|
||||
type: 'text' as const,
|
||||
help_text: 'Section 2 Setting 1 Help Text',
|
||||
},
|
||||
],
|
||||
header: 'Section2 Header',
|
||||
footer: 'Section2 Footer',
|
||||
},
|
||||
{
|
||||
key: 'section3',
|
||||
settings: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
renderWithContext(
|
||||
<CustomPluginSettings
|
||||
{...baseProps}
|
||||
config={config}
|
||||
schema={schema}
|
||||
patchConfig={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('testplugin')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('Section 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Section1 Header')).toBeInTheDocument();
|
||||
expect(screen.getByText('Section1 Footer')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('Section 2')).toBeInTheDocument();
|
||||
expect(screen.getByText('Section2 Header')).toBeInTheDocument();
|
||||
expect(screen.getByText('Section2 Footer')).toBeInTheDocument();
|
||||
expect(screen.getByText('Section 2 Setting 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Section 2 Setting 1 Help Text')).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByText('Section 3')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('custom sections and settings', () => {
|
||||
const schema = {
|
||||
id: 'testplugin',
|
||||
name: 'testplugin',
|
||||
description: '',
|
||||
version: '',
|
||||
active: true,
|
||||
webapp: {
|
||||
bundle_path: '/static/testplugin_bundle.js',
|
||||
},
|
||||
sections: [
|
||||
{
|
||||
key: 'section1',
|
||||
title: 'Custom Section 1',
|
||||
settings: [
|
||||
{
|
||||
key: 'customsectionnumbersetting',
|
||||
label: 'Custom Section Number Setting',
|
||||
type: 'number' as const,
|
||||
help_text: 'Custom Section Number Setting Help Text',
|
||||
},
|
||||
{
|
||||
key: 'customsectioncustomsetting',
|
||||
type: 'custom' as const,
|
||||
component: CustomSetting,
|
||||
},
|
||||
],
|
||||
custom: true,
|
||||
component: CustomSection,
|
||||
},
|
||||
{
|
||||
key: 'section2',
|
||||
title: 'Section 2',
|
||||
settings: [
|
||||
{
|
||||
key: 'section2setting1',
|
||||
label: 'Section 2 Setting 1',
|
||||
type: 'text' as const,
|
||||
help_text: 'Section 2 Setting 1 Help Text',
|
||||
},
|
||||
],
|
||||
header: 'Section2 Header',
|
||||
footer: 'Section2 Footer',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
renderWithContext(
|
||||
<CustomPluginSettings
|
||||
{...baseProps}
|
||||
config={config}
|
||||
schema={schema}
|
||||
patchConfig={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('testplugin')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('Custom Section')).toBeInTheDocument();
|
||||
expect(screen.getByText('Custom Section Number Setting')).toBeInTheDocument();
|
||||
expect(screen.getByText('Custom Section Number Setting Help Text')).toBeInTheDocument();
|
||||
expect(screen.getByText('Custom Setting')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('Section 2')).toBeInTheDocument();
|
||||
expect(screen.getByText('Section2 Header')).toBeInTheDocument();
|
||||
expect(screen.getByText('Section2 Footer')).toBeInTheDocument();
|
||||
expect(screen.getByText('Section 2 Setting 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Section 2 Setting 1 Help Text')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {MessageDescriptor} from 'react-intl';
|
||||
import {defineMessage} from 'react-intl';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import type {PluginRedux} from '@mattermost/types/plugins';
|
||||
import type {PluginRedux, PluginSetting, PluginSettingSection} from '@mattermost/types/plugins';
|
||||
import type {GlobalState} from '@mattermost/types/store';
|
||||
|
||||
import {createSelector} from 'mattermost-redux/selectors/create_selector';
|
||||
@@ -13,19 +13,19 @@ import {appsFeatureFlagEnabled} from 'mattermost-redux/selectors/entities/apps';
|
||||
import {isCurrentLicenseCloud} from 'mattermost-redux/selectors/entities/cloud';
|
||||
import {getRoles} from 'mattermost-redux/selectors/entities/roles';
|
||||
|
||||
import {getAdminConsoleCustomComponents} from 'selectors/admin_console';
|
||||
import {getAdminConsoleCustomComponents, getAdminConsoleCustomSections} from 'selectors/admin_console';
|
||||
|
||||
import {appsPluginID} from 'utils/apps';
|
||||
import {Constants} from 'utils/constants';
|
||||
|
||||
import type {AdminConsolePluginComponent} from 'types/store/plugins';
|
||||
import type {AdminConsolePluginComponent, AdminConsolePluginCustomSection} from 'types/store/plugins';
|
||||
|
||||
import CustomPluginSettings from './custom_plugin_settings';
|
||||
import getEnablePluginSetting from './enable_plugin_setting';
|
||||
|
||||
import {it} from '../admin_definition';
|
||||
import {escapePathPart} from '../schema_admin_settings';
|
||||
import type {AdminDefinitionSetting, AdminDefinitionSubSectionSchema} from '../types';
|
||||
import type {AdminDefinitionSetting, AdminDefinitionSubSectionSchema, AdminDefinitionConfigSchemaSection} from '../types';
|
||||
|
||||
type OwnProps = { match: { params: { plugin_id: string } } }
|
||||
|
||||
@@ -34,9 +34,10 @@ function makeGetPluginSchema() {
|
||||
'makeGetPluginSchema',
|
||||
(state: GlobalState, pluginId: string) => state.entities.admin.plugins?.[pluginId],
|
||||
(state: GlobalState, pluginId: string) => getAdminConsoleCustomComponents(state, pluginId),
|
||||
(state: GlobalState, pluginId: string) => getAdminConsoleCustomSections(state, pluginId),
|
||||
(state) => appsFeatureFlagEnabled(state),
|
||||
isCurrentLicenseCloud,
|
||||
(plugin: PluginRedux | undefined, customComponents: Record<string, AdminConsolePluginComponent>, appsFeatureFlagIsEnabled, isCloudLicense) => {
|
||||
(plugin: PluginRedux | undefined, customComponents: Record<string, AdminConsolePluginComponent>, customSections: Record<string, AdminConsolePluginCustomSection>, appsFeatureFlagIsEnabled, isCloudLicense) => {
|
||||
if (!plugin) {
|
||||
return null;
|
||||
}
|
||||
@@ -44,9 +45,8 @@ function makeGetPluginSchema() {
|
||||
const escapedPluginId = escapePathPart(plugin.id);
|
||||
const pluginEnabledConfigKey = 'PluginSettings.PluginStates.' + escapedPluginId + '.Enable';
|
||||
|
||||
let settings: Array<Partial<AdminDefinitionSetting>> = [];
|
||||
if (plugin.settings_schema && plugin.settings_schema.settings) {
|
||||
settings = plugin.settings_schema.settings.map((setting) => {
|
||||
const parsePluginSettings = (settings: PluginSetting[]) => {
|
||||
return settings.map((setting) => {
|
||||
const key = setting.key.toLowerCase();
|
||||
let component = null;
|
||||
let bannerType = '';
|
||||
@@ -82,8 +82,53 @@ function makeGetPluginSchema() {
|
||||
banner_type: bannerType,
|
||||
component,
|
||||
showTitle: customComponents[key] ? customComponents[key].options.showTitle : false,
|
||||
};
|
||||
}) as Array<Partial<AdminDefinitionSetting>>;
|
||||
} as Partial<AdminDefinitionSetting>;
|
||||
});
|
||||
};
|
||||
|
||||
const parsePluginSettingSections = (sections: PluginSettingSection[]) => {
|
||||
return sections.map((section) => {
|
||||
const key = section.key.toLowerCase();
|
||||
let component;
|
||||
let settings: Array<Partial<AdminDefinitionSetting>> = [];
|
||||
if (section.custom) {
|
||||
if (customSections[key]) {
|
||||
component = customSections[key]?.component;
|
||||
} else {
|
||||
// Show warning banner for custom sections when the plugin is disabled.
|
||||
settings = [{
|
||||
type: Constants.SettingsTypes.TYPE_BANNER,
|
||||
label: defineMessage({
|
||||
id: 'admin.plugin.customSection.pluginDisabledWarning',
|
||||
defaultMessage: 'In order to view this section, enable the plugin and click Save.',
|
||||
}),
|
||||
banner_type: 'warning',
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.length === 0) {
|
||||
settings = parsePluginSettings(section.settings);
|
||||
}
|
||||
|
||||
return {
|
||||
key,
|
||||
title: section.title,
|
||||
subtitle: section.subtitle,
|
||||
settings,
|
||||
header: section.header,
|
||||
footer: section.footer,
|
||||
component,
|
||||
} as AdminDefinitionConfigSchemaSection;
|
||||
});
|
||||
};
|
||||
|
||||
let sections: AdminDefinitionConfigSchemaSection[] = [];
|
||||
let settings: Array<Partial<AdminDefinitionSetting>> = [];
|
||||
if (plugin.settings_schema && plugin.settings_schema.sections) {
|
||||
sections = parsePluginSettingSections(plugin.settings_schema.sections);
|
||||
} else if (plugin.settings_schema && plugin.settings_schema.settings) {
|
||||
settings = parsePluginSettings(plugin.settings_schema.settings);
|
||||
}
|
||||
|
||||
if (plugin.id !== appsPluginID || appsFeatureFlagIsEnabled) {
|
||||
@@ -93,22 +138,34 @@ function makeGetPluginSchema() {
|
||||
} else {
|
||||
pluginEnableSetting.isDisabled = it.not(it.userHasWritePermissionOnResource('plugins'));
|
||||
}
|
||||
settings.unshift(pluginEnableSetting);
|
||||
|
||||
if (sections.length > 0) {
|
||||
sections[0].settings.unshift(pluginEnableSetting as AdminDefinitionSetting);
|
||||
} else {
|
||||
settings.unshift(pluginEnableSetting);
|
||||
}
|
||||
}
|
||||
|
||||
settings.forEach((s) => {
|
||||
const checkDisableSetting = (s: Partial<AdminDefinitionSetting>) => {
|
||||
if (s.isDisabled) {
|
||||
s.isDisabled = it.any(s.isDisabled, it.not(it.userHasWritePermissionOnResource('plugins')));
|
||||
} else {
|
||||
s.isDisabled = it.not(it.userHasWritePermissionOnResource('plugins'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (sections.length > 0) {
|
||||
sections.forEach((section) => section.settings.forEach(checkDisableSetting));
|
||||
} else {
|
||||
settings.forEach(checkDisableSetting);
|
||||
}
|
||||
|
||||
return {
|
||||
...plugin.settings_schema,
|
||||
id: plugin.id,
|
||||
name: plugin.name,
|
||||
settings,
|
||||
settings: sections.length > 0 ? undefined : settings,
|
||||
sections: sections.length > 0 ? sections : undefined,
|
||||
translate: Boolean(plugin.translate),
|
||||
} as AdminDefinitionSubSectionSchema;
|
||||
},
|
||||
|
||||
@@ -1073,6 +1073,17 @@ export class SchemaAdminSettings extends React.PureComponent<Props, State> {
|
||||
});
|
||||
}
|
||||
|
||||
if (section.component) {
|
||||
const CustomComponent = section.component;
|
||||
sections.push((
|
||||
<CustomComponent
|
||||
settingsList={settingsList}
|
||||
key={section.key}
|
||||
/>
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
let header;
|
||||
if (section.header) {
|
||||
header = (
|
||||
@@ -1098,7 +1109,10 @@ export class SchemaAdminSettings extends React.PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
sections.push(
|
||||
<div className={'config-section'}>
|
||||
<div
|
||||
className={'config-section'}
|
||||
key={section.key}
|
||||
>
|
||||
<SettingsGroup
|
||||
show={true}
|
||||
title={section.title}
|
||||
|
||||
@@ -167,12 +167,14 @@ type AdminDefinitionConfigSchemaSettings = {
|
||||
header?: string | MessageDescriptor;
|
||||
}
|
||||
|
||||
type AdminDefinitionConfigSchemaSection = {
|
||||
title: string;
|
||||
export type AdminDefinitionConfigSchemaSection = {
|
||||
key: string;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
settings: AdminDefinitionSetting[];
|
||||
header?: string | MessageDescriptor;
|
||||
footer?: string | MessageDescriptor;
|
||||
component?: Component;
|
||||
}
|
||||
|
||||
type RestrictedIndicatorType = {
|
||||
|
||||
@@ -1922,6 +1922,7 @@
|
||||
"admin.plugin.backToPlugins": "Go back to the Plugins",
|
||||
"admin.plugin.choose": "Choose File",
|
||||
"admin.plugin.cluster_instance": "Cluster Instance",
|
||||
"admin.plugin.customSection.pluginDisabledWarning": "In order to view this section, enable the plugin and click Save.",
|
||||
"admin.plugin.customSetting.pluginDisabledWarning": "In order to view this setting, enable the plugin and click Save.",
|
||||
"admin.plugin.disable": "Disable",
|
||||
"admin.plugin.disabling": "Disabling...",
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
registerAdminConsolePlugin,
|
||||
unregisterAdminConsolePlugin,
|
||||
registerAdminConsoleCustomSetting,
|
||||
registerAdminConsoleCustomSection,
|
||||
} from 'actions/admin_actions';
|
||||
import {showRHSPlugin, hideRHSPlugin, toggleRHSPlugin} from 'actions/views/rhs';
|
||||
import {
|
||||
@@ -864,6 +865,12 @@ export default class PluginRegistry {
|
||||
store.dispatch(registerAdminConsolePlugin(this.id, func));
|
||||
});
|
||||
|
||||
// Unregister a previously registered admin console definition override function.
|
||||
// Returns undefined.
|
||||
unregisterAdminConsolePlugin() {
|
||||
store.dispatch(unregisterAdminConsolePlugin(this.id));
|
||||
}
|
||||
|
||||
// Register a custom React component to manage the plugin configuration for the given setting key.
|
||||
// Accepts the following:
|
||||
// - key - A key specified in the settings_schema.settings block of the plugin's manifest.
|
||||
@@ -888,11 +895,22 @@ export default class PluginRegistry {
|
||||
store.dispatch(registerAdminConsoleCustomSetting(this.id, key, component, {showTitle}));
|
||||
});
|
||||
|
||||
// Unregister a previously registered admin console definition override function.
|
||||
// Returns undefined.
|
||||
unregisterAdminConsolePlugin() {
|
||||
store.dispatch(unregisterAdminConsolePlugin(this.id));
|
||||
}
|
||||
// Register a custom React component to render as a section in the plugin configuration page.
|
||||
// Accepts the following:
|
||||
// - key - A key specified in the settings_schema.sections block of the plugin's manifest.
|
||||
// - component - A react component to render in place of the default handling.
|
||||
registerAdminConsoleCustomSection = reArg([
|
||||
'key',
|
||||
'component',
|
||||
], ({
|
||||
key,
|
||||
component,
|
||||
}: {
|
||||
key: string;
|
||||
component: PluginComponent['component'];
|
||||
}) => {
|
||||
store.dispatch(registerAdminConsoleCustomSection(this.id, key, component));
|
||||
});
|
||||
|
||||
// Register a Right-Hand Sidebar component by providing a title for the right hand component.
|
||||
// Accepts the following:
|
||||
|
||||
@@ -13,6 +13,7 @@ import pluginReducers from '.';
|
||||
function getBaseState(): PluginsState {
|
||||
return {
|
||||
adminConsoleCustomComponents: {},
|
||||
adminConsoleCustomSections: {},
|
||||
adminConsoleReducers: {},
|
||||
components: {} as any,
|
||||
plugins: {},
|
||||
|
||||
@@ -13,7 +13,13 @@ import {UserTypes} from 'mattermost-redux/action_types';
|
||||
import {ActionTypes} from 'utils/constants';
|
||||
import {extractPluginConfiguration} from 'utils/plugins/plugin_setting_extraction';
|
||||
|
||||
import type {PluginsState, PluginComponent, AdminConsolePluginComponent, Menu} from 'types/store/plugins';
|
||||
import type {
|
||||
PluginsState,
|
||||
PluginComponent,
|
||||
AdminConsolePluginComponent,
|
||||
AdminConsolePluginCustomSection,
|
||||
Menu,
|
||||
} from 'types/store/plugins';
|
||||
|
||||
function hasMenuId(menu: Menu|PluginComponent, menuId: string) {
|
||||
if (!menu.subMenu) {
|
||||
@@ -361,6 +367,44 @@ function adminConsoleCustomComponents(state: {[pluginId: string]: Record<string,
|
||||
}
|
||||
}
|
||||
|
||||
function adminConsoleCustomSections(state: {[pluginId: string]: Record<string, AdminConsolePluginCustomSection>} = {}, action: AnyAction) {
|
||||
switch (action.type) {
|
||||
case ActionTypes.RECEIVED_ADMIN_CONSOLE_CUSTOM_SECTION: {
|
||||
if (!action.data) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const pluginId = action.data.pluginId;
|
||||
const key = action.data.key.toLowerCase();
|
||||
|
||||
const nextState = {...state};
|
||||
let nextObject: Record<string, AdminConsolePluginCustomSection> = {};
|
||||
if (nextState[pluginId]) {
|
||||
nextObject = {...nextState[pluginId]};
|
||||
}
|
||||
nextObject[key] = action.data;
|
||||
nextState[pluginId] = nextObject;
|
||||
|
||||
return nextState;
|
||||
}
|
||||
case ActionTypes.REMOVED_WEBAPP_PLUGIN: {
|
||||
if (!action.data || !state[action.data.id]) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const pluginId = action.data.id;
|
||||
const nextState = {...state};
|
||||
delete nextState[pluginId];
|
||||
return nextState;
|
||||
}
|
||||
|
||||
case UserTypes.LOGOUT_SUCCESS:
|
||||
return {};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function siteStatsHandlers(state: PluginsState['siteStatsHandlers'] = {}, action: AnyAction) {
|
||||
switch (action.type) {
|
||||
case ActionTypes.RECEIVED_PLUGIN_STATS_HANDLER:
|
||||
@@ -441,6 +485,10 @@ export default combineReducers({
|
||||
// React component to render on the plugin's system console.
|
||||
adminConsoleCustomComponents,
|
||||
|
||||
// objects where every key is a plugin id and the value is an object mapping keys to a custom
|
||||
// React component to render on the plugin's system console as custom section.
|
||||
adminConsoleCustomSections,
|
||||
|
||||
// objects where every key is a plugin id and the value is a promise to fetch stats from
|
||||
// a plugin to render on system console
|
||||
siteStatsHandlers,
|
||||
|
||||
@@ -29,6 +29,9 @@ export const getAdminDefinition = createSelector(
|
||||
export const getAdminConsoleCustomComponents = (state, pluginId) =>
|
||||
state.plugins.adminConsoleCustomComponents[pluginId] || {};
|
||||
|
||||
export const getAdminConsoleCustomSections = (state, pluginId) =>
|
||||
state.plugins.adminConsoleCustomSections[pluginId] || {};
|
||||
|
||||
export const getConsoleAccess = createSelector(
|
||||
'getConsoleAccess',
|
||||
getAdminDefinition,
|
||||
|
||||
@@ -49,6 +49,7 @@ export type PluginsState = {
|
||||
postTypes: {
|
||||
[postType: string]: PostPluginComponent;
|
||||
};
|
||||
|
||||
postCardTypes: {
|
||||
[postType: string]: PostPluginComponent;
|
||||
};
|
||||
@@ -56,11 +57,19 @@ export type PluginsState = {
|
||||
adminConsoleReducers: {
|
||||
[pluginId: string]: any;
|
||||
};
|
||||
|
||||
adminConsoleCustomComponents: {
|
||||
[pluginId: string]: {
|
||||
[settingName: string]: AdminConsolePluginComponent;
|
||||
};
|
||||
};
|
||||
|
||||
adminConsoleCustomSections: {
|
||||
[pluginId: string]: {
|
||||
[sectionKey: string]: AdminConsolePluginCustomSection;
|
||||
};
|
||||
};
|
||||
|
||||
siteStatsHandlers: {
|
||||
[pluginId: string]: PluginSiteStatsHandler;
|
||||
};
|
||||
@@ -149,6 +158,12 @@ export type AdminConsolePluginComponent = {
|
||||
};
|
||||
};
|
||||
|
||||
export type AdminConsolePluginCustomSection = {
|
||||
pluginId: string;
|
||||
key: string;
|
||||
component: React.Component;
|
||||
};
|
||||
|
||||
export type PostWillRenderEmbedPluginComponent = {
|
||||
id: string;
|
||||
pluginId: string;
|
||||
|
||||
@@ -235,6 +235,7 @@ export const ActionTypes = keyMirror({
|
||||
RECEIVED_ADMIN_CONSOLE_REDUCER: null,
|
||||
REMOVED_ADMIN_CONSOLE_REDUCER: null,
|
||||
RECEIVED_ADMIN_CONSOLE_CUSTOM_COMPONENT: null,
|
||||
RECEIVED_ADMIN_CONSOLE_CUSTOM_SECTION: null,
|
||||
RECEIVED_PLUGIN_STATS_HANDLER: null,
|
||||
RECEIVED_PLUGIN_USER_SETTINGS: null,
|
||||
|
||||
|
||||
@@ -44,6 +44,17 @@ export type PluginSettingsSchema = {
|
||||
header: string;
|
||||
footer: string;
|
||||
settings: PluginSetting[];
|
||||
sections?: PluginSettingSection[];
|
||||
};
|
||||
|
||||
export type PluginSettingSection = {
|
||||
key: string;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
settings: PluginSetting[];
|
||||
header?: string;
|
||||
footer?: string;
|
||||
custom?: boolean;
|
||||
};
|
||||
|
||||
export type PluginSetting = {
|
||||
|
||||
Reference in New Issue
Block a user