diff --git a/plugin.json b/plugin.json index 539a1a67c..31d020232 100644 --- a/plugin.json +++ b/plugin.json @@ -171,6 +171,12 @@ "value": "UserPrincipalName" } ] + },{ + "key": "enableRhs", + "display_name": "Enable RHS", + "type": "bool", + "help_text": "When true, users will be able to view RHS (right hand sidebar).", + "default": false },{ "key": "appManifestDownload", "display_name": "Download Manifest", diff --git a/server/api.go b/server/api.go index 1876bd1b9..7d648b9bd 100644 --- a/server/api.go +++ b/server/api.go @@ -95,7 +95,7 @@ func NewAPI(p *Plugin, store store.Store) *API { router.HandleFunc("/oauth-redirect", api.oauthRedirectHandler).Methods(http.MethodGet) router.HandleFunc("/connected-users", api.getConnectedUsers).Methods(http.MethodGet) router.HandleFunc("/connected-users/download", api.getConnectedUsersFile).Methods(http.MethodGet) - router.HandleFunc("/whitelist-user", api.handleAuthRequired(api.whitelistUser)).Methods(http.MethodGet) + router.HandleFunc("/config", api.handleAuthRequired(api.getConfig)).Methods(http.MethodGet) channelsRouter.HandleFunc("/link", api.handleAuthRequired(api.checkUserConnected(api.linkChannels))).Methods(http.MethodPost) channelsRouter.HandleFunc(fmt.Sprintf("/{%s}/unlink", PathParamChannelID), api.handleAuthRequired(api.unlinkChannels)).Methods(http.MethodDelete) @@ -892,17 +892,9 @@ func (a *API) getConnectedUsersFile(w http.ResponseWriter, r *http.Request) { } } -func (a *API) whitelistUser(w http.ResponseWriter, r *http.Request) { - userID := r.Header.Get(HeaderMattermostUserID) - presentInWhitelist, err := a.p.store.IsUserPresentInWhitelist(userID) - if err != nil { - a.p.API.LogError("Error in checking if a user is present in whitelist", "UserID", userID, "Error", err.Error()) - http.Error(w, "error in checking if a user is present in whitelist", http.StatusInternalServerError) - return - } - +func (a *API) getConfig(w http.ResponseWriter, _ *http.Request) { response := map[string]bool{ - "presentInWhitelist": presentInWhitelist, + "rhsEnabled": a.p.getConfiguration().EnableRHS, } w.Header().Set("Content-Type", "application/json") diff --git a/server/api_test.go b/server/api_test.go index e1d800698..140da408d 100644 --- a/server/api_test.go +++ b/server/api_test.go @@ -1813,64 +1813,36 @@ func TestUnlinkChannels(t *testing.T) { } } -func TestWhitelistUser(t *testing.T) { +func TestGetConfig(t *testing.T) { for _, test := range []struct { Name string - SetupPlugin func(*plugintest.API) - SetupStore func(*storemocks.Store) - SetupMetrics func(*metricsmocks.Metrics) + SetupPlugin func(*Plugin) ExpectedResult string ExpectedStatusCode int }{ { - Name: "WhitelistUser: unable to check user in the whitelist", - SetupPlugin: func(api *plugintest.API) { - api.On("LogError", "Error in checking if a user is present in whitelist", "UserID", testutils.GetUserID(), "Error", "unable to check user in the whitelist").Times(1) - }, - SetupStore: func(store *storemocks.Store) { - store.On("IsUserPresentInWhitelist", testutils.GetUserID()).Return(false, errors.New("unable to check user in the whitelist")).Times(1) - }, - SetupMetrics: func(mockmetrics *metricsmocks.Metrics) { - mockmetrics.On("IncrementHTTPErrors").Times(1) + Name: "RhsEnabled: rhs is enabled", + SetupPlugin: func(p *Plugin) { + p.configuration.EnableRHS = true }, - ExpectedResult: "error in checking if a user is present in whitelist\n", - ExpectedStatusCode: http.StatusInternalServerError, - }, - { - Name: "WhitelistUser: user is not present in whitelist", - SetupPlugin: func(api *plugintest.API) {}, - SetupStore: func(store *storemocks.Store) { - store.On("IsUserPresentInWhitelist", testutils.GetUserID()).Return(false, nil).Times(1) - }, - SetupMetrics: func(mockmetrics *metricsmocks.Metrics) {}, - ExpectedResult: `{"presentInWhitelist":false}`, + ExpectedResult: `{"rhsEnabled":true}`, ExpectedStatusCode: http.StatusOK, }, { - Name: "WhitelistUser: user present in whitelist", - SetupPlugin: func(api *plugintest.API) {}, - SetupStore: func(store *storemocks.Store) { - store.On("IsUserPresentInWhitelist", testutils.GetUserID()).Return(true, nil).Times(1) - }, - SetupMetrics: func(mockmetrics *metricsmocks.Metrics) {}, - ExpectedResult: `{"presentInWhitelist":true}`, + Name: "RhsEnabled: rhs is not enabled", + SetupPlugin: func(p *Plugin) {}, + ExpectedResult: `{"rhsEnabled":false}`, ExpectedStatusCode: http.StatusOK, }, } { t.Run(test.Name, func(t *testing.T) { assert := assert.New(t) plugin := newTestPlugin(t) - mockAPI := &plugintest.API{} - plugin.SetAPI(mockAPI) - defer mockAPI.AssertExpectations(t) - - test.SetupPlugin(mockAPI) - test.SetupStore(plugin.store.(*storemocks.Store)) - test.SetupMetrics(plugin.metricsService.(*metricsmocks.Metrics)) + test.SetupPlugin(plugin) w := httptest.NewRecorder() - r := httptest.NewRequest(http.MethodGet, "/whitelist-user", nil) + r := httptest.NewRequest(http.MethodGet, "/config", nil) r.Header.Add(HeaderMattermostUserID, testutils.GetUserID()) plugin.ServeHTTP(nil, w, r) diff --git a/server/configuration.go b/server/configuration.go index 702775c51..1fb391bc2 100644 --- a/server/configuration.go +++ b/server/configuration.go @@ -41,6 +41,7 @@ type configuration struct { SyntheticUserAuthService string `json:"syntheticUserAuthService"` SyntheticUserAuthData string `json:"syntheticUserAuthData"` AutomaticallyPromoteSyntheticUsers bool `json:"automaticallyPromoteSyntheticUsers"` + EnableRHS bool `json:"enableRhs"` } func (c *configuration) ProcessConfiguration() { diff --git a/webapp/src/App.tsx b/webapp/src/App.tsx index 4fbe1d38a..7c4a3fbda 100644 --- a/webapp/src/App.tsx +++ b/webapp/src/App.tsx @@ -1,30 +1,37 @@ import React, {useEffect} from 'react'; +import {Action, Store} from 'redux'; import {useDispatch} from 'react-redux'; -import usePluginApi from 'hooks/usePluginApi'; +import {GlobalState} from 'mattermost-redux/types/store'; + +import {RhsTitle} from 'components'; -//global styles import {pluginApiServiceConfigs} from 'constants/apiService.constant'; +import {defaultPage, defaultPerPage, pluginTitle, rhsButtonId} from 'constants/common.constants'; +import {iconUrl} from 'constants/illustrations.constants'; + +import {Rhs} from 'containers'; + import useApiRequestCompletionState from 'hooks/useApiRequestCompletionState'; +import usePluginApi from 'hooks/usePluginApi'; import {setConnected} from 'reducers/connectedState'; -import {defaultPage, defaultPerPage} from 'constants/common.constants'; import {setIsRhsLoading} from 'reducers/spinner'; +// global styles import 'styles/main.scss'; /** * This is the main App component for the plugin * @returns {JSX.Element} */ -const App = (): JSX.Element => { +const App = ({registry, store}:{registry: PluginRegistry, store: Store>>}): JSX.Element => { const dispatch = useDispatch(); const {makeApiRequestWithCompletionStatus, getApiState} = usePluginApi(); - useEffect(() => { const linkedChannelsParams: SearchLinkedChannelParams = {page: defaultPage, per_page: defaultPerPage}; - makeApiRequestWithCompletionStatus(pluginApiServiceConfigs.whitelistUser.apiServiceName); + makeApiRequestWithCompletionStatus(pluginApiServiceConfigs.getConfig.apiServiceName); makeApiRequestWithCompletionStatus(pluginApiServiceConfigs.needsConnect.apiServiceName); makeApiRequestWithCompletionStatus(pluginApiServiceConfigs.getLinkedChannels.apiServiceName, linkedChannelsParams); }, []); @@ -35,6 +42,8 @@ const App = (): JSX.Element => { dispatch(setIsRhsLoading(isLoading)); }, [isLoading]); + const {data: configData} = getApiState(pluginApiServiceConfigs.getConfig.apiServiceName); + useApiRequestCompletionState({ serviceName: pluginApiServiceConfigs.needsConnect.apiServiceName, handleSuccess: () => { @@ -43,6 +52,49 @@ const App = (): JSX.Element => { }, }); + useApiRequestCompletionState({ + serviceName: pluginApiServiceConfigs.getConfig.apiServiceName, + handleSuccess: () => { + const {rhsEnabled} = configData as ConfigResponse; + const rhsButtonData = localStorage.getItem(rhsButtonId); + + // Unregister registered components and remove data present in the local storage. + if (rhsButtonData) { + const data = JSON.parse(rhsButtonData); + registry.unregisterComponent(data.headerId); + registry.unregisterComponent(data.appBarId); + localStorage.removeItem(rhsButtonId); + } + + // Register the right hand sidebar component, channel header button and app bar if the rhs is enabled. + if (rhsEnabled) { + let appBarId; + const {_, toggleRHSPlugin} = registry.registerRightHandSidebarComponent(Rhs, ); + const headerId = registry.registerChannelHeaderButtonAction( + , () => store.dispatch(toggleRHSPlugin), null, pluginTitle); + + if (registry.registerAppBarComponent) { + appBarId = registry.registerAppBarComponent(iconUrl, () => store.dispatch(toggleRHSPlugin), pluginTitle); + } + + /** + * Store data in local storage to avoid extra registration of the above components. + * This was needed as on the load of the app, the plugin re-registers the above components, and multiple rhs buttons were becoming visible. + * We avoid this by keeping registered component id in local storage, unregistering on the load of the plugin (if registered previously), and registering them again. + */ + localStorage.setItem(rhsButtonId, JSON.stringify({ + headerId, + appBarId, + })); + } + }, + }); + return <>; }; diff --git a/webapp/src/components/LinkedChannelCard/LinkedChannelCard.component.tsx b/webapp/src/components/LinkedChannelCard/LinkedChannelCard.component.tsx index 5094c2661..1a2ae474c 100644 --- a/webapp/src/components/LinkedChannelCard/LinkedChannelCard.component.tsx +++ b/webapp/src/components/LinkedChannelCard/LinkedChannelCard.component.tsx @@ -7,21 +7,23 @@ import {LinkedChannelCardProps} from './LinkedChannelCard.types'; import './LinkedChannelCard.styles.scss'; export const LinkedChannelCard = ({msTeamsChannelName, msTeamsTeamName, mattermostChannelName, mattermostTeamName}: LinkedChannelCardProps) => ( -
-
- -
-
-
- {/* TODO: Update icon on basis of channel type */} - -
{mattermostChannelName}
-
{mattermostTeamName}
+
+
+
+
-
- -
{msTeamsChannelName}
-
{msTeamsTeamName}
+
+
+ {/* TODO: Update icon on basis of channel type */} + +
{mattermostChannelName}
+
{mattermostTeamName}
+
+
+ +
{msTeamsChannelName}
+
{msTeamsTeamName}
+
diff --git a/webapp/src/components/LinkedChannelCard/__snapshots__/LinkedChannelCard.test.tsx.snap b/webapp/src/components/LinkedChannelCard/__snapshots__/LinkedChannelCard.test.tsx.snap index c6d510982..adee45fc5 100644 --- a/webapp/src/components/LinkedChannelCard/__snapshots__/LinkedChannelCard.test.tsx.snap +++ b/webapp/src/components/LinkedChannelCard/__snapshots__/LinkedChannelCard.test.tsx.snap @@ -5,6 +5,186 @@ exports[`Linked Channel Card component Should render correctly 1`] = ` "asFragment": [Function], "baseElement":
+
+
+ +
+
+ + + + + +
+ mockMattermostChannelName +
+
+ mockMattermostTeamName +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
+ mockMSTeamsChannelName +
+
+ mockMSTeamsTeamName +
+
+
+
+
+
+ , + "container":
+
@@ -176,178 +356,6 @@ exports[`Linked Channel Card component Should render correctly 1`] = `
- , - "container":
-
- -
-
- - - - - -
- mockMattermostChannelName -
-
- mockMattermostTeamName -
-
-
- - - - - - - - - - - - - - - - - - - - - - - -
- mockMSTeamsChannelName -
-
- mockMSTeamsTeamName -
-
-
-
, "debug": [Function], "findAllByAltText": [Function], diff --git a/webapp/src/components/RhsTitle/RhsTitle.component.tsx b/webapp/src/components/RhsTitle/RhsTitle.component.tsx index 4901fb902..cfa6fffb8 100644 --- a/webapp/src/components/RhsTitle/RhsTitle.component.tsx +++ b/webapp/src/components/RhsTitle/RhsTitle.component.tsx @@ -4,12 +4,14 @@ import {iconUrl} from 'constants/illustrations.constants'; import {pluginTitle} from 'constants/common.constants'; export const RhsTitle = () => ( - - - {pluginTitle} - +
+ + + {pluginTitle} + +
); diff --git a/webapp/src/components/RhsTitle/__snapshots__/RhsTitle.test.tsx.snap b/webapp/src/components/RhsTitle/__snapshots__/RhsTitle.test.tsx.snap index 2776cf02e..8886b0151 100644 --- a/webapp/src/components/RhsTitle/__snapshots__/RhsTitle.test.tsx.snap +++ b/webapp/src/components/RhsTitle/__snapshots__/RhsTitle.test.tsx.snap @@ -5,6 +5,26 @@ exports[`Rhs Title component Should render correctly 1`] = ` "asFragment": [Function], "baseElement":
+
+ + + Microsoft Teams Sync + +
+
+ , + "container":
+
@@ -16,18 +36,6 @@ exports[`Rhs Title component Should render correctly 1`] = ` Microsoft Teams Sync
- , - "container":
- - - Microsoft Teams Sync -
, "debug": [Function], "findAllByAltText": [Function], diff --git a/webapp/src/components/Snackbar/Snackbar.component.tsx b/webapp/src/components/Snackbar/Snackbar.component.tsx index a81ebc381..f4cf4a25e 100644 --- a/webapp/src/components/Snackbar/Snackbar.component.tsx +++ b/webapp/src/components/Snackbar/Snackbar.component.tsx @@ -47,26 +47,28 @@ export const Snackbar = () => { }; return ( -
-
- -
{message}
-
- +
+ +
{message}
+
+ +
); }; diff --git a/webapp/src/components/Snackbar/Snackbar.test.tsx b/webapp/src/components/Snackbar/Snackbar.test.tsx index 98dcbb958..a04e3a545 100644 --- a/webapp/src/components/Snackbar/Snackbar.test.tsx +++ b/webapp/src/components/Snackbar/Snackbar.test.tsx @@ -19,7 +19,7 @@ describe('Snackbar component', () => { }); it('Should show correct type', () => { - expect(tree.container.firstChild).toHaveClass('bg-error'); + expect(tree.container.firstChild?.firstChild).toHaveClass('bg-error'); }); it('Should show correct message', () => { diff --git a/webapp/src/components/Snackbar/__snapshots__/Snackbar.test.tsx.snap b/webapp/src/components/Snackbar/__snapshots__/Snackbar.test.tsx.snap index db02da2cf..63e0a3737 100644 --- a/webapp/src/components/Snackbar/__snapshots__/Snackbar.test.tsx.snap +++ b/webapp/src/components/Snackbar/__snapshots__/Snackbar.test.tsx.snap @@ -5,6 +5,68 @@ exports[`Snackbar component Should render correctly 1`] = ` "asFragment": [Function], "baseElement":
+
+
+
+ + + + + +
+ mockMessage +
+
+ +
+
+
+ , + "container":
+
@@ -58,60 +120,6 @@ exports[`Snackbar component Should render correctly 1`] = `
- , - "container":
-
-
- - - - - -
- mockMessage -
-
- -
, "debug": [Function], "findAllByAltText": [Function], diff --git a/webapp/src/constants/apiService.constant.ts b/webapp/src/constants/apiService.constant.ts index e341b450f..38846d7e8 100644 --- a/webapp/src/constants/apiService.constant.ts +++ b/webapp/src/constants/apiService.constant.ts @@ -10,10 +10,10 @@ export const pluginApiServiceConfigs: Record = { success: 'success', diff --git a/webapp/src/containers/Rhs/Rhs.container.tsx b/webapp/src/containers/Rhs/Rhs.container.tsx index 1f577d31c..c5b77bf91 100644 --- a/webapp/src/containers/Rhs/Rhs.container.tsx +++ b/webapp/src/containers/Rhs/Rhs.container.tsx @@ -22,11 +22,8 @@ export const Rhs = () => { const {isOpen} = getSnackbarState(state); - const {data} = getApiState(pluginApiServiceConfigs.whitelistUser.apiServiceName); const {data: linkedChannels} = getApiState(pluginApiServiceConfigs.getLinkedChannels.apiServiceName); - const {presentInWhitelist} = data as WhitelistUserResponse; - // NOTE: Commented out on purpose.This is part of Phase-II // const isAnyChannelLinked = useMemo(() => Boolean((linkedChannels as ChannelLinkData[])?.length), [linkedChannels]); const isAnyChannelLinked = false; @@ -34,8 +31,10 @@ export const Rhs = () => { const getRhsView = useCallback(() => { if (isRhsLoading) { return ( -
- +
+
+ +
); } @@ -57,10 +56,7 @@ export const Rhs = () => { return ( <> - { - presentInWhitelist ? - getRhsView() : 'MS Teams Sync plugin' - } + {getRhsView()} {isOpen && } ); diff --git a/webapp/src/containers/Rhs/__snapshots__/Rhs.test.tsx.snap b/webapp/src/containers/Rhs/__snapshots__/Rhs.test.tsx.snap index 12b3103d8..cf9e7c2ae 100644 --- a/webapp/src/containers/Rhs/__snapshots__/Rhs.test.tsx.snap +++ b/webapp/src/containers/Rhs/__snapshots__/Rhs.test.tsx.snap @@ -11,93 +11,101 @@ exports[`RHS view Should render correctly 1`] = ` aria-hidden="true" >
- -
-
-
- Connected as - +
+
+
- John Doe - -
- + Connected as + + John Doe + + + +
+
-
- - - - - -
- mockMessage -
-
-
+ + + + + + +
, "debug": [Function], diff --git a/webapp/src/containers/Rhs/views/ConnectAccount/ConnectAccount.container.tsx b/webapp/src/containers/Rhs/views/ConnectAccount/ConnectAccount.container.tsx index 8c56be765..1d097bc76 100644 --- a/webapp/src/containers/Rhs/views/ConnectAccount/ConnectAccount.container.tsx +++ b/webapp/src/containers/Rhs/views/ConnectAccount/ConnectAccount.container.tsx @@ -25,31 +25,33 @@ export const ConnectAccount = () => { }); return ( -
-
-
- -

{Constants.connectAccountMsg}

+
+
+
+
+ +

{Constants.connectAccountMsg}

+
+ +
+
+
+
{Constants.listTitle}
+
    + {Constants.connectAccountFeatures.map(({icon, text}) => ( +
  • + +
    {text}
    +
  • + )) } +
- -
-
-
-
{Constants.listTitle}
-
    - {Constants.connectAccountFeatures.map(({icon, text}) => ( -
  • - -
    {text}
    -
  • - )) } -
); diff --git a/webapp/src/containers/Rhs/views/ConnectAccount/__snapshots__/ConnectAccount.test.tsx.snap b/webapp/src/containers/Rhs/views/ConnectAccount/__snapshots__/ConnectAccount.test.tsx.snap index d279a8b83..69d96fcac 100644 --- a/webapp/src/containers/Rhs/views/ConnectAccount/__snapshots__/ConnectAccount.test.tsx.snap +++ b/webapp/src/containers/Rhs/views/ConnectAccount/__snapshots__/ConnectAccount.test.tsx.snap @@ -5,6 +5,233 @@ exports[`Connect Account View Should render correctly 1`] = ` "asFragment": [Function], "baseElement":
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ Connect your Microsoft Teams account +

+
+ +
+
+
+
+ With your connected Microsoft Teams account, you will be able to: +
+
    +
  • + + + + + +
    + Send and receive direct and group messages with your colleagues on Microsoft Teams +
    +
  • +
  • + + + + + +
    + Send and receive messages in channels that are linked between the two platforms. +
    +
  • +
+
+
+
+
+ , + "container":
+
@@ -223,225 +450,6 @@ exports[`Connect Account View Should render correctly 1`] = `
- , - "container":
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- Connect your Microsoft Teams account -

-
- -
-
-
-
- With your connected Microsoft Teams account, you will be able to: -
-
    -
  • - - - - - -
    - Send and receive direct and group messages with your colleagues on Microsoft Teams -
    -
  • -
  • - - - - - -
    - Send and receive messages in channels that are linked between the two platforms. -
    -
  • -
-
-
, "debug": [Function], "findAllByAltText": [Function], diff --git a/webapp/src/containers/Rhs/views/ConnectedAccount/ConnectedAccount.container.tsx b/webapp/src/containers/Rhs/views/ConnectedAccount/ConnectedAccount.container.tsx index 99e9fb697..381fe3f18 100644 --- a/webapp/src/containers/Rhs/views/ConnectedAccount/ConnectedAccount.container.tsx +++ b/webapp/src/containers/Rhs/views/ConnectedAccount/ConnectedAccount.container.tsx @@ -57,47 +57,49 @@ export const ConnectedAccount = () => { }, [connected, isAlreadyConnected]); return ( -
-
- {/* TODO: Refactor user Avatar */} -
- +
+
+ {/* TODO: Refactor user Avatar */} +
+ > + +
+
+
{'Connected as '}{username}
+ +
-
-
{'Connected as '}{username}
- -
-
-
- {/* NOTE: Part of Phase-II */} - {/* +
+ {/* NOTE: Part of Phase-II */} + {/*

{'There are no linked channels yet'}

*/} +
+
-
); }; diff --git a/webapp/src/containers/Rhs/views/ConnectedAccount/__snapshots__/ConnectedAccount.test.tsx.snap b/webapp/src/containers/Rhs/views/ConnectedAccount/__snapshots__/ConnectedAccount.test.tsx.snap index 119814b5d..3412d0def 100644 --- a/webapp/src/containers/Rhs/views/ConnectedAccount/__snapshots__/ConnectedAccount.test.tsx.snap +++ b/webapp/src/containers/Rhs/views/ConnectedAccount/__snapshots__/ConnectedAccount.test.tsx.snap @@ -11,41 +11,45 @@ exports[`Connected Account View Should render correctly 1`] = ` aria-hidden="true" >
- -
-
-
- Connected as - +
+
+
- John Doe - -
- + Connected as + + John Doe + + + +
+
-