From 3c5fad563e466bd8c9741300e92a7a14877738e0 Mon Sep 17 00:00:00 2001 From: Colton Shaw <46071821+coltoneshaw@users.noreply.github.com> Date: Fri, 14 Apr 2023 09:34:28 -0700 Subject: [PATCH] Workspace dashboard improvements (#22883) Automatic Merge --- .../chips_list.test.tsx | 2 +- .../workspace-optimization/chips_list.tsx | 2 +- .../workspace-optimization/dashboard.data.tsx | 625 +++++++----------- .../workspace-optimization/dashboard.tsx | 192 +----- .../workspace-optimization/dashboard.type.ts | 75 +++ .../dashboard_checks/access.ts | 66 ++ .../dashboard_checks/config.ts | 84 +++ .../dashboard_checks/data_privacy.ts | 70 ++ .../dashboard_checks/easy_management.ts | 129 ++++ .../dashboard_checks/performance.ts | 72 ++ .../dashboard_checks/updates.ts | 94 +++ webapp/channels/src/i18n/en.json | 12 +- 12 files changed, 835 insertions(+), 588 deletions(-) create mode 100644 webapp/channels/src/components/admin_console/workspace-optimization/dashboard.type.ts create mode 100644 webapp/channels/src/components/admin_console/workspace-optimization/dashboard_checks/access.ts create mode 100644 webapp/channels/src/components/admin_console/workspace-optimization/dashboard_checks/config.ts create mode 100644 webapp/channels/src/components/admin_console/workspace-optimization/dashboard_checks/data_privacy.ts create mode 100644 webapp/channels/src/components/admin_console/workspace-optimization/dashboard_checks/easy_management.ts create mode 100644 webapp/channels/src/components/admin_console/workspace-optimization/dashboard_checks/performance.ts create mode 100644 webapp/channels/src/components/admin_console/workspace-optimization/dashboard_checks/updates.ts diff --git a/webapp/channels/src/components/admin_console/workspace-optimization/chips_list.test.tsx b/webapp/channels/src/components/admin_console/workspace-optimization/chips_list.test.tsx index 3953977204..514597a354 100644 --- a/webapp/channels/src/components/admin_console/workspace-optimization/chips_list.test.tsx +++ b/webapp/channels/src/components/admin_console/workspace-optimization/chips_list.test.tsx @@ -6,7 +6,7 @@ import {shallow} from 'enzyme'; import ChipsList, {ChipsInfoType} from 'components/admin_console/workspace-optimization/chips_list'; -import {ItemStatus} from './dashboard.data'; +import {ItemStatus} from './dashboard.type'; describe('components/admin_console/workspace-optimization/chips_list', () => { const overallScoreChips: ChipsInfoType = { diff --git a/webapp/channels/src/components/admin_console/workspace-optimization/chips_list.tsx b/webapp/channels/src/components/admin_console/workspace-optimization/chips_list.tsx index 2dd06b5bc1..d9911f749b 100644 --- a/webapp/channels/src/components/admin_console/workspace-optimization/chips_list.tsx +++ b/webapp/channels/src/components/admin_console/workspace-optimization/chips_list.tsx @@ -6,7 +6,7 @@ import {FormattedMessage} from 'react-intl'; import Chip from 'components/common/chip/chip'; -import {ItemStatus} from './dashboard.data'; +import {ItemStatus} from './dashboard.type'; import './dashboard.scss'; diff --git a/webapp/channels/src/components/admin_console/workspace-optimization/dashboard.data.tsx b/webapp/channels/src/components/admin_console/workspace-optimization/dashboard.data.tsx index dd5ee9b565..93bfad13b8 100644 --- a/webapp/channels/src/components/admin_console/workspace-optimization/dashboard.data.tsx +++ b/webapp/channels/src/components/admin_console/workspace-optimization/dashboard.data.tsx @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React from 'react'; +import React, {useEffect, useMemo, useState} from 'react'; import {useIntl} from 'react-intl'; import {useSelector} from 'react-redux'; @@ -14,56 +14,23 @@ import { AccountMultipleOutlineIcon, } from '@mattermost/compass-icons/components'; -import {getLicense} from 'mattermost-redux/selectors/entities/general'; +import {getLicense, getServerVersion} from 'mattermost-redux/selectors/entities/general'; import {GlobalState} from '@mattermost/types/store'; -import {CloudLinks, ConsolePages, DocLinks} from 'utils/constants'; +import {ConsolePages} from 'utils/constants'; import {daysToLicenseExpire, isEnterpriseOrE20License, getIsStarterLicense} from '../../../utils/license_utils'; import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink'; +import {AdminConfig} from '@mattermost/types/config'; +import {runConfigChecks} from './dashboard_checks/config'; +import {DataModel, ItemStatus, Options} from './dashboard.type'; +import {runAccessChecks} from './dashboard_checks/access'; +import {runDataPrivacyChecks} from './dashboard_checks/data_privacy'; +import {runPerformanceChecks} from './dashboard_checks/performance'; +import {runEaseOfUseChecks} from './dashboard_checks/easy_management'; +import {runUpdateChecks} from './dashboard_checks/updates'; -export type DataModel = { - [key: string]: { - title: string; - description: string; - descriptionOk: string; - items: ItemModel[]; - icon: React.ReactNode; - hide?: boolean; - }; -} - -export enum ItemStatus { - NONE = 'none', - OK = 'ok', - INFO = 'info', - WARNING = 'warning', - ERROR = 'error', -} - -export type ItemModel = { - id: string; - title: string; - description: string; - status: ItemStatus; - scoreImpact: number; - impactModifier: number; - configUrl?: string; - configText?: string; - telemetryAction?: string; - infoUrl?: string; - infoText?: string; -} - -export type UpdatesParam = { - serverVersion: { - type: string; - status: ItemStatus; - description: string; - }; -} - -const impactModifiers: Record = { +export const impactModifiers: Record = { [ItemStatus.NONE]: 1, [ItemStatus.OK]: 1, [ItemStatus.INFO]: 0.5, @@ -71,11 +38,191 @@ const impactModifiers: Record = { [ItemStatus.ERROR]: 0, }; -const useMetricsData = () => { +const getUpdatesData = async ( + config: Partial, + formatMessage: ReturnType['formatMessage'], + options: Options, +) => ({ + title: formatMessage({ + id: 'admin.reporting.workspace_optimization.updates.title', + defaultMessage: 'Server updates', + }), + description: formatMessage({ + id: 'admin.reporting.workspace_optimization.updates.description', + defaultMessage: 'An update is available.', + }), + descriptionOk: formatMessage({ + id: 'admin.reporting.workspace_optimization.updates.descriptionOk', + defaultMessage: 'Your workspace is completely up to date!', + }), + icon: ( +
+ +
+ ), + items: await runUpdateChecks(config, formatMessage, options), +}); + +const getConfigurationData = async ( + config: Partial, + formatMessage: ReturnType['formatMessage'], + options: Options, +) => ({ + title: formatMessage({ + id: 'admin.reporting.workspace_optimization.configuration.title', + defaultMessage: 'Configuration', + }), + description: formatMessage({ + id: 'admin.reporting.workspace_optimization.configuration.description', + defaultMessage: 'You have configuration issues to resolve', + }), + hide: options.isCloud, + descriptionOk: formatMessage({ + id: 'admin.reporting.workspace_optimization.configuration.descriptionOk', + defaultMessage: 'You\'ve successfully configured SSL and Session Lengths!', + }), + icon: ( +
+ +
+ ), + items: await runConfigChecks(config, formatMessage, options), +}); + +const getAccessData = async ( + config: Partial, + formatMessage: ReturnType['formatMessage'], + options: Options, +) => ({ + title: formatMessage({ + id: 'admin.reporting.workspace_optimization.access.title', + defaultMessage: 'Workspace access', + }), + description: formatMessage({ + id: 'admin.reporting.workspace_optimization.access.description', + defaultMessage: 'Web server configuration may be affecting access to your Mattermost workspace.', + }), + hide: options.isCloud, + descriptionOk: formatMessage({ + id: 'admin.reporting.workspace_optimization.access.descriptionOk', + defaultMessage: 'Your web server configuration is passing a live URL test!', + }), + icon: ( +
+ +
+ ), + items: await runAccessChecks(config, formatMessage), +}); + +const getPerformanceData = async ( + config: Partial, + formatMessage: ReturnType['formatMessage'], + options: Options, +) => ({ + title: formatMessage({ + id: 'admin.reporting.workspace_optimization.performance.title', + defaultMessage: 'Performance', + }), + description: formatMessage({ + id: 'admin.reporting.workspace_optimization.performance.description', + defaultMessage: 'Your server would benefit from some performance tweaks.', + }), + hide: options.isCloud, + descriptionOk: formatMessage({ + id: 'admin.reporting.workspace_optimization.performance.descriptionOk', + defaultMessage: 'Your search performance suits your workspace usage!', + }), + icon: ( +
+ +
+ ), + items: await runPerformanceChecks(config, formatMessage, options), +}); + +const getDataPrivacyData = async ( + config: Partial, + formatMessage: ReturnType['formatMessage'], + options: Options, +) => ({ + title: formatMessage({ + id: 'admin.reporting.workspace_optimization.data_privacy.title', + defaultMessage: 'Data privacy', + }), + description: formatMessage({ + id: 'admin.reporting.workspace_optimization.data_privacy.description', + defaultMessage: 'Get better insight and control over your data.', + }), + descriptionOk: formatMessage({ + id: 'admin.reporting.workspace_optimization.data_privacy.descriptionOk', + defaultMessage: 'You\'ve enabled data retention and compliance features!', + }), + icon: ( +
+ +
+ ), + items: await runDataPrivacyChecks(config, formatMessage, options), +}); + +const getEaseOfManagementData = async ( + config: Partial, + formatMessage: ReturnType['formatMessage'], + options: Options, +) => ({ + title: formatMessage({ + id: 'admin.reporting.workspace_optimization.ease_of_management.title', + defaultMessage: 'Ease of management', + }), + description: formatMessage({ + id: 'admin.reporting.workspace_optimization.ease_of_management.description', + defaultMessage: 'Make it easier to manage your Mattermost workspace.', + }), + descriptionOk: formatMessage({ + id: 'admin.reporting.workspace_optimization.ease_of_management.descriptionOk', + defaultMessage: 'Your user authentication setup is appropriate based on your current usage!', + }), + icon: ( +
+ +
+ ), + items: await runEaseOfUseChecks(config, formatMessage, options), +}); + +const useMetricsData = ( + config: Partial, +) => { + const [loading, setLoading] = useState(true); + const [data, setData] = useState(undefined); + const {formatMessage} = useIntl(); const prevTrialLicense = useSelector((state: GlobalState) => state.entities.admin.prevTrialLicense); const license = useSelector(getLicense); + // get the currently installed server version + const installedVersion = useSelector((state: GlobalState) => getServerVersion(state)); + const analytics = useSelector((state: GlobalState) => state.entities.admin.analytics) as unknown as Options['analytics']; + const canStartTrial = license?.IsLicensed !== 'true' && prevTrialLicense?.IsLicensed !== 'true'; const daysUntilExpiration = daysToLicenseExpire(license) || -1; @@ -87,358 +234,46 @@ const useMetricsData = () => { const [, contactSalesLink] = useOpenSalesLink(); - const trialOrEnterpriseCtaConfig = { + const trialOrEnterpriseCtaConfig = useMemo(() => ({ configUrl: canStartTrial ? ConsolePages.LICENSE : contactSalesLink, configText: canStartTrial ? formatMessage({id: 'admin.reporting.workspace_optimization.cta.startTrial', defaultMessage: 'Start trial'}) : formatMessage({id: 'admin.reporting.workspace_optimization.cta.upgradeLicense', defaultMessage: 'Contact sales'}), + }), [canStartTrial, contactSalesLink, formatMessage]); + + const options: Options = useMemo(() => ({ + isLicensed, + isEnterpriseLicense, + trialOrEnterpriseCtaConfig, + isStarterLicense, + isCloud, + analytics, + installedVersion, + }), [isLicensed, isEnterpriseLicense, trialOrEnterpriseCtaConfig, isStarterLicense, isCloud, analytics, installedVersion]); + + useEffect(() => { + setLoading(true); + const refreshData = async () => { + const data = { + updates: await getUpdatesData(config, formatMessage, options), + configuration: await getConfigurationData(config, formatMessage, options), + access: await getAccessData(config, formatMessage, options), + performance: await getPerformanceData(config, formatMessage, options), + dataPrivacy: await getDataPrivacyData(config, formatMessage, options), + easyManagement: await getEaseOfManagementData(config, formatMessage, options), + }; + + return data; + }; + + refreshData().then((data) => { + setData(data); + setLoading(false); + }); + }, [config, formatMessage, options]); + + return { + data, + loading, }; - - const getUpdatesData = (data: UpdatesParam) => ({ - title: formatMessage({ - id: 'admin.reporting.workspace_optimization.updates.title', - defaultMessage: 'Server updates', - }), - description: formatMessage({ - id: 'admin.reporting.workspace_optimization.updates.description', - defaultMessage: 'An update is available.', - }), - descriptionOk: formatMessage({ - id: 'admin.reporting.workspace_optimization.updates.descriptionOk', - defaultMessage: 'Your workspace is completely up to date!', - }), - icon: ( -
- -
- ), - items: [ - { - id: 'server_version', - title: formatMessage({ - id: 'admin.reporting.workspace_optimization.updates.server_version.status.title', - defaultMessage: '{type} version update available.', - }, {type: data.serverVersion.type}), - description: data.serverVersion.description, - configUrl: CloudLinks.DOWNLOAD_UPDATE, - configText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.downloadUpdate', defaultMessage: 'Download update'}), - infoUrl: DocLinks.UPGRADE_SERVER, - infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}), - telemetryAction: 'server-version', - status: data.serverVersion.status, - scoreImpact: 15, - impactModifier: impactModifiers[data.serverVersion.status], - }, - ], - }); - - type ConfigurationParam = { - ssl: { - status: ItemStatus; - }; - sessionLength: { - status: ItemStatus; - }; - } - - const getConfigurationData = (data: ConfigurationParam) => ({ - title: formatMessage({ - id: 'admin.reporting.workspace_optimization.configuration.title', - defaultMessage: 'Configuration', - }), - description: formatMessage({ - id: 'admin.reporting.workspace_optimization.configuration.description', - defaultMessage: 'You have configuration issues to resolve', - }), - hide: isCloud, - descriptionOk: formatMessage({ - id: 'admin.reporting.workspace_optimization.configuration.descriptionOk', - defaultMessage: 'You\'ve successfully configured SSL and Session Lengths!', - }), - icon: ( -
- -
- ), - items: [ - { - id: 'ssl', - title: formatMessage({ - id: 'admin.reporting.workspace_optimization.configuration.ssl.title', - defaultMessage: 'Configure SSL to make your server more secure', - }), - description: formatMessage({ - id: 'admin.reporting.workspace_optimization.configuration.ssl.description', - defaultMessage: 'We strongly recommend securing your Mattermost workspace by configuring SSL in production environments.', - }), - infoUrl: DocLinks.SSL_CERTIFICATE, - infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}), - telemetryAction: 'ssl', - status: data.ssl.status, - scoreImpact: 25, - impactModifier: impactModifiers[data.ssl.status], - }, - { - id: 'session-length', - title: formatMessage({ - id: 'admin.reporting.workspace_optimization.configuration.session_length.title', - defaultMessage: 'Session lengths is set to default', - }), - description: formatMessage({ - id: 'admin.reporting.workspace_optimization.configuration.session_length.description', - defaultMessage: 'Your session length is set to the default of 30 days. A longer session length provides convenience, and a shorter session provides tighter security. We recommend adjusting this based on your organization\'s security policies.', - }), - configUrl: ConsolePages.SESSION_LENGTHS, - configText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.configureSessionLength', defaultMessage: 'Configure session length'}), - infoUrl: DocLinks.SESSION_LENGTHS, - infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}), - telemetryAction: 'session-length', - status: data.sessionLength.status, - scoreImpact: 8, - impactModifier: impactModifiers[data.sessionLength.status], - }, - ], - }); - - type AccessParam = { - siteUrl: { - status: ItemStatus; - }; - } - - const getAccessData = (data: AccessParam) => ({ - title: formatMessage({ - id: 'admin.reporting.workspace_optimization.access.title', - defaultMessage: 'Workspace access', - }), - description: formatMessage({ - id: 'admin.reporting.workspace_optimization.access.description', - defaultMessage: 'Web server configuration may be affecting access to your Mattermost workspace.', - }), - hide: isCloud, - descriptionOk: formatMessage({ - id: 'admin.reporting.workspace_optimization.access.descriptionOk', - defaultMessage: 'Your web server configuration is passing a live URL test!', - }), - icon: ( -
- -
- ), - items: [ - { - id: 'site-url', - title: formatMessage({ - id: 'admin.reporting.workspace_optimization.access.site_url.title', - defaultMessage: 'Misconfigured web server', - }), - description: formatMessage({ - id: 'admin.reporting.workspace_optimization.access.site_url.description', - defaultMessage: 'Your web server settings aren\'t passing a live URL test which means your workspace may not be accessible to users. We recommend updating your web server settings.', - }), - configUrl: ConsolePages.WEB_SERVER, - configText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.configureWebServer', defaultMessage: 'Configure web server'}), - infoUrl: DocLinks.SITE_URL, - infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}), - telemetryAction: 'site-url', - status: data.siteUrl.status, - scoreImpact: 12, - impactModifier: impactModifiers[data.siteUrl.status], - }, - ], - }); - - type PerformanceParam = { - search: { - status: ItemStatus; - }; - } - - const getPerformanceData = (data: PerformanceParam) => ({ - title: formatMessage({ - id: 'admin.reporting.workspace_optimization.performance.title', - defaultMessage: 'Performance', - }), - description: formatMessage({ - id: 'admin.reporting.workspace_optimization.performance.description', - defaultMessage: 'Your server would benefit from some performance tweaks.', - }), - hide: isCloud, - descriptionOk: formatMessage({ - id: 'admin.reporting.workspace_optimization.performance.descriptionOk', - defaultMessage: 'Your search performance suits your workspace usage!', - }), - icon: ( -
- -
- ), - items: [ - { - id: 'search', - title: formatMessage({ - id: 'admin.reporting.workspace_optimization.performance.search.title', - defaultMessage: 'Search performance', - }), - description: formatMessage({ - id: 'admin.reporting.workspace_optimization.performance.search.description', - defaultMessage: 'Your server has reached over 500 users and 2 million posts which can result in slow search performance. We recommend enabling Elasticsearch for better performance.', - }), - ...(isLicensed && isEnterpriseLicense ? { - configUrl: ConsolePages.ELASTICSEARCH, - configText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.configureElasticsearch', defaultMessage: 'Try Elasticsearch'}), - } : trialOrEnterpriseCtaConfig), - infoUrl: DocLinks.ELASTICSEARCH, - infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}), - telemetryAction: 'search-optimization', - status: data.search.status, - scoreImpact: 20, - impactModifier: impactModifiers[data.search.status], - }, - ], - }); - - type DataPrivacyParam = { - retention: { - status: ItemStatus; - }; - } - - // TBD - const getDataPrivacyData = (data: DataPrivacyParam) => ({ - title: formatMessage({ - id: 'admin.reporting.workspace_optimization.data_privacy.title', - defaultMessage: 'Data privacy', - }), - description: formatMessage({ - id: 'admin.reporting.workspace_optimization.data_privacy.description', - defaultMessage: 'Get better insight and control over your data.', - }), - descriptionOk: formatMessage({ - id: 'admin.reporting.workspace_optimization.data_privacy.descriptionOk', - defaultMessage: 'You\'ve enabled data retention and compliance features!', - }), - icon: ( -
- -
- ), - items: [ - { - id: 'data-retention', - title: formatMessage({ - id: 'admin.reporting.workspace_optimization.data_privacy.retention.title', - defaultMessage: 'Become more data aware', - }), - description: formatMessage({ - id: 'admin.reporting.workspace_optimization.data_privacy.retention.description', - defaultMessage: 'Organizations in highly regulated industries require more control and insight with their data. We recommend enabling Data Retention and Compliance features.', - }), - ...(isLicensed && isEnterpriseLicense ? { - configUrl: ConsolePages.DATA_RETENTION, - configText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.configureDataRetention', defaultMessage: 'Try data retention'}), - } : trialOrEnterpriseCtaConfig), - infoUrl: DocLinks.DATA_RETENTION_POLICY, - infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}), - telemetryAction: 'data-retention', - status: data.retention.status, - scoreImpact: 16, - impactModifier: impactModifiers[data.retention.status], - }, - ], - }); - - type EaseOfManagementParam = { - ldap: { - status: ItemStatus; - }; - guestAccounts?: { - status: ItemStatus; - }; - } - - // TBD - const getEaseOfManagementData = (data: EaseOfManagementParam) => ({ - title: formatMessage({ - id: 'admin.reporting.workspace_optimization.ease_of_management.title', - defaultMessage: 'Ease of management', - }), - description: formatMessage({ - id: 'admin.reporting.workspace_optimization.ease_of_management.description', - defaultMessage: 'Make it easier to manage your Mattermost workspace.', - }), - descriptionOk: formatMessage({ - id: 'admin.reporting.workspace_optimization.ease_of_management.descriptionOk', - defaultMessage: 'Your user authentication setup is appropriate based on your current usage!', - }), - icon: ( -
- -
- ), - items: [ - { - id: 'ad-ldap', - title: formatMessage({ - id: 'admin.reporting.workspace_optimization.ease_of_management.ldap.title', - defaultMessage: 'AD/LDAP integration recommended', - }), - description: formatMessage({ - id: 'admin.reporting.workspace_optimization.ease_of_management.ldap.description', - defaultMessage: 'You\'ve reached over 100 users! We recommend setting up AD/LDAP user authentication for easier onboarding as well as automated deactivations and role assignments.', - }), - ...(isLicensed && !isStarterLicense ? { - configUrl: ConsolePages.AD_LDAP, - configText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.configureLDAP', defaultMessage: 'Try AD/LDAP'}), - } : trialOrEnterpriseCtaConfig), - infoUrl: DocLinks.AD_LDAP, - infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}), - telemetryAction: 'ad-ldap', - status: data.ldap.status, - scoreImpact: 22, - impactModifier: impactModifiers[data.ldap.status], - }, - - // commented out for now. - // @see discussion here: https://github.com/mattermost/mattermost-webapp/pull/9822#discussion_r806879385 - // { - // id: 'guest-accounts', - // title: formatMessage({ - // id: 'admin.reporting.workspace_optimization.ease_of_management.guests_accounts.title', - // defaultMessage: 'Guest Accounts recommended', - // }), - // description: formatMessage({ - // id: 'admin.reporting.workspace_optimization.ease_of_management.guests_accounts.description', - // defaultMessage: 'Several user accounts are using different domains than your Site URL. You can control user access to channels and teams with guest accounts. We recommend starting an Enterprise trial and enabling Guest Access.', - // }), - // ...trialOrEnterpriseCtaConfig, - // infoUrl: 'https://docs.mattermost.com/onboard/guest-accounts.html', - // infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}), - // telemetryAction: 'guest-accounts', - // status: data.guestAccounts.status, - // scoreImpact: 6, - // impactModifier: impactModifiers[data.guestAccounts.status], - // }, - ], - }); - - return {getAccessData, getConfigurationData, getUpdatesData, getPerformanceData, getDataPrivacyData, getEaseOfManagementData, isLicensed, isEnterpriseLicense}; }; export default useMetricsData; diff --git a/webapp/channels/src/components/admin_console/workspace-optimization/dashboard.tsx b/webapp/channels/src/components/admin_console/workspace-optimization/dashboard.tsx index 6fcbd8b6f3..2409601512 100644 --- a/webapp/channels/src/components/admin_console/workspace-optimization/dashboard.tsx +++ b/webapp/channels/src/components/admin_console/workspace-optimization/dashboard.tsx @@ -4,17 +4,11 @@ import {CheckIcon} from '@mattermost/compass-icons/components'; import classNames from 'classnames'; -import React, {useEffect, useState} from 'react'; -import {useIntl} from 'react-intl'; -import {useSelector} from 'react-redux'; +import React from 'react'; import styled from 'styled-components'; -import {GlobalState} from '@mattermost/types/store'; -import {getServerVersion} from 'mattermost-redux/selectors/entities/general'; -import {Client4} from 'mattermost-redux/client'; import Accordion, {AccordionItemType} from 'components/common/accordion/accordion'; -import {elasticsearchTest, ldapTest, testSiteURL} from '../../../actions/admin_actions'; import LoadingScreen from '../../loading_screen'; import FormattedAdminHeader from '../../widgets/admin_console/formatted_admin_header'; import {Props} from '../admin_console'; @@ -22,10 +16,11 @@ import {Props} from '../admin_console'; import ChipsList, {ChipsInfoType} from './chips_list'; import CtaButtons from './cta_buttons'; -import useMetricsData, {DataModel, ItemStatus, UpdatesParam} from './dashboard.data'; +import useMetricsData from './dashboard.data'; import './dashboard.scss'; import OverallScore from './overall-score'; +import {ItemStatus} from './dashboard.type'; const AccordionItem = styled.div` padding: 12px; @@ -49,180 +44,7 @@ const successIcon = ( ); const WorkspaceOptimizationDashboard = (props: Props) => { - const [loading, setLoading] = useState(true); - const [versionData, setVersionData] = useState({type: '', description: '', status: ItemStatus.NONE}); - - // const [guestAccountStatus, setGuestAccountStatus] = useState('none'); - const [liveUrlStatus, setLiveUrlStatus] = useState(ItemStatus.ERROR); - const [elastisearchStatus, setElasticsearchStatus] = useState(ItemStatus.INFO); - const [ldapStatus, setLdapStatus] = useState(ItemStatus.INFO); - const [dataRetentionStatus, setDataRetentionStatus] = useState(ItemStatus.INFO); - const {formatMessage} = useIntl(); - const {getAccessData, getConfigurationData, getUpdatesData, getPerformanceData, getDataPrivacyData, getEaseOfManagementData, isLicensed, isEnterpriseLicense} = useMetricsData(); - - // get the currently installed server version - const installedVersion = useSelector((state: GlobalState) => getServerVersion(state)); - const analytics = useSelector((state: GlobalState) => state.entities.admin.analytics); - const {TOTAL_USERS: totalUsers, TOTAL_POSTS: totalPosts} = analytics!; - - // gather locally available data - const { - ServiceSettings, - DataRetentionSettings, - ElasticsearchSettings, - LdapSettings, - - // TeamSettings, - // GuestAccountsSettings, - } = props.config; - const {location} = document; - - const sessionLengthWebInHours = ServiceSettings?.SessionLengthWebInHours || -1; - - const testURL = () => { - if (!ServiceSettings?.SiteURL) { - return Promise.resolve(); - } - - const onSuccess = ({status}: any) => setLiveUrlStatus(status === 'OK' ? ItemStatus.OK : ItemStatus.ERROR); - const onError = () => setLiveUrlStatus(ItemStatus.ERROR); - return testSiteURL(onSuccess, onError, ServiceSettings?.SiteURL); - }; - - const testDataRetention = async () => { - if (!isLicensed || !isEnterpriseLicense) { - return Promise.resolve(); - } - - if (DataRetentionSettings?.EnableMessageDeletion || DataRetentionSettings?.EnableFileDeletion) { - setDataRetentionStatus(ItemStatus.OK); - return Promise.resolve(); - } - - const result = await fetch(`${Client4.getBaseRoute()}/data_retention/policies?page=0&per_page=0`).then((result) => result.json()); - - setDataRetentionStatus(result.total_count > 0 ? ItemStatus.OK : ItemStatus.INFO); - return Promise.resolve(); - }; - - const fetchVersion = async () => { - const result = await fetch(`${Client4.getBaseRoute()}/latest_version`).then((result) => result.json()); - - if (result.tag_name) { - const sanitizedVersion = result.tag_name.startsWith('v') ? result.tag_name.slice(1) : result.tag_name; - const newVersionParts = sanitizedVersion.split('.'); - const installedVersionParts = installedVersion.split('.').slice(0, 3); - - // quick general check if a newer version is available - let type = ''; - let status: ItemStatus = ItemStatus.OK; - - if (newVersionParts.join('') > installedVersionParts.join('')) { - // get correct values to be inserted into the accordion item - switch (true) { - case newVersionParts[0] > installedVersionParts[0]: - type = formatMessage({ - id: 'admin.reporting.workspace_optimization.updates.server_version.update_type.major', - defaultMessage: 'Major', - }); - status = ItemStatus.ERROR; - break; - case newVersionParts[1] > installedVersionParts[1]: - type = formatMessage({ - id: 'admin.reporting.workspace_optimization.updates.server_version.update_type.minor', - defaultMessage: 'Minor', - }); - status = ItemStatus.WARNING; - break; - case newVersionParts[2] > installedVersionParts[2]: - type = formatMessage({ - id: 'admin.reporting.workspace_optimization.updates.server_version.update_type.patch', - defaultMessage: 'Patch', - }); - status = ItemStatus.INFO; - break; - } - } - - setVersionData({type, description: result.body, status}); - } - }; - - const testElasticsearch = () => { - if (!isLicensed || !isEnterpriseLicense || !(ElasticsearchSettings?.EnableIndexing && ElasticsearchSettings?.EnableSearching)) { - return Promise.resolve(); - } - - const onSuccess = ({status}: any) => setElasticsearchStatus(status === 'OK' ? ItemStatus.OK : ItemStatus.INFO); - const onError = () => setElasticsearchStatus(ItemStatus.INFO); - - return elasticsearchTest(props.config, onSuccess, onError); - }; - - const testLdap = () => { - if (!isLicensed || !LdapSettings?.Enable) { - return Promise.resolve(); - } - - const onSuccess = ({status}: any) => setLdapStatus(status === 'OK' ? ItemStatus.OK : ItemStatus.INFO); - const onError = () => setLdapStatus(ItemStatus.INFO); - - return ldapTest(onSuccess, onError); - }; - - // commented out for now. - // @see discussion here: https://github.com/mattermost/mattermost-webapp/pull/9822#discussion_r806879385 - // const fetchGuestAccounts = async () => { - // if (TeamSettings?.EnableOpenServer && GuestAccountsSettings?.Enable) { - // let usersArray = await fetch(`${Client4.getBaseRoute()}/users/invalid_emails`).then((result) => result.json()); - // - // // this setting is just a string with a list of domains, or an empty string - // if (GuestAccountsSettings?.RestrictCreationToDomains) { - // const domainList = GuestAccountsSettings?.RestrictCreationToDomains; - // usersArray = usersArray.filter(({email}: Record) => domainList.includes((email as string).split('@')[1])); - // } - // - // // if guest accounts make up more than 5% of the user base show the info accordion - // if (usersArray.length > (totalUsers as number * 0.05)) { - // setGuestAccountStatus(ItemStatus.INFO); - // return; - // } - // } - // - // setGuestAccountStatus(ItemStatus.OK); - // }; - - useEffect(() => { - const promises = []; - promises.push(testURL()); - promises.push(testLdap()); - promises.push(fetchVersion()); - promises.push(testElasticsearch()); - promises.push(testDataRetention()); - - // promises.push(fetchGuestAccounts()); - Promise.all(promises).then(() => setLoading(false)); - }, [props.config, isLicensed, isEnterpriseLicense]); - - const data: DataModel = { - updates: getUpdatesData({serverVersion: versionData}), - configuration: getConfigurationData({ - ssl: {status: location.protocol === 'https:' ? ItemStatus.OK : ItemStatus.ERROR}, - sessionLength: {status: sessionLengthWebInHours === 720 ? ItemStatus.INFO : ItemStatus.OK}, - }), - access: getAccessData({siteUrl: {status: liveUrlStatus}}), - performance: getPerformanceData({ - search: { - status: totalPosts < 2_000_000 && totalUsers < 500 ? ItemStatus.OK : elastisearchStatus, - }, - }), - dataPrivacy: getDataPrivacyData({retention: {status: dataRetentionStatus}}), - easyManagement: getEaseOfManagementData({ - ldap: {status: totalUsers < 100 ? ItemStatus.OK : ldapStatus}, - - // guestAccounts: {status: guestAccountStatus}, - }), - }; + const {data, loading} = useMetricsData(props.config); const overallScoreChips: ChipsInfoType = { [ItemStatus.INFO]: 0, @@ -236,7 +58,7 @@ const WorkspaceOptimizationDashboard = (props: Props) => { }; // eslint-disable-next-line @typescript-eslint/no-unused-vars - const accData: AccordionItemType[] = Object.entries(data).filter(([_, y]) => !y.hide).map(([accordionKey, accordionData]) => { + const accordionItemsData: AccordionItemType[] | undefined = data && Object.entries(data).filter(([_, y]) => !y.hide).map(([accordionKey, accordionData]) => { const accordionDataChips: ChipsInfoType = { [ItemStatus.INFO]: 0, [ItemStatus.WARNING]: 0, @@ -297,7 +119,7 @@ const WorkspaceOptimizationDashboard = (props: Props) => { }; }); - return loading ? : ( + return loading || !accordionItemsData ? : (
{ chartValue={Math.floor((overallScore.current / overallScore.max) * 100)} />
diff --git a/webapp/channels/src/components/admin_console/workspace-optimization/dashboard.type.ts b/webapp/channels/src/components/admin_console/workspace-optimization/dashboard.type.ts new file mode 100644 index 0000000000..b87c9d1bdd --- /dev/null +++ b/webapp/channels/src/components/admin_console/workspace-optimization/dashboard.type.ts @@ -0,0 +1,75 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +export type DataModel = { + [key: string]: { + title: string; + description: string; + descriptionOk: string; + items: ItemModel[]; + icon: React.ReactNode; + hide?: boolean; + }; +} + +export enum ItemStatus { + + /** Return NONE if it's not relevant, not configured, or not an option */ + NONE = 'none', + + /** Return OK if all checks for this have passed */ + OK = 'ok', + + /** Return info if it might not be relevant to them, but they could utilize it */ + INFO = 'info', + WARNING = 'warning', + ERROR = 'error', +} + +export type ItemModel = { + id: string; + title: string; + description: string; + status: ItemStatus; + scoreImpact: number; + impactModifier: number; + configUrl?: string; + configText?: string; + telemetryAction?: string; + infoUrl?: string; + infoText?: string; +} + +export type UpdatesParam = { + serverVersion: { + type: string; + status: ItemStatus; + description: string; + }; +} + +type Analytics = { + DAILY_ACTIVE_USERS: number; + MONTHLY_ACTIVE_USERS: number; + TOTAL_INACTIVE_USERS: number; + TOTAL_MASTER_DB_CONNECTIONS: number; + TOTAL_POSTS: number; + TOTAL_PRIVATE_GROUPS: number; + TOTAL_PUBLIC_CHANNELS: number; + TOTAL_READ_DB_CONNECTIONS: number; + TOTAL_TEAMS: number; + TOTAL_USERS: number; + TOTAL_WEBSOCKET_CONNECTIONS: number; +} +export type Options = { + isLicensed: boolean; + isEnterpriseLicense: boolean; + trialOrEnterpriseCtaConfig: { + configUrl: string; + configText: string; + }; + isCloud: boolean; + isStarterLicense: boolean; + analytics: Analytics | undefined; + installedVersion: string; +}; diff --git a/webapp/channels/src/components/admin_console/workspace-optimization/dashboard_checks/access.ts b/webapp/channels/src/components/admin_console/workspace-optimization/dashboard_checks/access.ts new file mode 100644 index 0000000000..e8f37024bb --- /dev/null +++ b/webapp/channels/src/components/admin_console/workspace-optimization/dashboard_checks/access.ts @@ -0,0 +1,66 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {useIntl} from 'react-intl'; +import {ItemModel, ItemStatus} from '../dashboard.type'; +import {ConsolePages, DocLinks} from 'utils/constants'; +import {impactModifiers} from '../dashboard.data'; +import {AdminConfig} from '@mattermost/types/config'; +import {testSiteURL} from 'actions/admin_actions'; + +/** + * + * @description Checking to see if the siteURL is configured correctly by running it through the same "check siteURL" button that exists on the webserver page. + */ +const siteURLCheck = async (config: Partial, formatMessage: ReturnType['formatMessage']): Promise => { + let status = ItemStatus.OK; + const testURL = async () => { + if (!config.ServiceSettings?.SiteURL) { + status = ItemStatus.ERROR; + } + + const onSuccess = ({status: s}: any) => { + if (s === 'OK') { + status = ItemStatus.OK; + } + }; + const onError = () => { + status = ItemStatus.ERROR; + }; + await testSiteURL(onSuccess, onError, config.ServiceSettings?.SiteURL); + }; + + await testURL(); + return { + id: 'site-url', + title: formatMessage({ + id: 'admin.reporting.workspace_optimization.access.site_url.title', + defaultMessage: 'Misconfigured web server', + }), + description: formatMessage({ + id: 'admin.reporting.workspace_optimization.access.site_url.description', + defaultMessage: 'Your web server settings aren\'t passing a live URL test which means your workspace may not be accessible to users. We recommend updating your web server settings.', + }), + configUrl: ConsolePages.WEB_SERVER, + configText: formatMessage({id: 'admin.reporting.workspace_optimization.access.site_url.cta', defaultMessage: 'Configure web server'}), + infoUrl: DocLinks.SITE_URL, + infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}), + telemetryAction: 'site-url', + status, + scoreImpact: 12, + impactModifier: impactModifiers[status], + }; +}; + +const checks = [ + siteURLCheck, +]; + +export const runAccessChecks = async ( + config: Partial, + formatMessage: ReturnType['formatMessage'], +) => { + const results = await Promise.all(checks.map((check) => check(config, formatMessage))); + return results; +}; + diff --git a/webapp/channels/src/components/admin_console/workspace-optimization/dashboard_checks/config.ts b/webapp/channels/src/components/admin_console/workspace-optimization/dashboard_checks/config.ts new file mode 100644 index 0000000000..3cc59e7939 --- /dev/null +++ b/webapp/channels/src/components/admin_console/workspace-optimization/dashboard_checks/config.ts @@ -0,0 +1,84 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import {AdminConfig} from '@mattermost/types/config'; +import {useIntl} from 'react-intl'; +import {ConsolePages, DocLinks} from 'utils/constants'; +import {ItemModel, ItemStatus, Options} from '../dashboard.type'; +import {impactModifiers} from '../dashboard.data'; + +/** + * + * @description This checks to see if the user's active session is done over https. This does not check if the server is configured to use https. + */ +const ssl = ( + config: Partial, + formatMessage: ReturnType['formatMessage'], + options: Options, +): ItemModel => { + const status = document.location.protocol === 'https:' ? ItemStatus.OK : ItemStatus.ERROR; + + return { + id: 'ssl', + title: formatMessage({ + id: 'admin.reporting.workspace_optimization.configuration.ssl.title', + defaultMessage: 'Configure SSL to make your server more secure', + }), + description: formatMessage({ + id: 'admin.reporting.workspace_optimization.configuration.ssl.description', + defaultMessage: 'We strongly recommend securing your Mattermost workspace by configuring SSL in production environments.', + }), + infoUrl: DocLinks.SSL_CERTIFICATE, + infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}), + telemetryAction: 'ssl', + status, + scoreImpact: 25, + impactModifier: impactModifiers[status], + }; +}; + +/** + * + * @description This checks to see if the user has adjusted the default session lengths to something other than 720 hours. + */ +const sessionLength = ( + config: Partial, + formatMessage: ReturnType['formatMessage'], + options: Options, +): ItemModel => { + const status = config.ServiceSettings?.SessionLengthMobileInHours === 720 ? ItemStatus.INFO : ItemStatus.OK; + return { + id: 'session-length', + title: formatMessage({ + id: 'admin.reporting.workspace_optimization.configuration.session_length.title', + defaultMessage: 'Session lengths is set to default', + }), + description: formatMessage({ + id: 'admin.reporting.workspace_optimization.configuration.session_length.description', + defaultMessage: 'Your session length is set to the default of 30 days. A longer session length provides convenience, and a shorter session provides tighter security. We recommend adjusting this based on your organization\'s security policies.', + }), + configUrl: ConsolePages.SESSION_LENGTHS, + configText: formatMessage({id: 'admin.reporting.workspace_optimization.configuration.session_length.cta', defaultMessage: 'Configure session length'}), + infoUrl: DocLinks.SESSION_LENGTHS, + infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}), + telemetryAction: 'session-length', + status, + scoreImpact: 8, + impactModifier: impactModifiers[status], + }; +}; + +export const runConfigChecks = async ( + config: Partial, + formatMessage: ReturnType['formatMessage'], + options: Options, +) => { + const checks = [ + ssl, + sessionLength, + ]; + const results = await Promise.all(checks.map((check) => check(config, formatMessage, options))); + return results; +}; diff --git a/webapp/channels/src/components/admin_console/workspace-optimization/dashboard_checks/data_privacy.ts b/webapp/channels/src/components/admin_console/workspace-optimization/dashboard_checks/data_privacy.ts new file mode 100644 index 0000000000..049f90015c --- /dev/null +++ b/webapp/channels/src/components/admin_console/workspace-optimization/dashboard_checks/data_privacy.ts @@ -0,0 +1,70 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {useIntl} from 'react-intl'; +import {impactModifiers} from '../dashboard.data'; +import {ConsolePages, DocLinks} from 'utils/constants'; +import {Client4} from 'mattermost-redux/client'; +import {ItemStatus, Options} from '../dashboard.type'; +import {AdminConfig} from '@mattermost/types/config'; + +/** + * + * @description Checks if they they have a global policy deletion enabled, or if a custom policy has been created. + */ +const dataRetentionCheck = async ( + config: Partial, + formatMessage: ReturnType['formatMessage'], + options: Options, +) => { + const testDataRetention = async ( + config: Partial, + options: Options, + ) => { + if (!options.isLicensed || !options.isEnterpriseLicense) { + return ItemStatus.INFO; + } + + if (config.DataRetentionSettings?.EnableMessageDeletion || config.DataRetentionSettings?.EnableFileDeletion) { + return ItemStatus.OK; + } + + const policyCount: {total_count: number} = await fetch(`${Client4.getBaseRoute()}/data_retention/policies_count`).then((result) => result.json()); + return policyCount.total_count > 0 ? ItemStatus.OK : ItemStatus.INFO; + }; + + const status = await testDataRetention(config, options); + return { + id: 'data-retention', + title: formatMessage({ + id: 'admin.reporting.workspace_optimization.data_privacy.retention.title', + defaultMessage: 'Become more data aware', + }), + description: formatMessage({ + id: 'admin.reporting.workspace_optimization.data_privacy.retention.description', + defaultMessage: 'Organizations in highly regulated industries require more control and insight with their data. We recommend enabling Data Retention and Compliance features.', + }), + ...(options.isLicensed && options.isEnterpriseLicense ? { + configUrl: ConsolePages.DATA_RETENTION, + configText: formatMessage({id: 'admin.reporting.workspace_optimization.data_privacy.retention.cta', defaultMessage: 'Try data retention'}), + } : options.trialOrEnterpriseCtaConfig), + infoUrl: DocLinks.DATA_RETENTION_POLICY, + infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}), + telemetryAction: 'data-retention', + status, + scoreImpact: 16, + impactModifier: impactModifiers[status], + }; +}; + +export const runDataPrivacyChecks = async ( + config: Partial, + formatMessage: ReturnType['formatMessage'], + options: Options, +) => { + const checks = [ + dataRetentionCheck, + ]; + const results = await Promise.all(checks.map((check) => check(config, formatMessage, options))); + return results; +}; diff --git a/webapp/channels/src/components/admin_console/workspace-optimization/dashboard_checks/easy_management.ts b/webapp/channels/src/components/admin_console/workspace-optimization/dashboard_checks/easy_management.ts new file mode 100644 index 0000000000..1a959918ad --- /dev/null +++ b/webapp/channels/src/components/admin_console/workspace-optimization/dashboard_checks/easy_management.ts @@ -0,0 +1,129 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {AdminConfig} from '@mattermost/types/config'; +import {useIntl} from 'react-intl'; +import {ItemModel, ItemStatus, Options} from '../dashboard.type'; +import {ConsolePages, DocLinks} from 'utils/constants'; +import {impactModifiers} from '../dashboard.data'; + +// import {Client4} from 'mattermost-redux/client'; +// import {AnalyticsRow} from '@mattermost/types/admin'; +import {ldapTest} from 'actions/admin_actions'; + +const usesLDAP = async ( + config: Partial, + formatMessage: ReturnType['formatMessage'], + options: Options, +): Promise => { + const testLdap = async ( + config: Partial, + options: Options, + ): Promise => { + let check = ItemStatus.INFO; + + if (!options.isLicensed || !config.LdapSettings?.Enable) { + return check; + } + + const onSuccess = ({status}: any) => { + if (status === 'OK') { + check = ItemStatus.OK; + } + }; + + await ldapTest(onSuccess); + + return check; + }; + + // something feels flawed in this check. + const status = options.analytics?.TOTAL_USERS as number > 100 ? await testLdap(config, options) : ItemStatus.OK; + + return { + id: 'ad-ldap', + title: formatMessage({ + id: 'admin.reporting.workspace_optimization.ease_of_management.ldap.title', + defaultMessage: 'AD/LDAP integration recommended', + }), + description: formatMessage({ + id: 'admin.reporting.workspace_optimization.ease_of_management.ldap.description', + defaultMessage: 'You\'ve reached over 100 users! We recommend setting up AD/LDAP user authentication for easier onboarding as well as automated deactivations and role assignments.', + }), + ...(options.isLicensed && !options.isStarterLicense ? { + configUrl: ConsolePages.AD_LDAP, + configText: formatMessage({id: 'admin.reporting.workspace_optimization.ease_of_management.ldap.cta', defaultMessage: 'Try AD/LDAP'}), + } : options.trialOrEnterpriseCtaConfig), + infoUrl: DocLinks.AD_LDAP, + infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}), + telemetryAction: 'ad-ldap', + status, + scoreImpact: 22, + impactModifier: impactModifiers[status], + }; +}; + +// // commented out for now. +// // @see discussion here: https://github.com/mattermost/mattermost-webapp/pull/9822#discussion_r806879385 +// const fetchGuestAccounts = async ( +// config: Partial, +// analytics: Record | undefined, +// ) => { +// if (config.TeamSettings?.EnableOpenServer && config.GuestAccountsSettings?.Enable) { +// let usersArray = await fetch(`${Client4.getBaseRoute()}/users/invalid_emails`).then((result) => result.json()); + +// // this setting is just a string with a list of domains, or an empty string +// if (config.GuestAccountsSettings?.RestrictCreationToDomains) { +// const domainList = config.GuestAccountsSettings?.RestrictCreationToDomains; +// usersArray = usersArray.filter(({email}: Record) => domainList.includes((email as string).split('@')[1])); +// } + +// // if guest accounts make up more than 5% of the user base show the info accordion +// if (analytics && usersArray.length > (analytics.totalUsers as number * 0.05)) { +// return ItemStatus.INFO; +// } +// } + +// return ItemStatus.OK; +// }; + +// const guestAccounts = async ( +// config: Partial, +// formatMessage: ReturnType['formatMessage'], +// options: Options, +// ): Promise => { +// const status = await fetchGuestAccounts(config, options.analytics); +// return { +// id: 'guest-accounts', +// title: formatMessage({ +// id: 'admin.reporting.workspace_optimization.ease_of_management.guests_accounts.title', +// defaultMessage: 'Guest Accounts recommended', +// }), +// description: formatMessage({ +// id: 'admin.reporting.workspace_optimization.ease_of_management.guests_accounts.description', +// defaultMessage: 'Several user accounts are using different domains than your Site URL. You can control user access to channels and teams with guest accounts. We recommend starting an Enterprise trial and enabling Guest Access.', +// }), +// ...options.trialOrEnterpriseCtaConfig, +// infoUrl: 'https://docs.mattermost.com/onboard/guest-accounts.html', +// infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}), +// telemetryAction: 'guest-accounts', +// status, +// scoreImpact: 6, +// impactModifier: impactModifiers[status], +// }; +// }; + +export const runEaseOfUseChecks = async ( + config: Partial, + formatMessage: ReturnType['formatMessage'], + options: Options, +): Promise => { + const checks = [ + usesLDAP, + + // guestAccounts, + ]; + + const results = await Promise.all(checks.map((check) => check(config, formatMessage, options))); + return results; +}; diff --git a/webapp/channels/src/components/admin_console/workspace-optimization/dashboard_checks/performance.ts b/webapp/channels/src/components/admin_console/workspace-optimization/dashboard_checks/performance.ts new file mode 100644 index 0000000000..50d7f6f662 --- /dev/null +++ b/webapp/channels/src/components/admin_console/workspace-optimization/dashboard_checks/performance.ts @@ -0,0 +1,72 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {AdminConfig} from '@mattermost/types/config'; +import {ItemModel, ItemStatus, Options} from '../dashboard.type'; +import {elasticsearchTest} from 'actions/admin_actions'; +import {useIntl} from 'react-intl'; +import {ConsolePages, DocLinks} from 'utils/constants'; +import {impactModifiers} from '../dashboard.data'; + +const search = async ( + config: Partial, + formatMessage: ReturnType['formatMessage'], + options: Options, +): Promise => { + const testElasticsearch = async ( + config: Partial, + options: Options, + ) => { + let check = ItemStatus.INFO; + + if (!options.isLicensed || !options.isEnterpriseLicense || !(config.ElasticsearchSettings?.EnableIndexing && config.ElasticsearchSettings?.EnableSearching)) { + return check; + } + + const onSuccess = ({status}: any) => { + if (status === 'OK') { + check = ItemStatus.OK; + } + }; + await elasticsearchTest(config, onSuccess); + return check; + }; + + const totalPosts = options.analytics?.TOTAL_POSTS as number; + const totalUsers = options.analytics?.TOTAL_USERS as number; + const status = totalPosts < 2_000_000 && totalUsers < 500 ? ItemStatus.OK : await testElasticsearch(config, options); + return { + id: 'search', + title: formatMessage({ + id: 'admin.reporting.workspace_optimization.performance.search.title', + defaultMessage: 'Search performance', + }), + description: formatMessage({ + id: 'admin.reporting.workspace_optimization.performance.search.description', + defaultMessage: 'Your server has reached over 500 users and 2 million posts which can result in slow search performance. We recommend enabling Elasticsearch for better performance.', + }), + ...(options.isLicensed && options.isEnterpriseLicense ? { + configUrl: ConsolePages.ELASTICSEARCH, + configText: formatMessage({id: 'admin.reporting.workspace_optimization.search.cta', defaultMessage: 'Try Elasticsearch'}), + } : options.trialOrEnterpriseCtaConfig), + infoUrl: DocLinks.ELASTICSEARCH, + infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}), + telemetryAction: 'search-optimization', + status, + scoreImpact: 20, + impactModifier: impactModifiers[status], + }; +}; + +export const runPerformanceChecks = async ( + config: Partial, + formatMessage: ReturnType['formatMessage'], + options: Options, +) => { + const checks = [ + search, + ]; + + const results = await Promise.all(checks.map((check) => check(config, formatMessage, options))); + return results; +}; diff --git a/webapp/channels/src/components/admin_console/workspace-optimization/dashboard_checks/updates.ts b/webapp/channels/src/components/admin_console/workspace-optimization/dashboard_checks/updates.ts new file mode 100644 index 0000000000..438e358f6d --- /dev/null +++ b/webapp/channels/src/components/admin_console/workspace-optimization/dashboard_checks/updates.ts @@ -0,0 +1,94 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Client4} from 'mattermost-redux/client'; +import {ItemStatus, Options} from '../dashboard.type'; +import {useIntl} from 'react-intl'; +import {AdminConfig} from '@mattermost/types/config'; +import {CloudLinks, DocLinks} from 'utils/constants'; +import {impactModifiers} from '../dashboard.data'; + +const testServerVersion = async ( + config: Partial, + formatMessage: ReturnType['formatMessage'], + options: Options, +) => { + const fetchVersion = async ( + installedVersion: string, + formatMessage: ReturnType['formatMessage'], + ) => { + const result = await fetch(`${Client4.getBaseRoute()}/latest_version`).then((result) => result.json()); + + if (result.tag_name) { + const sanitizedVersion = result.tag_name.startsWith('v') ? result.tag_name.slice(1) : result.tag_name; + const newVersionParts = sanitizedVersion.split('.'); + const installedVersionParts = installedVersion.split('.').slice(0, 3); + + // quick general check if a newer version is available + let type = ''; + let status: ItemStatus = ItemStatus.OK; + + if (newVersionParts.join('') > installedVersionParts.join('')) { + // get correct values to be inserted into the accordion item + switch (true) { + case newVersionParts[0] > installedVersionParts[0]: + type = formatMessage({ + id: 'admin.reporting.workspace_optimization.updates.server_version.update_type.major', + defaultMessage: 'Major', + }); + status = ItemStatus.ERROR; + break; + case newVersionParts[1] > installedVersionParts[1]: + type = formatMessage({ + id: 'admin.reporting.workspace_optimization.updates.server_version.update_type.minor', + defaultMessage: 'Minor', + }); + status = ItemStatus.WARNING; + break; + case newVersionParts[2] > installedVersionParts[2]: + type = formatMessage({ + id: 'admin.reporting.workspace_optimization.updates.server_version.update_type.patch', + defaultMessage: 'Patch', + }); + status = ItemStatus.INFO; + break; + } + } + + return {type, description: result.body, status}; + } + + return {type: '', description: '', status: ItemStatus.OK}; + }; + + const serverVersion = await fetchVersion(options.installedVersion, formatMessage); + return { + id: 'server_version', + title: formatMessage({ + id: 'admin.reporting.workspace_optimization.updates.server_version.status.title', + defaultMessage: '{type} version update available.', + }, {type: serverVersion.type}), + description: serverVersion.description, + configUrl: CloudLinks.DOWNLOAD_UPDATE, + configText: formatMessage({id: 'admin.reporting.workspace_optimization.updates.server_version.cta', defaultMessage: 'Download update'}), + infoUrl: DocLinks.UPGRADE_SERVER, + infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}), + telemetryAction: 'server-version', + status: serverVersion.status, + scoreImpact: 15, + impactModifier: impactModifiers[serverVersion.status], + }; +}; + +export const runUpdateChecks = async ( + config: Partial, + formatMessage: ReturnType['formatMessage'], + options: Options, +) => { + const checks = [ + testServerVersion, + ]; + + const results = await Promise.all(checks.map((check) => check(config, formatMessage, options))); + return results; +}; diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json index 804769ce3b..238b42172d 100644 --- a/webapp/channels/src/i18n/en.json +++ b/webapp/channels/src/i18n/en.json @@ -1944,6 +1944,7 @@ "admin.reload.reloadFail": "Reloading unsuccessful: {error}", "admin.reporting.workspace_optimization.access.description": "Web server configuration may be affecting access to your Mattermost workspace.", "admin.reporting.workspace_optimization.access.descriptionOk": "Access to your workspace seems available!", + "admin.reporting.workspace_optimization.access.site_url.cta": "Configure web server", "admin.reporting.workspace_optimization.access.site_url.description": "Your web server settings aren't passing a live URL test which means your workspace may not be accessible to users. We recommend updating your web server settings.", "admin.reporting.workspace_optimization.access.site_url.title": "Misconfigured web server", "admin.reporting.workspace_optimization.access.title": "Workspace access", @@ -1952,27 +1953,24 @@ "admin.reporting.workspace_optimization.chip_warnings": "Warnings: {count}", "admin.reporting.workspace_optimization.configuration.description": "You have configuration issues to resolve", "admin.reporting.workspace_optimization.configuration.descriptionOk": "You seem to have good configuration for SSL and Session Lengths!", + "admin.reporting.workspace_optimization.configuration.session_length.cta": "Configure session length", "admin.reporting.workspace_optimization.configuration.session_length.description": "Your session length is set to the default of 30 days. A longer session length provides convenience, and a shorter session provides tighter security. We recommend adjusting this based on your organization's security policies.", "admin.reporting.workspace_optimization.configuration.session_length.title": "Session lengths is set to default", "admin.reporting.workspace_optimization.configuration.ssl.description": "We strongly recommend securing your Mattermost workspace by configuring SSL in production environments.", "admin.reporting.workspace_optimization.configuration.ssl.title": "Configure SSL to make your server more secure", "admin.reporting.workspace_optimization.configuration.title": "Configuration", - "admin.reporting.workspace_optimization.cta.configureDataRetention": "Try data retention", - "admin.reporting.workspace_optimization.cta.configureElasticsearch": "Try Elasticsearch", - "admin.reporting.workspace_optimization.cta.configureLDAP": "Try AD/LDAP", - "admin.reporting.workspace_optimization.cta.configureSessionLength": "Configure session length", - "admin.reporting.workspace_optimization.cta.configureWebServer": "Configure web server", - "admin.reporting.workspace_optimization.cta.downloadUpdate": "Download update", "admin.reporting.workspace_optimization.cta.learnMore": "Learn more", "admin.reporting.workspace_optimization.cta.startTrial": "Start trial", "admin.reporting.workspace_optimization.cta.upgradeLicense": "Contact sales", "admin.reporting.workspace_optimization.data_privacy.description": "Get better insight and control over your data.", "admin.reporting.workspace_optimization.data_privacy.descriptionOk": "You seem to be using data retention and compliance features!", + "admin.reporting.workspace_optimization.data_privacy.retention.cta": "Try data retention", "admin.reporting.workspace_optimization.data_privacy.retention.description": "Organizations in highly regulated industries require more control and insight with their data. We recommend enabling Data Retention and Compliance features.", "admin.reporting.workspace_optimization.data_privacy.retention.title": "Become more data aware", "admin.reporting.workspace_optimization.data_privacy.title": "Data privacy", "admin.reporting.workspace_optimization.ease_of_management.description": "Make it easier to manage your Mattermost workspace.", "admin.reporting.workspace_optimization.ease_of_management.descriptionOk": "Your user authentication setup is appropriate based on your current usage!", + "admin.reporting.workspace_optimization.ease_of_management.ldap.cta": "Try AD/LDAP", "admin.reporting.workspace_optimization.ease_of_management.ldap.description": "You've reached over 100 users! We recommend setting up AD/LDAP user authentication for easier onboarding as well as automated deactivations and role assignments.", "admin.reporting.workspace_optimization.ease_of_management.ldap.title": "AD/LDAP integration recommended", "admin.reporting.workspace_optimization.ease_of_management.title": "Ease of management", @@ -1983,9 +1981,11 @@ "admin.reporting.workspace_optimization.performance.search.description": "Your server has reached over 500 users and 2 million posts which can result in slow search performance. We recommend enabling Elasticsearch for better performance.", "admin.reporting.workspace_optimization.performance.search.title": "Search performance", "admin.reporting.workspace_optimization.performance.title": "Performance", + "admin.reporting.workspace_optimization.search.cta": "Try Elasticsearch", "admin.reporting.workspace_optimization.title": "Workspace Optimization", "admin.reporting.workspace_optimization.updates.description": "An update is available.", "admin.reporting.workspace_optimization.updates.descriptionOk": "Your workspace is completely up to date!", + "admin.reporting.workspace_optimization.updates.server_version.cta": "Download update", "admin.reporting.workspace_optimization.updates.server_version.status.title": "{type} version update available.", "admin.reporting.workspace_optimization.updates.server_version.update_type.major": "Major", "admin.reporting.workspace_optimization.updates.server_version.update_type.minor": "Minor",