-
Notifications
You must be signed in to change notification settings - Fork 336
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: Mattermost plugin module federation (#10517)
- Loading branch information
1 parent
ecff092
commit 79fd8a2
Showing
55 changed files
with
3,567 additions
and
490 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
__generated__/* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import {Variables} from 'react-relay' | ||
import { | ||
Environment, | ||
Network, | ||
Observable, | ||
RecordSource, | ||
RelayFeatureFlags, | ||
RelayFieldLogger, | ||
RequestParameters | ||
} from 'relay-runtime' | ||
import RelayModernStore from 'relay-runtime/lib/store/RelayModernStore' | ||
|
||
import {AnyAction, Store} from '@reduxjs/toolkit' | ||
import {Client4} from 'mattermost-redux/client' | ||
import {GlobalState} from 'mattermost-redux/types/store' | ||
RelayFeatureFlags.ENABLE_RELAY_RESOLVERS = true | ||
|
||
type State = { | ||
authToken: string | null | ||
serverUrl: string | ||
store: Store<GlobalState, AnyAction> | ||
} | ||
|
||
const fetchFunction = (state: State) => (params: RequestParameters, variables: Variables) => { | ||
const {serverUrl, authToken} = state | ||
const response = fetch( | ||
serverUrl, | ||
Client4.getOptions({ | ||
method: 'POST', | ||
headers: { | ||
accept: 'application/json', | ||
'content-type': 'application/json', | ||
'x-application-authorization': authToken ? `Bearer ${authToken}` : '' | ||
}, | ||
body: JSON.stringify({ | ||
type: 'start', | ||
payload: { | ||
documentId: params.id, | ||
query: params.text, | ||
variables | ||
} | ||
}) | ||
}) | ||
) | ||
|
||
return Observable.from( | ||
response.then(async (data) => { | ||
const json = await data.json() | ||
return json.payload | ||
}) | ||
) | ||
} | ||
|
||
const relayFieldLogger: RelayFieldLogger = (event) => { | ||
if (event.kind === 'relay_resolver.error') { | ||
console.warn(`Resolver error encountered in ${event.owner}.${event.fieldPath}`) | ||
console.warn(event.error) | ||
} | ||
} | ||
|
||
export type ResolverContext = { | ||
serverUrl: string | ||
store: Store<GlobalState, AnyAction> | ||
} | ||
|
||
export class Atmosphere extends Environment { | ||
state: State | ||
|
||
constructor(serverUrl: string, reduxStore: Store<GlobalState, AnyAction>) { | ||
const state = { | ||
serverUrl: serverUrl + '/graphql', | ||
store: reduxStore, | ||
authToken: null | ||
} | ||
|
||
const network = Network.create(fetchFunction(state)) | ||
const relayStore = new RelayModernStore(new RecordSource(), { | ||
resolverContext: { | ||
store: reduxStore, | ||
serverUrl | ||
} | ||
}) | ||
super({ | ||
store: relayStore, | ||
network, | ||
relayFieldLogger | ||
}) | ||
this.state = state | ||
} | ||
} | ||
|
||
/** | ||
* Creates a new Relay environment instance for managing (fetching, storing) GraphQL data. | ||
*/ | ||
export function createEnvironment(serverUrl: string, reduxStore: Store<GlobalState, AnyAction>) { | ||
return new Atmosphere(serverUrl, reduxStore) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import {Client4} from 'mattermost-redux/client' | ||
import {ReactNode, useCallback, useEffect} from 'react' | ||
import {useSelector} from 'react-redux' | ||
import {RelayEnvironmentProvider} from 'react-relay' | ||
import {Atmosphere} from './Atmosphere' | ||
import {getPluginServerRoute} from './selectors' | ||
|
||
type Props = { | ||
environment: Atmosphere | ||
children: ReactNode | ||
} | ||
|
||
export default function AtmosphereProvider({environment, children}: Props) { | ||
const pluginServerRoute = useSelector(getPluginServerRoute) | ||
const serverUrl = `${pluginServerRoute}/login` | ||
const login = useCallback(async () => { | ||
const response = await fetch( | ||
serverUrl, | ||
Client4.getOptions({ | ||
method: 'POST', | ||
headers: { | ||
'Content-Type': 'application/json' | ||
} | ||
}) | ||
) | ||
const body = await response.json() | ||
environment.state.authToken = body.authToken | ||
}, [serverUrl]) | ||
|
||
useEffect(() => { | ||
if (!environment.state.authToken) { | ||
login() | ||
} | ||
}, [environment.state.authToken, login]) | ||
|
||
if (!environment.state.authToken) { | ||
return null | ||
} | ||
|
||
return <RelayEnvironmentProvider environment={environment}>{children}</RelayEnvironmentProvider> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
# `Mattermost Plugin` | ||
|
||
The Mattermost plugin itself lives in [ParabolInc/parabol-mattermost-plugin](https://github.com/ParabolInc/parabol-mattermost-plugin). | ||
It needs to be installed to the Mattermost server and configured to point to the Parabol instance. | ||
It then loads the code in this package via Webpacks module federation. | ||
|
116 changes: 116 additions & 0 deletions
116
packages/mattermost-plugin/components/LinkTeamModal/LinkTeamModal.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
import graphql from 'babel-plugin-relay/macro' | ||
import React, {useEffect} from 'react' | ||
import {Modal} from 'react-bootstrap' | ||
import {useDispatch, useSelector} from 'react-redux' | ||
|
||
import {useLazyLoadQuery} from 'react-relay' | ||
import {LinkTeamModalQuery} from '../../__generated__/LinkTeamModalQuery.graphql' | ||
import {useConfig} from '../../hooks/useConfig' | ||
import {useCurrentChannel} from '../../hooks/useCurrentChannel' | ||
import {useLinkTeam} from '../../hooks/useLinkTeam' | ||
import {closeLinkTeamModal} from '../../reducers' | ||
import {getAssetsUrl, isLinkTeamModalVisible} from '../../selectors' | ||
import Select from '../Select' | ||
|
||
const LinkTeamModal = () => { | ||
const isVisible = useSelector(isLinkTeamModalVisible) | ||
const channel = useCurrentChannel() | ||
const config = useConfig() | ||
const data = useLazyLoadQuery<LinkTeamModalQuery>( | ||
graphql` | ||
query LinkTeamModalQuery($channel: ID!) { | ||
viewer { | ||
linkedTeamIds(channel: $channel) | ||
teams { | ||
id | ||
name | ||
} | ||
} | ||
} | ||
`, | ||
{ | ||
channel: channel.id | ||
} | ||
) | ||
const viewer = data.viewer | ||
const unlinkedTeams = viewer.teams.filter((team) => !viewer.linkedTeamIds?.includes(team.id)) | ||
const linkTeam = useLinkTeam() | ||
|
||
const [selectedTeam, setSelectedTeam] = React.useState<(typeof data.viewer.teams)[number]>() | ||
|
||
useEffect(() => { | ||
if (!selectedTeam && unlinkedTeams && unlinkedTeams.length > 0) { | ||
setSelectedTeam(unlinkedTeams[0]) | ||
} | ||
}, [unlinkedTeams, selectedTeam]) | ||
|
||
const dispatch = useDispatch() | ||
|
||
const handleClose = () => { | ||
dispatch(closeLinkTeamModal()) | ||
} | ||
|
||
const handleLink = async () => { | ||
if (!selectedTeam) { | ||
return | ||
} | ||
await linkTeam(selectedTeam.id) | ||
handleClose() | ||
} | ||
|
||
const assetsPath = useSelector(getAssetsUrl) | ||
|
||
if (!isVisible) { | ||
return null | ||
} | ||
|
||
return ( | ||
<Modal | ||
dialogClassName='modal--scroll' | ||
show={true} | ||
onHide={handleClose} | ||
onExited={handleClose} | ||
bsSize='large' | ||
backdrop='static' | ||
> | ||
<Modal.Header closeButton={true}> | ||
<Modal.Title> | ||
<img width={36} height={36} src={`${assetsPath}/parabol.png`} /> | ||
{` Link a Parabol Team to ${channel.name}`} | ||
</Modal.Title> | ||
</Modal.Header> | ||
<Modal.Body> | ||
{unlinkedTeams && unlinkedTeams.length > 0 ? ( | ||
<> | ||
<Select | ||
label='Choose Parabol Team' | ||
required={true} | ||
value={selectedTeam} | ||
options={unlinkedTeams} | ||
onChange={setSelectedTeam} | ||
/> | ||
</> | ||
) : ( | ||
<> | ||
<div> | ||
<p> | ||
All your teams are already linked to this channel. Visit{' '} | ||
<a href={`${config?.parabolUrl}/newteam/`}>Parabol</a> to create new teams. | ||
</p> | ||
</div> | ||
</> | ||
)} | ||
</Modal.Body> | ||
<Modal.Footer> | ||
<button className='btn btn-tertiary cancel-button' onClick={handleClose}> | ||
Cancel | ||
</button> | ||
<button className='btn btn-primary save-button' onClick={handleLink}> | ||
Link Team | ||
</button> | ||
</Modal.Footer> | ||
</Modal> | ||
) | ||
} | ||
|
||
export default LinkTeamModal |
21 changes: 21 additions & 0 deletions
21
packages/mattermost-plugin/components/LinkTeamModal/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import {lazy, Suspense} from 'react' | ||
import {useSelector} from 'react-redux' | ||
|
||
import {isLinkTeamModalVisible} from '../../selectors' | ||
|
||
const LinkTeamModal = lazy(() => import(/* webpackChunkName: 'LinkTeamModal' */ './LinkTeamModal')) | ||
|
||
const LinkTeamModalRoot = () => { | ||
const isVisible = useSelector(isLinkTeamModalVisible) | ||
if (!isVisible) { | ||
return null | ||
} | ||
|
||
return ( | ||
<Suspense fallback={null}> | ||
<LinkTeamModal /> | ||
</Suspense> | ||
) | ||
} | ||
|
||
export default LinkTeamModalRoot |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
//import classNames from 'classnames' | ||
import React from 'react' | ||
|
||
type Props = { | ||
text?: React.ReactNode | ||
style?: React.CSSProperties | ||
} | ||
const LoadingSpinner = ({text, style}: Props) => { | ||
return ( | ||
<span | ||
id='loadingSpinner' | ||
//className={classNames('LoadingSpinner', {'with-text': Boolean(text)})} | ||
style={style} | ||
data-testid='loadingSpinner' | ||
> | ||
<span className='fa fa-spinner fa-fw fa-pulse spinner' /> | ||
{text} | ||
</span> | ||
) | ||
} | ||
|
||
export default LoadingSpinner |
Oops, something went wrong.