From 81bf9cb36c694d1222350d28981b1bcda5487f06 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 20 Sep 2024 15:40:15 +0100 Subject: [PATCH] feat: rework user permissions in the ui to use a react context --- .../src/client/lib/KeyboardFocusIndicator.tsx | 10 +- packages/webui/src/client/lib/localStorage.ts | 10 +- packages/webui/src/client/lib/logStatus.ts | 3 +- packages/webui/src/client/ui/App.tsx | 230 +++--- packages/webui/src/client/ui/RundownList.tsx | 12 +- .../client/ui/RundownList/RundownListItem.tsx | 17 +- .../ui/RundownList/RundownListItemView.tsx | 6 +- .../ui/RundownList/RundownPlaylistUi.tsx | 8 +- .../webui/src/client/ui/RundownList/util.ts | 5 +- packages/webui/src/client/ui/RundownView.tsx | 114 +-- .../client/ui/RundownView/RundownNotifier.tsx | 51 +- .../src/client/ui/Status/ExternalMessages.tsx | 8 +- .../src/client/ui/Status/MediaManager.tsx | 8 +- .../src/client/ui/Status/SystemStatus.tsx | 762 +++++++++--------- .../webui/src/client/ui/UserPermissions.tsx | 74 ++ 15 files changed, 678 insertions(+), 640 deletions(-) create mode 100644 packages/webui/src/client/ui/UserPermissions.tsx diff --git a/packages/webui/src/client/lib/KeyboardFocusIndicator.tsx b/packages/webui/src/client/lib/KeyboardFocusIndicator.tsx index c23f7b2bb4..d254dc49da 100644 --- a/packages/webui/src/client/lib/KeyboardFocusIndicator.tsx +++ b/packages/webui/src/client/lib/KeyboardFocusIndicator.tsx @@ -1,17 +1,17 @@ import { Meteor } from 'meteor/meteor' import * as React from 'react' -import { getAllowStudio, getAllowConfigure, getAllowService } from '../lib/localStorage' - import { MeteorCall } from '../lib/meteorApi' import { getCurrentTime } from './systemTime' import { catchError } from './lib' +import { UserPermissions } from '../ui/UserPermissions' interface IKeyboardFocusIndicatorState { inFocus: boolean } interface IKeyboardFocusIndicatorProps { showWhenFocused?: boolean + userPermissions: Readonly } export class KeyboardFocusIndicator extends React.Component< @@ -54,9 +54,9 @@ export class KeyboardFocusIndicator extends React.Component< url: window.location.href + window.location.search, width: window.innerWidth, height: window.innerHeight, - studio: getAllowStudio(), - configure: getAllowConfigure(), - service: getAllowService(), + studio: this.props.userPermissions.studio, + configure: this.props.userPermissions.configure, + service: this.props.userPermissions.service, } if (focusNow) { MeteorCall.userAction diff --git a/packages/webui/src/client/lib/localStorage.ts b/packages/webui/src/client/lib/localStorage.ts index 9a9e45f093..ae0982433b 100644 --- a/packages/webui/src/client/lib/localStorage.ts +++ b/packages/webui/src/client/lib/localStorage.ts @@ -47,35 +47,35 @@ function localStorageUnsetCachedItem(key: LocalStorageProperty): void { export function setAllowStudio(studioMode: boolean): void { localStorageSetCachedItem(LocalStorageProperty.STUDIO, studioMode ? '1' : '0') } -export function getAllowStudio(): boolean { +export function getLocalAllowStudio(): boolean { return localStorageGetCachedItem(LocalStorageProperty.STUDIO) === '1' } export function setAllowConfigure(configureMode: boolean): void { localStorageSetCachedItem(LocalStorageProperty.CONFIGURE, configureMode ? '1' : '0') } -export function getAllowConfigure(): boolean { +export function getLocalAllowConfigure(): boolean { return localStorageGetCachedItem(LocalStorageProperty.CONFIGURE) === '1' } export function setAllowService(serviceMode: boolean): void { localStorageSetCachedItem(LocalStorageProperty.SERVICE, serviceMode ? '1' : '0') } -export function getAllowService(): boolean { +export function getLocalAllowService(): boolean { return localStorageGetCachedItem(LocalStorageProperty.SERVICE) === '1' } export function setAllowDeveloper(developerMode: boolean): void { localStorageSetCachedItem(LocalStorageProperty.DEVELOPER, developerMode ? '1' : '0') } -export function getAllowDeveloper(): boolean { +export function getLocalAllowDeveloper(): boolean { return localStorageGetCachedItem(LocalStorageProperty.DEVELOPER) === '1' } export function setAllowTesting(testingMode: boolean): void { localStorageSetCachedItem(LocalStorageProperty.TESTING, testingMode ? '1' : '0') } -export function getAllowTesting(): boolean { +export function getLocalAllowTesting(): boolean { return localStorageGetCachedItem(LocalStorageProperty.TESTING) === '1' } diff --git a/packages/webui/src/client/lib/logStatus.ts b/packages/webui/src/client/lib/logStatus.ts index 3420441c1b..a19dd50e21 100644 --- a/packages/webui/src/client/lib/logStatus.ts +++ b/packages/webui/src/client/lib/logStatus.ts @@ -1,7 +1,6 @@ import { Meteor } from 'meteor/meteor' import { Tracker } from 'meteor/tracker' import { getRandomString } from './tempLib' -import { getAllowStudio } from './localStorage' import { logger } from './logging' /* @@ -12,7 +11,7 @@ import { logger } from './logging' const browserSessionId = getRandomString(8) // Only log status for studio users -const logStatusEnable = getAllowStudio() +const logStatusEnable = true // getAllowStudio() HACK: this needs to be setup to be reactive if this is wanted to work correctly const previouslyLogged: { connected?: boolean diff --git a/packages/webui/src/client/ui/App.tsx b/packages/webui/src/client/ui/App.tsx index 7ca92199c9..77c4d7afa7 100644 --- a/packages/webui/src/client/ui/App.tsx +++ b/packages/webui/src/client/ui/App.tsx @@ -4,18 +4,8 @@ import 'moment/min/locales' import { parse as queryStringParse } from 'query-string' import Header from './Header' import { - setAllowStudio, - setAllowConfigure, - getAllowStudio, - getAllowConfigure, - setAllowDeveloper, - setAllowTesting, - getAllowTesting, - getAllowDeveloper, setAllowSpeaking, setAllowVibrating, - setAllowService, - getAllowService, setHelpMode, setUIZoom, getUIZoom, @@ -48,6 +38,7 @@ import { Settings } from '../lib/Settings' import { DocumentTitleProvider } from '../lib/DocumentTitleProvider' import { catchError, firstIfArray, isRunningInPWA } from '../lib/lib' import { protectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' +import { useUserPermissions, UserPermissionsContext } from './UserPermissions' const NullComponent = () => null @@ -61,7 +52,7 @@ export const App: React.FC = function App() { const [lastStart] = useState(Date.now()) - const roles = useRoles() + const roles = useUserPermissions() const featureFlags = useFeatureFlags() useEffect(() => { @@ -155,138 +146,97 @@ export const App: React.FC = function App() { }, []) return ( - -
- {/* Header switch - render the usual header for all pages but the rundown view */} - - - - - - - ( -
- )} - /> - - - {/* Main app switch */} - - - - } /> - ( - - )} - /> - ( - - )} - /> - ( - - )} - /> - ( - - )} - /> - {/* We switch to the general ClockView component, and allow it to do the switch between various types of countdowns */} - ( - - )} - /> - } /> - } /> - } /> - - - - - - - - {/* Put views that should NOT have the Notification center here: */} - - - - - - - - - - - -
-
+ + +
+ {/* Header switch - render the usual header for all pages but the rundown view */} + + + + + + + ( +
+ )} + /> + + + {/* Main app switch */} + + + + } /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + {/* We switch to the general ClockView component, and allow it to do the switch between various types of countdowns */} + ( + + )} + /> + } /> + } /> + } /> + + + + + + + + {/* Put views that should NOT have the Notification center here: */} + + + + + + + + + + + +
+
+
) } -function useRoles() { - const location = window.location - - const [roles, setRoles] = useState({ - studio: getAllowStudio(), - configure: getAllowConfigure(), - developer: getAllowDeveloper(), - testing: getAllowTesting(), - service: getAllowService(), - }) - - useEffect(() => { - if (!location.search) return - - const params = queryStringParse(location.search) - - if (params['studio']) setAllowStudio(params['studio'] === '1') - if (params['configure']) setAllowConfigure(params['configure'] === '1') - if (params['develop']) setAllowDeveloper(params['develop'] === '1') - if (params['testing']) setAllowTesting(params['testing'] === '1') - if (params['service']) setAllowService(params['service'] === '1') - - if (params['admin']) { - const val = params['admin'] === '1' - setAllowStudio(val) - setAllowConfigure(val) - setAllowDeveloper(val) - setAllowTesting(val) - setAllowService(val) - } - - setRoles({ - studio: getAllowStudio(), - configure: getAllowConfigure(), - developer: getAllowDeveloper(), - testing: getAllowTesting(), - service: getAllowService(), - }) - }, [location.search]) - - return roles -} - function useFeatureFlags() { const location = window.location diff --git a/packages/webui/src/client/ui/RundownList.tsx b/packages/webui/src/client/ui/RundownList.tsx index e14d16ddd4..3aa949d50d 100644 --- a/packages/webui/src/client/ui/RundownList.tsx +++ b/packages/webui/src/client/ui/RundownList.tsx @@ -2,7 +2,7 @@ import Tooltip from 'rc-tooltip' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' import { GENESIS_SYSTEM_VERSION } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { getAllowConfigure, getAllowStudio, getHelpMode } from '../lib/localStorage' +import { getHelpMode } from '../lib/localStorage' import { literal, unprotectString } from '../lib/tempLib' import { useSubscription, useTracker } from '../lib/ReactMeteorData/react-meteor-data' import { Spinner } from '../lib/Spinner' @@ -20,6 +20,8 @@ import { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' import { CreateAdlibTestingRundownPanel } from './RundownList/CreateAdlibTestingRundownPanel' +import { UserPermissionsContext } from './UserPermissions' +import React from 'react' export enum ToolTipStep { TOOLTIP_START_HERE = 'TOOLTIP_START_HERE', @@ -30,6 +32,8 @@ export enum ToolTipStep { export function RundownList(): JSX.Element { const { t } = useTranslation() + const userPermissions = React.useContext(UserPermissionsContext) + const playlistIds = useTracker( () => RundownPlaylists.find(undefined, { @@ -113,11 +117,11 @@ export function RundownList(): JSX.Element { } if (coreSystem?.version === GENESIS_SYSTEM_VERSION && gotPlaylists === true) { - return getAllowConfigure() ? ToolTipStep.TOOLTIP_RUN_MIGRATIONS : ToolTipStep.TOOLTIP_START_HERE + return userPermissions.configure ? ToolTipStep.TOOLTIP_RUN_MIGRATIONS : ToolTipStep.TOOLTIP_START_HERE } else { return ToolTipStep.TOOLTIP_EXTRAS } - }, [coreSystem, rundownPlaylists]) + }, [coreSystem, rundownPlaylists, userPermissions]) const showGettingStarted = coreSystem?.version === GENESIS_SYSTEM_VERSION && rundownPlaylists.length === 0 @@ -185,7 +189,7 @@ export function RundownList(): JSX.Element { )} - {getAllowStudio() && } + {userPermissions.studio && } diff --git a/packages/webui/src/client/ui/RundownList/RundownListItem.tsx b/packages/webui/src/client/ui/RundownList/RundownListItem.tsx index 2aa6a73883..f62fd844a2 100644 --- a/packages/webui/src/client/ui/RundownList/RundownListItem.tsx +++ b/packages/webui/src/client/ui/RundownList/RundownListItem.tsx @@ -1,7 +1,6 @@ -import { useEffect } from 'react' +import { useContext, useEffect } from 'react' import classNames from 'classnames' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { getAllowConfigure, getAllowService, getAllowStudio } from '../../lib/localStorage' import { useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import { confirmDeleteRundown, confirmReSyncRundown, getShowStyleBaseLink } from './util' import { useDrag, useDrop } from 'react-dnd' @@ -15,6 +14,7 @@ import { ShowStyleBases, ShowStyleVariants } from '../../collections' import { useTranslation } from 'react-i18next' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' +import { UserPermissionsContext } from '../UserPermissions' export const HTML_ID_PREFIX = 'rundown-' @@ -36,6 +36,7 @@ export function RundownListItem({ action?: IRundownPlaylistUiAction }>): JSX.Element | null { const { t } = useTranslation() + const userPermissions = useContext(UserPermissionsContext) const showStyleBase = useTracker( () => @@ -52,8 +53,6 @@ export function RundownListItem({ [rundown.showStyleVariantId] ) - const userCanConfigure = getAllowConfigure() - const [dragState, connectDragSource, connectDragPreview] = useDrag( { type: RundownListDragDropTypes.RUNDOWN, @@ -118,17 +117,17 @@ export function RundownListItem({ isOnlyRundownInPlaylist={isOnlyRundownInPlaylist} rundownLayouts={rundownLayouts} showStyleName={showStyleLabel} - showStyleBaseURL={userCanConfigure ? getShowStyleBaseLink(rundown.showStyleBaseId) : undefined} + showStyleBaseURL={userPermissions.configure ? getShowStyleBaseLink(rundown.showStyleBaseId) : undefined} confirmDeleteRundownHandler={ - (getAllowStudio() && + (userPermissions.studio && (rundown.orphaned || rundown.source.type === 'testing' || rundown.source.type === 'snapshot')) || - userCanConfigure || - getAllowService() + userPermissions.configure || + userPermissions.service ? () => confirmDeleteRundown(rundown, t) : undefined } confirmReSyncRundownHandler={ - rundown.orphaned && getAllowStudio() ? () => confirmReSyncRundown(rundown, t) : undefined + rundown.orphaned && userPermissions.studio ? () => confirmReSyncRundown(userPermissions, rundown, t) : undefined } /> ) diff --git a/packages/webui/src/client/ui/RundownList/RundownListItemView.tsx b/packages/webui/src/client/ui/RundownList/RundownListItemView.tsx index 5d0cfc4a6b..a5e69f269b 100644 --- a/packages/webui/src/client/ui/RundownList/RundownListItemView.tsx +++ b/packages/webui/src/client/ui/RundownList/RundownListItemView.tsx @@ -3,7 +3,6 @@ import React from 'react' import { useTranslation } from 'react-i18next' import { Link } from 'react-router-dom' import { Rundown, getRundownNrcsName } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { getAllowStudio } from '../../lib/localStorage' import { RundownUtils } from '../../lib/rundown' import { iconDragHandle, iconRemove, iconResync } from './icons' import { DisplayFormattedTime } from './DisplayFormattedTime' @@ -17,6 +16,7 @@ import { TOOLTIP_DEFAULT_DELAY } from '../../lib/lib' import { Meteor } from 'meteor/meteor' import { RundownPlaylists } from '../../collections' import { isLoopDefined } from '../../lib/RundownResolver' +import { UserPermissionsContext } from '../UserPermissions' interface IRundownListItemViewProps { isActive: boolean @@ -55,6 +55,8 @@ export default React.memo(function RundownListItemView({ }: IRundownListItemViewProps): JSX.Element | null { const { t } = useTranslation() + const userPermissions = React.useContext(UserPermissionsContext) + if (!rundown.playlistId) throw new Meteor.Error(500, 'Rundown is not a part of a rundown playlist!') const playlist = RundownPlaylists.findOne(rundown.playlistId) if (!playlist) throw new Meteor.Error(404, `Rundown Playlist "${rundown.playlistId}" not found!`) @@ -81,7 +83,7 @@ export default React.memo(function RundownListItemView({ > <> - {getAllowStudio() ? ( + {userPermissions.studio ? ( rundown._id)) + const userPermissions = useContext(UserPermissionsContext) + useEffect(() => { setRundownOrder(playlist.rundowns.map((rundown) => rundown._id)) }, [playlist.rundowns.map((rundown) => rundown._id).join(',')]) @@ -207,7 +209,7 @@ export function RundownPlaylistUi({ - {getAllowStudio() ? ( + {userPermissions.studio ? ( , rundown: Rundown, t: TFunction): void { doModalDialog({ title: t('Re-Sync rundown?'), yes: t('Re-Sync'), @@ -83,7 +84,7 @@ export function confirmReSyncRundown(rundown: Rundown, t: TFunction): void { async (e, ts) => MeteorCall.userAction.resyncRundown(e, ts, rundown._id), (err, res) => { if (!err && res) { - return handleRundownReloadResponse(t, rundown._id, res) + return handleRundownReloadResponse(t, userPermissions, rundown._id, res) } } ) diff --git a/packages/webui/src/client/ui/RundownView.tsx b/packages/webui/src/client/ui/RundownView.tsx index 40e0e68996..541904b381 100644 --- a/packages/webui/src/client/ui/RundownView.tsx +++ b/packages/webui/src/client/ui/RundownView.tsx @@ -45,7 +45,7 @@ import { getCurrentTime } from '../lib/systemTime' import { RundownUtils } from '../lib/rundown' import { ErrorBoundary } from '../lib/ErrorBoundary' import { ModalDialog, doModalDialog, isModalShowing } from '../lib/ModalDialog' -import { getAllowStudio, getAllowDeveloper, getHelpMode } from '../lib/localStorage' +import { getHelpMode } from '../lib/localStorage' import { ClientAPI } from '@sofie-automation/meteor-lib/dist/api/client' import { scrollToPosition, @@ -165,6 +165,7 @@ import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' import { isEntirePlaylistLooping, isLoopRunning } from '../lib/RundownResolver' import { useRundownAndShowStyleIdsForPlaylist } from './util/useRundownAndShowStyleIdsForPlaylist' import { RundownPlaylistClientUtil } from '../lib/rundownPlaylistUtil' +import { UserPermissionsContext, UserPermissions } from './UserPermissions' import { MAGIC_TIME_SCALE_FACTOR } from './SegmentTimeline/Constants' @@ -354,9 +355,9 @@ interface IRundownHeaderProps { rundownIds: RundownId[] firstRundown: Rundown | undefined onActivate?: (isRehearsal: boolean) => void - studioMode: boolean inActiveRundownView?: boolean layout: RundownLayoutRundownHeader | undefined + userPermissions: Readonly } interface IRundownHeaderState { @@ -448,7 +449,7 @@ const RundownHeader = withTranslation()( disableNextPiece = (e: any) => { const { t } = this.props - if (this.props.studioMode) { + if (this.props.userPermissions.studio) { doUserAction( t, e, @@ -462,7 +463,7 @@ const RundownHeader = withTranslation()( disableNextPieceUndo = (e: any) => { const { t } = this.props - if (this.props.studioMode) { + if (this.props.userPermissions.studio) { doUserAction( t, e, @@ -475,7 +476,7 @@ const RundownHeader = withTranslation()( take = (e: any) => { const { t } = this.props - if (this.props.studioMode) { + if (this.props.userPermissions.studio) { if (!this.props.playlist.activationId) { const onSuccess = () => { if (typeof this.props.onActivate === 'function') this.props.onActivate(false) @@ -546,7 +547,7 @@ const RundownHeader = withTranslation()( hold = (e: any) => { const { t } = this.props - if (this.props.studioMode && this.props.playlist.activationId) { + if (this.props.userPermissions.studio && this.props.playlist.activationId) { doUserAction(t, e, UserAction.ACTIVATE_HOLD, (e, ts) => MeteorCall.userAction.activateHold(e, ts, this.props.playlist._id, false) ) @@ -556,7 +557,7 @@ const RundownHeader = withTranslation()( holdUndo = (e: any) => { const { t } = this.props if ( - this.props.studioMode && + this.props.userPermissions.studio && this.props.playlist.activationId && this.props.playlist.holdState === RundownHoldState.PENDING ) { @@ -653,7 +654,7 @@ const RundownHeader = withTranslation()( if (e.persist) e.persist() if ( - this.props.studioMode && + this.props.userPermissions.studio && (!this.props.playlist.activationId || (this.props.playlist.activationId && this.props.playlist.rehearsal)) ) { const onSuccess = () => { @@ -727,7 +728,7 @@ const RundownHeader = withTranslation()( if (e.persist) e.persist() if ( - this.props.studioMode && + this.props.userPermissions.studio && (!this.props.playlist.activationId || (this.props.playlist.activationId && !this.props.playlist.rehearsal)) ) { const onSuccess = () => { @@ -807,7 +808,7 @@ const RundownHeader = withTranslation()( const { t } = this.props if (e.persist) e.persist() - if (this.props.studioMode && this.props.playlist.activationId) { + if (this.props.userPermissions.studio && this.props.playlist.activationId) { if (this.rundownShouldHaveStarted()) { if (this.props.playlist.rehearsal) { // We're in rehearsal mode @@ -839,7 +840,7 @@ const RundownHeader = withTranslation()( if (e.persist) e.persist() if ( - this.props.studioMode && + this.props.userPermissions.studio && this.props.studio.settings.allowAdlibTestingSegment && this.props.playlist.activationId && this.props.currentRundown @@ -889,7 +890,7 @@ const RundownHeader = withTranslation()( reloadRundownPlaylist = (e: any) => { const { t } = this.props - if (this.props.studioMode) { + if (this.props.userPermissions.studio) { doUserAction( t, e, @@ -897,7 +898,7 @@ const RundownHeader = withTranslation()( (e, ts) => MeteorCall.userAction.resyncRundownPlaylist(e, ts, this.props.playlist._id), (err, reloadResponse) => { if (!err && reloadResponse) { - if (!handleRundownPlaylistReloadResponse(t, reloadResponse)) { + if (!handleRundownPlaylistReloadResponse(t, this.props.userPermissions, reloadResponse)) { if (this.props.playlist && this.props.playlist.nextPartInfo) { scrollToPartInstance(this.props.playlist.nextPartInfo.partInstanceId).catch((error) => { if (!error.toString().match(/another scroll/)) console.warn(error) @@ -912,7 +913,7 @@ const RundownHeader = withTranslation()( takeRundownSnapshot = (e: any) => { const { t } = this.props - if (this.props.studioMode) { + if (this.props.userPermissions.studio) { const doneMessage = t('A snapshot of the current Running\xa0Order has been created for troubleshooting.') doUserAction( t, @@ -955,7 +956,7 @@ const RundownHeader = withTranslation()( resetAndActivateRundown = (e: any) => { // Called from the ModalDialog, 1 minute before broadcast starts - if (this.props.studioMode) { + if (this.props.userPermissions.studio) { const { t } = this.props this.rewindSegments() // Do a rewind right away @@ -1007,7 +1008,7 @@ const RundownHeader = withTranslation()(
{this.props.playlist && this.props.playlist.name}
- {this.props.studioMode ? ( + {this.props.userPermissions.studio ? ( {!(this.props.playlist.activationId && this.props.playlist.rehearsal) ? ( !this.rundownShouldHaveStarted() && !this.props.playlist.activationId ? ( @@ -1071,7 +1072,7 @@ const RundownHeader = withTranslation()( holdToDisplay={contextMenuHoldToDisplayTime()} >
@@ -1098,7 +1099,7 @@ const RundownHeader = withTranslation()( showStyleBase={this.props.showStyleBase} showStyleVariant={this.props.showStyleVariant} studio={this.props.studio} - studioMode={this.props.studioMode} + studioMode={this.props.userPermissions.studio} shouldQueue={this.state.shouldQueue} onChangeQueueAdLib={this.changeQueueAdLib} selectedPiece={this.state.selectedPiece} @@ -1162,7 +1163,6 @@ export interface IContextMenuContext { interface IState { timeScale: number - studioMode: boolean contextMenuContext: IContextMenuContext | null bottomMargin: string followLiveSegments: boolean @@ -1221,6 +1221,8 @@ interface ITrackedProps { nextSegmentPartIds: PartId[] } export function RundownView(props: Readonly): JSX.Element { + const userPermissions = React.useContext(UserPermissionsContext) + const playlistId = props.playlistId const requiredSubsReady: boolean[] = [] @@ -1321,11 +1323,12 @@ export function RundownView(props: Readonly): JSX.Element { }, [playlistId]) const subsReady = requiredSubsReady.findIndex((ready) => !ready) === -1 - return + return } interface IPropsWithReady extends IProps { subsReady: boolean + userPermissions: Readonly } interface IRundownViewContentSnapshot { @@ -1485,7 +1488,6 @@ const RundownViewContent = translateWithTracker { const { t } = this.props - if (this.state.studioMode && part && part._id && this.props.playlist) { + if (this.props.userPermissions.studio && part && part._id && this.props.playlist) { const playlistId = this.props.playlist._id doUserAction( t, @@ -2130,7 +2137,7 @@ const RundownViewContent = translateWithTracker { const { t } = this.props - if (this.state.studioMode && segmentId && this.props.playlist) { + if (this.props.userPermissions.studio && segmentId && this.props.playlist) { const playlistId = this.props.playlist._id doUserAction( t, @@ -2149,7 +2156,7 @@ const RundownViewContent = translateWithTracker { const { t } = this.props - if (this.state.studioMode && (segmentId || segmentId === null) && this.props.playlist) { + if (this.props.userPermissions.studio && (segmentId || segmentId === null) && this.props.playlist) { const playlistId = this.props.playlist._id doUserAction( t, @@ -2168,7 +2175,7 @@ const RundownViewContent = translateWithTracker { const { t } = this.props - if (this.state.studioMode && this.props.playlist) { + if (this.props.userPermissions.studio && this.props.playlist) { const playlistId = this.props.playlist._id doUserAction( t, @@ -2184,7 +2191,7 @@ const RundownViewContent = translateWithTracker { const { t } = this.props - if (this.state.studioMode && this.props.playlist) { + if (this.props.userPermissions.studio && this.props.playlist) { const playlistId = this.props.playlist._id doUserAction( t, @@ -2201,7 +2208,7 @@ const RundownViewContent = translateWithTracker) => { const { t } = this.props if ( - this.state.studioMode && + this.props.userPermissions.studio && item && item.instance && this.props.playlist && @@ -2645,7 +2652,7 @@ const RundownViewContent = translateWithTracker): boolean => { - if (!getAllowDeveloper()) { + if (!this.props.userPermissions.developer) { e.preventDefault() e.stopPropagation() } @@ -2963,7 +2970,7 @@ const RundownViewContent = translateWithTracker {this.renderSegmentsList()} - {this.props.matchedSegments && this.props.matchedSegments.length > 0 && getAllowStudio() && ( - - )} + {this.props.matchedSegments && + this.props.matchedSegments.length > 0 && + this.props.userPermissions.studio && } r._id)} firstRundown={this.props.rundowns[0]} onActivate={this.onActivate} - studioMode={this.state.studioMode} + userPermissions={this.props.userPermissions} inActiveRundownView={this.props.inActiveRundownView} currentRundown={this.state.currentRundown || this.props.rundowns[0]} layout={this.state.rundownHeaderLayout} @@ -2991,8 +2998,8 @@ const RundownViewContent = translateWithTracker - {this.state.studioMode && !Settings.disableBlurBorder && ( - + {this.props.userPermissions.studio && !Settings.disableBlurBorder && ( +

- {this.state.studioMode && ( + {this.props.userPermissions.studio && ( <> - - ))} - {getAllowDeveloper() ? ( - - ) : null} - {this.props.showRemoveButtons ? ( +
+ ) : null} + + {this.props.debugState ? : null} + +
+
+ {configManifest?.actions?.map((action) => ( + - ) : null} - {getAllowStudio() && this.props.device.subType === PERIPHERAL_SUBTYPE_PROCESS ? ( - - - - ) : null} -
-
- -
-
- ) - } - } -) - -interface ICoreItemProps { - systemStatus: StatusResponse | undefined - coreSystem: ICoreSystem -} - -interface ICoreItemState {} - -export const CoreItem = reacti18next.withTranslation()( - class CoreItem extends React.Component, ICoreItemState> { - constructor(props: Translated) { - super(props) - this.state = {} - } - - render(): JSX.Element { - const { t } = this.props - - return ( -
- -
-
- {t('Sofie Automation Server Core: {{name}}', { name: this.props.coreSystem.name || 'unnamed' })} -
-
-
- -
{__APP_VERSION__ || 'UNSTABLE'}
-
- - {(getAllowConfigure() || getAllowDeveloper()) && ( -
-
+ + ))} + {this.props.userPermissions.developer ? ( + + ) : null} + {this.props.showRemoveButtons ? ( + + ) : null} + {this.props.userPermissions.studio && this.props.device.subType === PERIPHERAL_SUBTYPE_PROCESS ? ( + -
-
- )} - -
+ + ) : null} +
- ) - } + +
+ + ) } -) +} + +interface ICoreItemProps { + systemStatus: StatusResponse | undefined + coreSystem: ICoreSystem +} + +export function CoreItem({ systemStatus, coreSystem }: ICoreItemProps): JSX.Element { + const { t } = reacti18next.useTranslation() + + const userPermissions = React.useContext(UserPermissionsContext) + + return ( +
+ +
+
+ {t('Sofie Automation Server Core: {{name}}', { name: coreSystem.name || 'unnamed' })} +
+
+
+ +
{__APP_VERSION__ || 'UNSTABLE'}
+
+ + {(userPermissions.configure || userPermissions.developer) && ( +
+
+ +
+
+ )} + +
+
+ ) +} interface ISystemStatusProps {} interface ISystemStatusState { diff --git a/packages/webui/src/client/ui/UserPermissions.tsx b/packages/webui/src/client/ui/UserPermissions.tsx new file mode 100644 index 0000000000..a11fa4bb9e --- /dev/null +++ b/packages/webui/src/client/ui/UserPermissions.tsx @@ -0,0 +1,74 @@ +import React, { useEffect, useMemo, useState } from 'react' +import { + getLocalAllowStudio, + getLocalAllowConfigure, + getLocalAllowDeveloper, + getLocalAllowTesting, + getLocalAllowService, + setAllowStudio, + setAllowConfigure, + setAllowDeveloper, + setAllowTesting, + setAllowService, +} from '../lib/localStorage' +import { parse as queryStringParse } from 'query-string' + +export interface UserPermissions { + studio: boolean + configure: boolean + developer: boolean + testing: boolean + service: boolean +} + +export const UserPermissionsContext = React.createContext>({ + studio: false, + configure: false, + developer: false, + testing: false, + service: false, +}) + +export function useUserPermissions(): UserPermissions { + const location = window.location + + const [permissions, setPermissions] = useState({ + studio: getLocalAllowStudio(), + configure: getLocalAllowConfigure(), + developer: getLocalAllowDeveloper(), + testing: getLocalAllowTesting(), + service: getLocalAllowService(), + }) + + useEffect(() => { + if (!location.search) return + + const params = queryStringParse(location.search) + + if (params['studio']) setAllowStudio(params['studio'] === '1') + if (params['configure']) setAllowConfigure(params['configure'] === '1') + if (params['develop']) setAllowDeveloper(params['develop'] === '1') + if (params['testing']) setAllowTesting(params['testing'] === '1') + if (params['service']) setAllowService(params['service'] === '1') + + if (params['admin']) { + const val = params['admin'] === '1' + setAllowStudio(val) + setAllowConfigure(val) + setAllowDeveloper(val) + setAllowTesting(val) + setAllowService(val) + } + + setPermissions({ + studio: getLocalAllowStudio(), + configure: getLocalAllowConfigure(), + developer: getLocalAllowDeveloper(), + testing: getLocalAllowTesting(), + service: getLocalAllowService(), + }) + }, [location.search]) + + // A naive memoizing of the value, to avoid reactions when the value is identical + return useMemo(() => permissions, [JSON.stringify(permissions)]) +}