diff --git a/meteor/client/ui/Collections.tsx b/meteor/client/ui/Collections.tsx index a8d8227bf8..2ced6bb5fd 100644 --- a/meteor/client/ui/Collections.tsx +++ b/meteor/client/ui/Collections.tsx @@ -44,3 +44,10 @@ export const UIPieceContentStatuses = createSyncCustomPublicationMongoCollection export const UIBucketContentStatuses = createSyncCustomPublicationMongoCollection( CustomCollectionName.UIBucketContentStatuses ) + +/** + * Pre-processed Blueprint Upgrade statuses + */ +export const UIBlueprintUpgradeStatuses = createSyncCustomPublicationMongoCollection( + CustomCollectionName.UIBlueprintUpgradeStatuses +) diff --git a/meteor/client/ui/Settings/Migration.tsx b/meteor/client/ui/Settings/Migration.tsx index 07b76e821d..b417158a13 100644 --- a/meteor/client/ui/Settings/Migration.tsx +++ b/meteor/client/ui/Settings/Migration.tsx @@ -12,8 +12,8 @@ import * as _ from 'underscore' import { EditAttribute, EditAttributeBase } from '../../lib/EditAttribute' import { MeteorCall } from '../../../lib/api/methods' import { checkForOldDataAndCleanUp } from './SystemManagement' -import { UpgradesView } from './Upgrades' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' +import { UpgradesView } from './Upgrades/View' interface IProps {} interface IState { diff --git a/meteor/client/ui/Settings/ShowStyle/BlueprintConfiguration/index.tsx b/meteor/client/ui/Settings/ShowStyle/BlueprintConfiguration/index.tsx index d554c561e3..7c6e51695e 100644 --- a/meteor/client/ui/Settings/ShowStyle/BlueprintConfiguration/index.tsx +++ b/meteor/client/ui/Settings/ShowStyle/BlueprintConfiguration/index.tsx @@ -8,6 +8,11 @@ import { MappingsExt } from '@sofie-automation/corelib/dist/dataModel/Studio' import { DBShowStyleBase, SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { SelectConfigPreset } from './SelectConfigPreset' import { SelectBlueprint } from './SelectBlueprint' +import { ShowStyleBaseId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { PubSub } from '../../../../../lib/api/pubsub' +import { useSubscription, useTracker } from '../../../../lib/ReactMeteorData/ReactMeteorData' +import { UIBlueprintUpgradeStatuses } from '../../../Collections' +import { getUpgradeStatusMessage, UpgradeStatusButtons } from '../../Upgrades/Components' interface ShowStyleBaseBlueprintConfigurationSettingsProps { showStyleBase: DBShowStyleBase @@ -46,6 +51,8 @@ export function ShowStyleBaseBlueprintConfigurationSettings( + + ) } + +interface BlueprintUpgradeStatusProps { + showStyleBaseId: ShowStyleBaseId +} + +function BlueprintUpgradeStatus({ showStyleBaseId }: BlueprintUpgradeStatusProps): JSX.Element { + const { t } = useTranslation() + + const isReady = useSubscription(PubSub.uiBlueprintUpgradeStatuses) + + const status = useTracker( + () => + UIBlueprintUpgradeStatuses.findOne({ + documentId: showStyleBaseId, + documentType: 'showStyle', + }), + [showStyleBaseId] + ) + + const statusMessage = isReady && status ? getUpgradeStatusMessage(t, status) ?? t('OK') : t('Loading...') + + return ( +

+ {t('Upgrade Status')}: {statusMessage} + {status && } +

+ ) +} diff --git a/meteor/client/ui/Settings/Studio/BlueprintConfiguration/index.tsx b/meteor/client/ui/Settings/Studio/BlueprintConfiguration/index.tsx index 76d05f9033..9316002382 100644 --- a/meteor/client/ui/Settings/Studio/BlueprintConfiguration/index.tsx +++ b/meteor/client/ui/Settings/Studio/BlueprintConfiguration/index.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useMemo } from 'react' -import { useTracker } from '../../../../lib/ReactMeteorData/react-meteor-data' +import { useSubscription, useTracker } from '../../../../lib/ReactMeteorData/react-meteor-data' import { BlueprintManifestType } from '@sofie-automation/blueprints-integration' import { BlueprintConfigSchemaSettings } from '../../BlueprintConfigSchema' import { @@ -12,6 +12,10 @@ import { useTranslation } from 'react-i18next' import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { SelectConfigPreset } from './SelectConfigPreset' import { SelectBlueprint } from './SelectBlueprint' +import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { PubSub } from '../../../../../lib/api/pubsub' +import { UIBlueprintUpgradeStatuses } from '../../../Collections' +import { getUpgradeStatusMessage, UpgradeStatusButtons } from '../../Upgrades/Components' interface StudioBlueprintConfigurationSettingsProps { studio: DBStudio @@ -58,6 +62,8 @@ export function StudioBlueprintConfigurationSettings(props: StudioBlueprintConfi + + ) } + +interface BlueprintUpgradeStatusProps { + studioId: StudioId +} + +function BlueprintUpgradeStatus({ studioId }: BlueprintUpgradeStatusProps): JSX.Element { + const { t } = useTranslation() + + const isReady = useSubscription(PubSub.uiBlueprintUpgradeStatuses) + + const status = useTracker( + () => + UIBlueprintUpgradeStatuses.findOne({ + documentId: studioId, + documentType: 'studio', + }), + [studioId] + ) + + const statusMessage = isReady && status ? getUpgradeStatusMessage(t, status) ?? t('OK') : t('Loading...') + + return ( +

+ {t('Upgrade Status')}: {statusMessage} + {status && } +

+ ) +} diff --git a/meteor/client/ui/Settings/Upgrades.tsx b/meteor/client/ui/Settings/Upgrades.tsx deleted file mode 100644 index 6c8064e2ac..0000000000 --- a/meteor/client/ui/Settings/Upgrades.tsx +++ /dev/null @@ -1,254 +0,0 @@ -import React, { useCallback, useEffect, useState } from 'react' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faClipboardCheck, faDatabase, faEye } from '@fortawesome/free-solid-svg-icons' -import { - GetUpgradeStatusResult, - GetUpgradeStatusResultShowStyleBase, - GetUpgradeStatusResultStudio, -} from '../../../lib/api/migration' -import { MeteorCall } from '../../../lib/api/methods' -import { useTranslation } from 'react-i18next' -import { Spinner } from '../../lib/Spinner' -import { getRandomString } from '@sofie-automation/corelib/dist/lib' -import { i18nTranslator } from '../i18n' -import { translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' -import { doModalDialog } from '../../lib/ModalDialog' -import { NoteSeverity } from '@sofie-automation/blueprints-integration' -import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' -import { BlueprintValidateConfigForStudioResult } from '@sofie-automation/corelib/dist/worker/studio' -import { NotificationCenter, NoticeLevel, Notification } from '../../../lib/notifications/notifications' -import { catchError } from '../../lib/lib' - -export function UpgradesView(): JSX.Element { - const { t } = useTranslation() - - const [refreshToken, setRefreshToken] = useState(() => getRandomString()) - const [upgradeStatus, setUpgradeStatus] = useState(null) - - useEffect(() => { - // clear cached data - setUpgradeStatus(null) - - MeteorCall.migration - .getUpgradeStatus() - .then((res) => { - setUpgradeStatus(res) - }) - .catch((e) => { - catchError('migration.getUpgradeStatus')(e) - - NotificationCenter.push( - new Notification(undefined, NoticeLevel.WARNING, t('Failed to check status.'), 'UpgradesView') - ) - }) - }, [refreshToken]) - - const clickRefresh = useCallback(() => setRefreshToken(getRandomString()), []) - - return ( -
-

{t('Apply blueprint upgrades')}

- -
- -
- -
- {!upgradeStatus && } - - - - - - - - - {upgradeStatus && upgradeStatus.showStyleBases.length === 0 && upgradeStatus.studios.length === 0 && ( - - - - )} - - {upgradeStatus && - upgradeStatus.studios.map((studio) => ( - MeteorCall.migration.validateConfigForStudio(studio.studioId)} - applyConfig={() => - MeteorCall.migration.runUpgradeForStudio(studio.studioId).finally(() => { - clickRefresh() - }) - } - /> - ))} - - {upgradeStatus && - upgradeStatus.showStyleBases.map((showStyleBase) => ( - - MeteorCall.migration.validateConfigForShowStyleBase(showStyleBase.showStyleBaseId) - } - applyConfig={() => - MeteorCall.migration.runUpgradeForShowStyleBase(showStyleBase.showStyleBaseId).finally(() => { - clickRefresh() - }) - } - /> - ))} - -
Name  
No Studios or ShowStyles were found
-
-
- ) -} - -interface ShowUpgradesRowProps { - resourceName: string - upgradeResult: GetUpgradeStatusResultShowStyleBase | GetUpgradeStatusResultStudio - validateConfig: () => Promise - applyConfig: () => Promise -} -function ShowUpgradesRow({ resourceName, upgradeResult, validateConfig, applyConfig }: ShowUpgradesRowProps) { - const { t } = useTranslation() - - const clickValidate = useCallback(() => { - validateConfig() - .then((res) => { - const nonInfoMessagesCount = res.messages.filter((msg) => msg.level !== NoteSeverity.INFO).length - - doModalDialog({ - title: t('Upgrade config for {{name}}', { name: upgradeResult.name }), - message: ( -
- {res.messages.length === 0 &&

{t('Config looks good')}

} - {res.messages.map((msg, i) => ( -

- {NoteSeverity[msg.level]}: {translateMessage(msg.message, i18nTranslator)} -

- ))} -
- ), - yes: nonInfoMessagesCount === 0 ? t('Apply') : t('Ignore and apply'), - no: t('Cancel'), - onAccept: () => { - applyConfig() - .then(() => { - NotificationCenter.push( - new Notification( - undefined, - NoticeLevel.NOTIFICATION, - t('Config for {{name}} upgraded successfully', { name: upgradeResult.name }), - 'UpgradesView' - ) - ) - }) - .catch((e) => { - catchError('Upgrade applyConfig')(e) - NotificationCenter.push( - new Notification( - undefined, - NoticeLevel.WARNING, - t('Config for {{name}} upgraded failed', { name: upgradeResult.name }), - 'UpgradesView' - ) - ) - }) - }, - }) - }) - .catch(() => { - doModalDialog({ - title: t('Upgrade config for {{name}}', { name: upgradeResult.name }), - message: t('Failed to validate config'), - yes: t('Ignore and apply'), - no: t('Cancel'), - onAccept: () => { - applyConfig() - .then(() => { - NotificationCenter.push( - new Notification( - undefined, - NoticeLevel.NOTIFICATION, - t('Config for {{name}} upgraded successfully', { name: upgradeResult.name }), - 'UpgradesView' - ) - ) - }) - .catch((e) => { - catchError('Upgrade applyConfig: ignore and apply')(e) - NotificationCenter.push( - new Notification( - undefined, - NoticeLevel.WARNING, - t('Config for {{name}} upgraded failed', { name: upgradeResult.name }), - 'UpgradesView' - ) - ) - }) - }, - }) - }) - }, [upgradeResult]) - - const clickShowChanges = useCallback(() => { - doModalDialog({ - title: t('Upgrade config for {{name}}', { name: upgradeResult.name }), - message: ( -
- {upgradeResult.changes.length === 0 &&

{t('No changes')}

} - {upgradeResult.changes.map((msg, i) => ( -

{translateMessage(msg, i18nTranslator)}

- ))} -
- ), - acceptOnly: true, - yes: t('Dismiss'), - onAccept: () => { - // Do nothing - }, - }) - }, [upgradeResult]) - - return ( - - - {resourceName}:{upgradeResult.name} - - - - {upgradeResult.invalidReason && ( - <> - {t('Unable to upgrade')}: {translateMessage(upgradeResult.invalidReason, i18nTranslator)} - - )} - {upgradeResult.changes.length > 0 && t('Upgrade required')} - - - -
- - - -
- - - ) -} diff --git a/meteor/client/ui/Settings/Upgrades/Components.tsx b/meteor/client/ui/Settings/Upgrades/Components.tsx new file mode 100644 index 0000000000..4829c1b265 --- /dev/null +++ b/meteor/client/ui/Settings/Upgrades/Components.tsx @@ -0,0 +1,172 @@ +import React, { useCallback } from 'react' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faDatabase, faEye } from '@fortawesome/free-solid-svg-icons' +import { MeteorCall } from '../../../../lib/api/methods' +import { TFunction, useTranslation } from 'react-i18next' +import { i18nTranslator } from '../../i18n' +import { translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' +import { doModalDialog } from '../../../lib/ModalDialog' +import { NoteSeverity } from '@sofie-automation/blueprints-integration' +import { NotificationCenter, NoticeLevel, Notification } from '../../../../lib/notifications/notifications' +import { + UIBlueprintUpgradeStatusBase, + UIBlueprintUpgradeStatusShowStyle, + UIBlueprintUpgradeStatusStudio, +} from '../../../../lib/api/upgradeStatus' +import { assertNever } from '@sofie-automation/corelib/dist/lib' +import { catchError } from '../../../lib/lib' + +export function getUpgradeStatusMessage(t: TFunction, upgradeResult: UIBlueprintUpgradeStatusBase): string | null { + if (upgradeResult.invalidReason) + return `${t('Unable to upgrade')}: ${translateMessage(upgradeResult.invalidReason, i18nTranslator)}` + + if (upgradeResult.changes.length > 0) return t('Upgrade required') + + return null +} + +interface UpgradeStatusButtonsProps { + upgradeResult: UIBlueprintUpgradeStatusStudio | UIBlueprintUpgradeStatusShowStyle +} +export function UpgradeStatusButtons({ upgradeResult }: UpgradeStatusButtonsProps): JSX.Element { + const { t } = useTranslation() + + const validateConfig = useCallback(async () => { + switch (upgradeResult.documentType) { + case 'studio': + return MeteorCall.migration.validateConfigForStudio(upgradeResult.documentId) + case 'showStyle': + return MeteorCall.migration.validateConfigForShowStyleBase(upgradeResult.documentId) + default: + assertNever(upgradeResult) + throw new Error(`Unknown UIBlueprintUpgradeStatusBase documentType`) + } + }, [upgradeResult.documentId, upgradeResult.documentType]) + const applyConfig = useCallback(async () => { + switch (upgradeResult.documentType) { + case 'studio': + return MeteorCall.migration.runUpgradeForStudio(upgradeResult.documentId) + case 'showStyle': + return MeteorCall.migration.runUpgradeForShowStyleBase(upgradeResult.documentId) + default: + assertNever(upgradeResult) + throw new Error(`Unknown UIBlueprintUpgradeStatusBase documentType`) + } + }, [upgradeResult.documentId, upgradeResult.documentType]) + + const clickValidate = useCallback(() => { + validateConfig() + .then((res) => { + const nonInfoMessagesCount = res.messages.filter((msg) => msg.level !== NoteSeverity.INFO).length + + doModalDialog({ + title: t('Upgrade config for {{name}}', { name: upgradeResult.name }), + message: ( +
+ {res.messages.length === 0 &&

{t('Config looks good')}

} + {res.messages.map((msg, i) => ( +

+ {NoteSeverity[msg.level]}: {translateMessage(msg.message, i18nTranslator)} +

+ ))} +
+ ), + yes: nonInfoMessagesCount === 0 ? t('Apply') : t('Ignore and apply'), + no: t('Cancel'), + onAccept: () => { + applyConfig() + .then(() => { + NotificationCenter.push( + new Notification( + undefined, + NoticeLevel.NOTIFICATION, + t('Config for {{name}} upgraded successfully', { name: upgradeResult.name }), + 'UpgradesView' + ) + ) + }) + .catch((e) => { + catchError('Upgrade applyConfig')(e) + NotificationCenter.push( + new Notification( + undefined, + NoticeLevel.WARNING, + t('Config for {{name}} upgraded failed', { name: upgradeResult.name }), + 'UpgradesView' + ) + ) + }) + }, + }) + }) + .catch(() => { + doModalDialog({ + title: t('Upgrade config for {{name}}', { name: upgradeResult.name }), + message: t('Failed to validate config'), + yes: t('Ignore and apply'), + no: t('Cancel'), + onAccept: () => { + applyConfig() + .then(() => { + NotificationCenter.push( + new Notification( + undefined, + NoticeLevel.NOTIFICATION, + t('Config for {{name}} upgraded successfully', { name: upgradeResult.name }), + 'UpgradesView' + ) + ) + }) + .catch((e) => { + catchError('Upgrade applyConfig')(e) + NotificationCenter.push( + new Notification( + undefined, + NoticeLevel.WARNING, + t('Config for {{name}} upgraded failed', { name: upgradeResult.name }), + 'UpgradesView' + ) + ) + }) + }, + }) + }) + }, [upgradeResult, validateConfig, applyConfig]) + + const clickShowChanges = useCallback(() => { + doModalDialog({ + title: t('Upgrade config for {{name}}', { name: upgradeResult.name }), + message: ( +
+ {upgradeResult.changes.length === 0 &&

{t('No changes')}

} + {upgradeResult.changes.map((msg, i) => ( +

{translateMessage(msg, i18nTranslator)}

+ ))} +
+ ), + acceptOnly: true, + yes: t('Dismiss'), + onAccept: () => { + // Do nothing + }, + }) + }, [upgradeResult]) + + return ( +
+ + + +
+ ) +} diff --git a/meteor/client/ui/Settings/Upgrades/View.tsx b/meteor/client/ui/Settings/Upgrades/View.tsx new file mode 100644 index 0000000000..059755e852 --- /dev/null +++ b/meteor/client/ui/Settings/Upgrades/View.tsx @@ -0,0 +1,86 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import { Spinner } from '../../../lib/Spinner' +import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' +import { useSubscription, useTracker } from '../../../lib/ReactMeteorData/ReactMeteorData' +import { PubSub } from '../../../../lib/api/pubsub' +import { UIBlueprintUpgradeStatuses } from '../../Collections' +import { UIBlueprintUpgradeStatusShowStyle, UIBlueprintUpgradeStatusStudio } from '../../../../lib/api/upgradeStatus' +import { getUpgradeStatusMessage, UpgradeStatusButtons } from './Components' + +export function UpgradesView(): JSX.Element { + const { t } = useTranslation() + + const isReady = useSubscription(PubSub.uiBlueprintUpgradeStatuses) + + const statuses = useTracker(() => UIBlueprintUpgradeStatuses.find().fetch(), []) + + return ( +
+

{t('Apply blueprint upgrades')}

+ +
+ {(!isReady || !statuses) && } + + + + + + + + + {isReady && statuses && statuses.length === 0 && ( + + + + )} + + {statuses?.map( + (document) => + document.documentType === 'studio' && ( + + ) + )} + + {statuses?.map( + (document) => + document.documentType === 'showStyle' && ( + + ) + )} + +
Name  
No Studios or ShowStyles were found
+
+
+ ) +} + +interface ShowUpgradesRowProps { + resourceName: string + upgradeResult: UIBlueprintUpgradeStatusStudio | UIBlueprintUpgradeStatusShowStyle +} +function ShowUpgradesRow({ resourceName, upgradeResult }: ShowUpgradesRowProps) { + const { t } = useTranslation() + + return ( + + + {resourceName}:{upgradeResult.name} + + + {getUpgradeStatusMessage(t, upgradeResult)} + + + + + + ) +} diff --git a/meteor/lib/api/migration.ts b/meteor/lib/api/migration.ts index ce2b8c0ae9..d2c4d145b6 100644 --- a/meteor/lib/api/migration.ts +++ b/meteor/lib/api/migration.ts @@ -1,6 +1,5 @@ import { MigrationStepInput, MigrationStepInputResult } from '@sofie-automation/blueprints-integration' import { BlueprintId, ShowStyleBaseId, SnapshotId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { ITranslatableMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' import { BlueprintValidateConfigForStudioResult } from '@sofie-automation/corelib/dist/worker/studio' export interface NewMigrationAPI { @@ -14,11 +13,6 @@ export interface NewMigrationAPI { forceMigration(chunks: Array): Promise resetDatabaseVersions(): Promise - /** - * Get the status information for each Studio and ShowStyle on their blueprintConfig upgrade status - */ - getUpgradeStatus(): Promise - /** * Run `validateConfig` on the blueprint for a Studio * @param studioId Id of the Studio @@ -59,28 +53,6 @@ export enum MigrationAPIMethods { 'runUpgradeForShowStyleBase' = 'migration.runUpgradeForShowStyleBase', } -export interface GetUpgradeStatusResultStudio { - studioId: StudioId - name: string - - invalidReason?: ITranslatableMessage - - changes: ITranslatableMessage[] -} -export interface GetUpgradeStatusResultShowStyleBase { - showStyleBaseId: ShowStyleBaseId - name: string - - invalidReason?: ITranslatableMessage - - changes: ITranslatableMessage[] -} - -export interface GetUpgradeStatusResult { - studios: GetUpgradeStatusResultStudio[] - showStyleBases: GetUpgradeStatusResultShowStyleBase[] -} - export interface GetMigrationStatusResult { migrationNeeded: boolean diff --git a/meteor/lib/api/pubsub.ts b/meteor/lib/api/pubsub.ts index 6b08d13566..d0ebc7ba31 100644 --- a/meteor/lib/api/pubsub.ts +++ b/meteor/lib/api/pubsub.ts @@ -63,6 +63,7 @@ import { import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { RoutedMappings } from '@sofie-automation/shared-lib/dist/core/model/Timeline' import { logger } from '../logging' +import { UIBlueprintUpgradeStatus } from './upgradeStatus' /** * Ids of possible DDP subscriptions @@ -139,6 +140,7 @@ export enum PubSub { uiSegmentPartNotes = 'uiSegmentPartNotes', uiPieceContentStatuses = 'uiPieceContentStatuses', uiBucketContentStatuses = 'uiBucketContentStatuses', + uiBlueprintUpgradeStatuses = 'uiBlueprintUpgradeStatuses', packageManagerPlayoutContext = 'packageManagerPlayoutContext', packageManagerPackageContainers = 'packageManagerPackageContainers', @@ -250,6 +252,7 @@ export interface PubSubTypes { [PubSub.uiSegmentPartNotes]: (playlistId: RundownPlaylistId | null) => UISegmentPartNote [PubSub.uiPieceContentStatuses]: (rundownPlaylistId: RundownPlaylistId | null) => UIPieceContentStatus [PubSub.uiBucketContentStatuses]: (studioId: StudioId, bucketId: BucketId) => UIBucketContentStatus + [PubSub.uiBlueprintUpgradeStatuses]: () => UIBlueprintUpgradeStatus /** Custom publications for package-manager */ [PubSub.packageManagerPlayoutContext]: ( @@ -283,6 +286,7 @@ export enum CustomCollectionName { UISegmentPartNotes = 'uiSegmentPartNotes', UIPieceContentStatuses = 'uiPieceContentStatuses', UIBucketContentStatuses = 'uiBucketContentStatuses', + UIBlueprintUpgradeStatuses = 'uiBlueprintUpgradeStatuses', PackageManagerPlayoutContext = 'packageManagerPlayoutContext', PackageManagerPackageContainers = 'packageManagerPackageContainers', @@ -306,6 +310,7 @@ export type CustomCollectionType = { [CustomCollectionName.UISegmentPartNotes]: UISegmentPartNote [CustomCollectionName.UIPieceContentStatuses]: UIPieceContentStatus [CustomCollectionName.UIBucketContentStatuses]: UIBucketContentStatus + [CustomCollectionName.UIBlueprintUpgradeStatuses]: UIBlueprintUpgradeStatus [CustomCollectionName.PackageManagerPlayoutContext]: PackageManagerPlayoutContext [CustomCollectionName.PackageManagerPackageContainers]: PackageManagerPackageContainers [CustomCollectionName.PackageManagerExpectedPackages]: PackageManagerExpectedPackage diff --git a/meteor/lib/api/upgradeStatus.ts b/meteor/lib/api/upgradeStatus.ts new file mode 100644 index 0000000000..f1eda15071 --- /dev/null +++ b/meteor/lib/api/upgradeStatus.ts @@ -0,0 +1,29 @@ +import { ITranslatableMessage } from '@sofie-automation/blueprints-integration' +import { StudioId, ShowStyleBaseId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { ProtectedString } from '../lib' + +export type UIBlueprintUpgradeStatusId = ProtectedString<'UIBlueprintUpgradeStatus'> + +export type UIBlueprintUpgradeStatus = UIBlueprintUpgradeStatusStudio | UIBlueprintUpgradeStatusShowStyle + +export interface UIBlueprintUpgradeStatusBase { + _id: UIBlueprintUpgradeStatusId + + documentType: 'studio' | 'showStyle' + documentId: StudioId | ShowStyleBaseId + + name: string + + invalidReason?: ITranslatableMessage + + changes: ITranslatableMessage[] +} + +export interface UIBlueprintUpgradeStatusStudio extends UIBlueprintUpgradeStatusBase { + documentType: 'studio' + documentId: StudioId +} +export interface UIBlueprintUpgradeStatusShowStyle extends UIBlueprintUpgradeStatusBase { + documentType: 'showStyle' + documentId: ShowStyleBaseId +} diff --git a/meteor/server/lib/customPublication/__tests__/optimizedObserver.test.ts b/meteor/server/lib/customPublication/__tests__/optimizedObserver.test.ts index 5038d3b5d3..d4bec4a0a8 100644 --- a/meteor/server/lib/customPublication/__tests__/optimizedObserver.test.ts +++ b/meteor/server/lib/customPublication/__tests__/optimizedObserver.test.ts @@ -9,7 +9,7 @@ interface CustomPublishMockExt { } class CustomPublishMock }> - implements Omit, '#onStop' | '#isReady'>, CustomPublishMockExt + implements CustomPublish, CustomPublishMockExt { static create }>(): CustomPublish & CustomPublishMockExt { const mock = new CustomPublishMock() diff --git a/meteor/server/lib/customPublication/publish.ts b/meteor/server/lib/customPublication/publish.ts index 59677c857b..740dc3cdbf 100644 --- a/meteor/server/lib/customPublication/publish.ts +++ b/meteor/server/lib/customPublication/publish.ts @@ -10,7 +10,26 @@ export interface CustomPublishChanges }> { removed: T['_id'][] } -export class CustomPublish }> { +export interface CustomPublish }> { + get isReady(): boolean + + /** + * Register a function to be called when the subscriber unsubscribes + */ + onStop(callback: () => void): void + + /** + * Send the intial documents to the subscriber + */ + init(docs: DBObj[]): void + + /** + * Send a batch of changes to the subscriber + */ + changed(changes: CustomPublishChanges): void +} + +export class CustomPublishMeteor }> { #onStop: (() => void) | undefined #isReady = false @@ -80,6 +99,6 @@ export function meteorCustomPublish( ) => Promise ): void { meteorPublishUnsafe(publicationName, async function (this: SubscriptionContext, ...args: any[]) { - return cb.call(this, new CustomPublish(this, customCollectionName), ...(args as any)) + return cb.call(this, new CustomPublishMeteor(this, customCollectionName), ...(args as any)) }) } diff --git a/meteor/server/migration/api.ts b/meteor/server/migration/api.ts index 408ef1978b..c56690f8b7 100644 --- a/meteor/server/migration/api.ts +++ b/meteor/server/migration/api.ts @@ -1,12 +1,11 @@ import { check, Match } from '../../lib/check' import { registerClassToMeteorMethods } from '../methods' -import { MigrationChunk, NewMigrationAPI, MigrationAPIMethods, GetUpgradeStatusResult } from '../../lib/api/migration' +import { MigrationChunk, NewMigrationAPI, MigrationAPIMethods } from '../../lib/api/migration' import * as Migrations from './databaseMigration' import { MigrationStepInputResult } from '@sofie-automation/blueprints-integration' import { MethodContextAPI } from '../../lib/api/methods' import { SystemWriteAccess } from '../security/system' import { - getUpgradeStatus, runUpgradeForShowStyleBase, runUpgradeForStudio, validateConfigForShowStyleBase, @@ -50,12 +49,6 @@ class ServerMigrationAPI extends MethodContextAPI implements NewMigrationAPI { return Migrations.resetDatabaseVersions() } - async getUpgradeStatus(): Promise { - await SystemWriteAccess.migrations(this) - - return getUpgradeStatus() - } - async validateConfigForStudio(studioId: StudioId): Promise { check(studioId, String) diff --git a/meteor/server/migration/upgrades/__tests__/checkStatus.test.ts b/meteor/server/migration/upgrades/__tests__/checkStatus.test.ts deleted file mode 100644 index 83041c037e..0000000000 --- a/meteor/server/migration/upgrades/__tests__/checkStatus.test.ts +++ /dev/null @@ -1,341 +0,0 @@ -import '../../../../__mocks__/_extendJest' -import { protectString } from '@sofie-automation/corelib/dist/protectedString' -import { testInFiber } from '../../../../__mocks__/helpers/jest' -import { getUpgradeStatus } from '../checkStatus' -import { literal } from '@sofie-automation/corelib/dist/lib' -import { GetUpgradeStatusResult } from '../../../../lib/api/migration' -import { - setupMockShowStyleBase, - setupMockShowStyleBlueprint, - setupMockStudio, - setupMockStudioBlueprint, -} from '../../../../__mocks__/helpers/database' -import { generateTranslation } from '../../../../lib/lib' -import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' -import { ShowStyleBases, Studios } from '../../../collections' - -describe('getUpgradeStatus', () => { - afterEach(async () => { - await Studios.removeAsync({}) - await ShowStyleBases.removeAsync({}) - }) - - testInFiber('no studios or showstyles', async () => { - const result = await getUpgradeStatus() - expect(result).toEqual( - literal({ - showStyleBases: [], - studios: [], - }) - ) - }) - - testInFiber('Studios and showStyles missing blueprints', async () => { - const studio0 = await setupMockStudio() - const studio1 = await setupMockStudio() - const showStyle0 = await setupMockShowStyleBase(protectString('')) - const showStyle1 = await setupMockShowStyleBase(protectString('')) - - const result = await getUpgradeStatus() - expect(result).toEqual( - literal({ - showStyleBases: [ - { - showStyleBaseId: showStyle0._id, - name: showStyle0.name, - changes: [], - invalidReason: generateTranslation('Invalid blueprint: "{{blueprintId}}"', { - blueprintId: showStyle0.blueprintId, - }), - }, - { - showStyleBaseId: showStyle1._id, - name: showStyle1.name, - changes: [], - invalidReason: generateTranslation('Invalid blueprint: "{{blueprintId}}"', { - blueprintId: showStyle1.blueprintId, - }), - }, - ], - studios: [ - { - studioId: studio0._id, - name: studio0.name, - changes: [], - invalidReason: generateTranslation('Invalid blueprint: "{{blueprintId}}"', { - blueprintId: studio0.blueprintId, - }), - }, - { - studioId: studio1._id, - name: studio1.name, - changes: [], - invalidReason: generateTranslation('Invalid blueprint: "{{blueprintId}}"', { - blueprintId: studio1.blueprintId, - }), - }, - ], - }) - ) - }) - - testInFiber('Invalid config preset', async () => { - const showStyleBlueprint = await setupMockShowStyleBlueprint(protectString('')) - const showStyle0 = await setupMockShowStyleBase(showStyleBlueprint._id) - - const result = await getUpgradeStatus() - expect(result).toEqual( - literal({ - showStyleBases: [ - { - showStyleBaseId: showStyle0._id, - name: showStyle0.name, - changes: [], - invalidReason: generateTranslation( - 'Invalid config preset for blueprint: "{{configPresetId}}" ({{blueprintId}})', - { - configPresetId: showStyle0.blueprintConfigPresetId ?? '', - blueprintId: showStyle0.blueprintId, - } - ), - }, - ], - studios: [], - }) - ) - }) - - testInFiber('Not run before', async () => { - const showStyleBlueprint = await setupMockShowStyleBlueprint(protectString('')) - const showStyle0 = await setupMockShowStyleBase(showStyleBlueprint._id, { blueprintConfigPresetId: 'main' }) - - const result = await getUpgradeStatus() - expect(result).toEqual( - literal({ - showStyleBases: [ - { - showStyleBaseId: showStyle0._id, - name: showStyle0.name, - changes: [generateTranslation('Config has not been applied before')], - }, - ], - studios: [], - }) - ) - }) - - testInFiber('Blueprint id has changed', async () => { - const showStyleBlueprint = await setupMockShowStyleBlueprint(protectString('')) - const showStyle0 = await setupMockShowStyleBase(showStyleBlueprint._id, { - blueprintConfigPresetId: 'main', - lastBlueprintConfig: { - blueprintHash: showStyleBlueprint.blueprintHash, - blueprintId: protectString('old-blueprint'), - blueprintConfigPresetId: 'main', - config: {}, - }, - }) - - const result = await getUpgradeStatus() - expect(result).toEqual( - literal({ - showStyleBases: [ - { - showStyleBaseId: showStyle0._id, - name: showStyle0.name, - changes: [ - generateTranslation( - 'Blueprint has been changed. From "{{ oldValue }}", to "{{ newValue }}"', - { - oldValue: protectString('old-blueprint'), - newValue: showStyle0.blueprintId || '', - } - ), - ], - }, - ], - studios: [], - }) - ) - }) - - testInFiber('Config preset has changed', async () => { - const showStyleBlueprint = await setupMockShowStyleBlueprint(protectString('')) - const showStyle0 = await setupMockShowStyleBase(showStyleBlueprint._id, { - blueprintConfigPresetId: 'main', - lastBlueprintConfig: { - blueprintHash: showStyleBlueprint.blueprintHash, - blueprintId: showStyleBlueprint._id, - blueprintConfigPresetId: 'old', - config: {}, - }, - }) - - const result = await getUpgradeStatus() - expect(result).toEqual( - literal({ - showStyleBases: [ - { - showStyleBaseId: showStyle0._id, - name: showStyle0.name, - changes: [ - generateTranslation( - 'Blueprint config preset has been changed. From "{{ oldValue }}", to "{{ newValue }}"', - { - oldValue: 'old', - newValue: showStyle0.blueprintConfigPresetId || '', - } - ), - ], - }, - ], - studios: [], - }) - ) - }) - - testInFiber('Blueprint hash has changed', async () => { - const showStyleBlueprint = await setupMockShowStyleBlueprint(protectString('')) - const showStyle0 = await setupMockShowStyleBase(showStyleBlueprint._id, { - blueprintConfigPresetId: 'main', - lastBlueprintConfig: { - blueprintHash: protectString('old-hash'), - blueprintId: showStyleBlueprint._id, - blueprintConfigPresetId: 'main', - config: {}, - }, - }) - - const result = await getUpgradeStatus() - expect(result).toEqual( - literal({ - showStyleBases: [ - { - showStyleBaseId: showStyle0._id, - name: showStyle0.name, - changes: [generateTranslation('Blueprint has a new version')], - }, - ], - studios: [], - }) - ) - }) - - testInFiber('Conifg has changed', async () => { - const showStyleBlueprint = await setupMockShowStyleBlueprint(protectString('')) - const showStyle0 = await setupMockShowStyleBase(showStyleBlueprint._id, { - blueprintConfigPresetId: 'main', - blueprintConfigWithOverrides: wrapDefaultObject({ - prop1: 'new-value', - some: { - deep: { - property: 2, - }, - }, - prop3: false, - }), - lastBlueprintConfig: { - blueprintHash: showStyleBlueprint.blueprintHash, - blueprintId: showStyleBlueprint._id, - blueprintConfigPresetId: 'main', - config: { - prop1: 'old-value', - some: { - deep: { - property: 1, - }, - }, - prop2: true, - }, - }, - }) - - const result = await getUpgradeStatus() - expect(result).toEqual( - literal({ - showStyleBases: [ - { - showStyleBaseId: showStyle0._id, - name: showStyle0.name, - changes: [generateTranslation('Blueprint config has changed')], - }, - ], - studios: [], - }) - ) - }) - - testInFiber('Good studios and showstyles', async () => { - const showStyleBlueprint = await setupMockShowStyleBlueprint(protectString('')) - const studioBlueprint = await setupMockStudioBlueprint(protectString('')) - - const studio0 = await setupMockStudio({ - blueprintId: studioBlueprint._id, - blueprintConfigPresetId: 'main', - lastBlueprintConfig: { - blueprintHash: studioBlueprint.blueprintHash, - blueprintId: studioBlueprint._id, - blueprintConfigPresetId: 'main', - config: {}, - }, - }) - const studio1 = await setupMockStudio({ - blueprintId: studioBlueprint._id, - blueprintConfigPresetId: 'main', - lastBlueprintConfig: { - blueprintHash: studioBlueprint.blueprintHash, - blueprintId: studioBlueprint._id, - blueprintConfigPresetId: 'main', - config: {}, - }, - }) - const showStyle0 = await setupMockShowStyleBase(showStyleBlueprint._id, { - blueprintConfigPresetId: 'main', - lastBlueprintConfig: { - blueprintHash: showStyleBlueprint.blueprintHash, - blueprintId: showStyleBlueprint._id, - blueprintConfigPresetId: 'main', - config: {}, - }, - }) - const showStyle1 = await setupMockShowStyleBase(showStyleBlueprint._id, { - blueprintConfigPresetId: 'main', - lastBlueprintConfig: { - blueprintHash: showStyleBlueprint.blueprintHash, - blueprintId: showStyleBlueprint._id, - blueprintConfigPresetId: 'main', - config: {}, - }, - }) - - const result = await getUpgradeStatus() - expect(result).toEqual( - literal({ - showStyleBases: [ - { - showStyleBaseId: showStyle0._id, - name: showStyle0.name, - changes: [], - }, - { - showStyleBaseId: showStyle1._id, - name: showStyle1.name, - changes: [], - }, - ], - studios: [ - { - studioId: studio0._id, - name: studio0.name, - changes: [], - }, - { - studioId: studio1._id, - name: studio1.name, - changes: [], - }, - ], - }) - ) - }) -}) diff --git a/meteor/server/migration/upgrades/index.ts b/meteor/server/migration/upgrades/index.ts index 59706f9fdb..301efe5fd0 100644 --- a/meteor/server/migration/upgrades/index.ts +++ b/meteor/server/migration/upgrades/index.ts @@ -1,4 +1,2 @@ -export * from './checkStatus' export * from './showStyleBase' export * from './studio' -export * from './systemStatus' diff --git a/meteor/server/migration/upgrades/systemStatus.ts b/meteor/server/migration/upgrades/systemStatus.ts deleted file mode 100644 index 18a4d5a936..0000000000 --- a/meteor/server/migration/upgrades/systemStatus.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { StatusCode } from '@sofie-automation/blueprints-integration' -import { literal } from '@sofie-automation/corelib/dist/lib' -import { Component } from '../../../lib/api/systemStatus' -import { status2ExternalStatus } from '../../systemStatus/systemStatus' -import { getUpgradeStatus } from './checkStatus' - -export async function getUpgradeSystemStatusMessages(): Promise { - const result: Component[] = [] - - const upgradeStatus = await getUpgradeStatus() - for (const studioStatus of upgradeStatus.studios) { - let status = StatusCode.GOOD - const messages: string[] = [] - if (studioStatus.invalidReason) { - status = StatusCode.WARNING_MAJOR - messages.push('Invalid configuration') - } else if (studioStatus.changes.length) { - status = StatusCode.WARNING_MINOR - messages.push('Configuration changed') - } - - result.push( - literal({ - name: `studio-blueprints-upgrade-${studioStatus.studioId}`, - status: status2ExternalStatus(status), - updated: new Date().toISOString(), - _status: status, - _internal: { - statusCodeString: StatusCode[status], - messages: messages, - versions: {}, - }, - }) - ) - } - for (const showStyleStatus of upgradeStatus.showStyleBases) { - let status = StatusCode.GOOD - const messages: string[] = [] - if (showStyleStatus.invalidReason) { - status = StatusCode.WARNING_MAJOR - messages.push('Invalid configuration') - } else if (showStyleStatus.changes.length) { - status = StatusCode.WARNING_MINOR - messages.push('Configuration changed') - } - - result.push( - literal({ - name: `showStyleBase-blueprints-upgrade-${showStyleStatus.showStyleBaseId}`, - status: status2ExternalStatus(status), - updated: new Date().toISOString(), - _status: status, - _internal: { - statusCodeString: StatusCode[status], - messages: messages, - versions: {}, - }, - }) - ) - } - - return result -} diff --git a/meteor/server/publications/_publications.ts b/meteor/server/publications/_publications.ts index 18057c5009..e05588e8df 100644 --- a/meteor/server/publications/_publications.ts +++ b/meteor/server/publications/_publications.ts @@ -1,6 +1,7 @@ import './lib' import './buckets' +import './blueprintUpgradeStatus/publication' import './packageManager/expectedPackages/publication' import './packageManager/packageContainers' import './packageManager/playoutContext' diff --git a/meteor/server/migration/upgrades/checkStatus.ts b/meteor/server/publications/blueprintUpgradeStatus/checkStatus.ts similarity index 53% rename from meteor/server/migration/upgrades/checkStatus.ts rename to meteor/server/publications/blueprintUpgradeStatus/checkStatus.ts index 3d7c8d422b..df317bb66d 100644 --- a/meteor/server/migration/upgrades/checkStatus.ts +++ b/meteor/server/publications/blueprintUpgradeStatus/checkStatus.ts @@ -1,181 +1,37 @@ import { - BlueprintManifestType, - IStudioConfigPreset, + getSchemaUIField, IShowStyleConfigPreset, + IStudioConfigPreset, ITranslatableMessage, + JSONBlob, + JSONBlobParse, + JSONSchema, + SchemaFormUIField, } from '@sofie-automation/blueprints-integration' -import { Blueprint, BlueprintHash } from '@sofie-automation/corelib/dist/dataModel/Blueprint' +import { BlueprintHash } from '@sofie-automation/corelib/dist/dataModel/Blueprint' import { BlueprintId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { - normalizeArrayToMap, - literal, - objectPathGet, - joinObjectPathFragments, -} from '@sofie-automation/corelib/dist/lib' -import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' -import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' -import _ from 'underscore' -import { - GetUpgradeStatusResult, - GetUpgradeStatusResultStudio, - GetUpgradeStatusResultShowStyleBase, -} from '../../../lib/api/migration' -import { Blueprints, ShowStyleBases, Studios } from '../../collections' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { joinObjectPathFragments, objectPathGet } from '@sofie-automation/corelib/dist/lib' +import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { generateTranslation } from '../../../lib/lib' -import { JSONBlob, JSONBlobParse } from '@sofie-automation/shared-lib/dist/lib/JSONBlob' -import { JSONSchema } from '@sofie-automation/shared-lib/dist/lib/JSONSchemaTypes' import { logger } from '../../logging' +import { ShowStyleBaseFields, StudioFields } from './reactiveContentCache' +import _ from 'underscore' +import { UIBlueprintUpgradeStatusBase } from '../../../lib/api/upgradeStatus' +import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' -export async function getUpgradeStatus(): Promise { - const studioUpgrades = await checkStudiosUpgradeStatus() - const showStyleUpgrades = await checkShowStyleBaseUpgradeStatus() - - return { - studios: studioUpgrades, - showStyleBases: showStyleUpgrades, - } -} - -async function checkStudiosUpgradeStatus(): Promise { - const result: GetUpgradeStatusResultStudio[] = [] - - const studios = (await Studios.findFetchAsync( - {}, - { - fields: { - _id: 1, - blueprintId: 1, - blueprintConfigPresetId: 1, - lastBlueprintConfig: 1, - blueprintConfigWithOverrides: 1, - name: 1, - }, - } - )) as Array - - const studioBlueprints = (await Blueprints.findFetchAsync( - { - blueprintType: BlueprintManifestType.STUDIO, - _id: { $in: _.compact(studios.map((st) => st.blueprintId)) }, - }, - { - fields: { - _id: 1, - studioConfigPresets: 1, - studioConfigSchema: 1, - blueprintHash: 1, - }, - } - )) as Array - - // Check each studio - const blueprintsMap = normalizeArrayToMap( - studioBlueprints.map((doc) => - literal({ - _id: doc._id, - configPresets: doc.studioConfigPresets, - configSchema: doc.studioConfigSchema, - blueprintHash: doc.blueprintHash, - }) - ), - '_id' - ) - for (const studio of studios) { - result.push({ - ...checkDocUpgradeStatus(blueprintsMap, studio), - studioId: studio._id, - name: studio.name, - }) - } - - return result -} - -async function checkShowStyleBaseUpgradeStatus(): Promise { - const result: GetUpgradeStatusResultShowStyleBase[] = [] - - const showStyles = (await ShowStyleBases.findFetchAsync( - {}, - { - fields: { - _id: 1, - blueprintId: 1, - blueprintConfigPresetId: 1, - lastBlueprintConfig: 1, - blueprintConfigWithOverrides: 1, - name: 1, - }, - } - )) as Array - - const showStyleBlueprints = (await Blueprints.findFetchAsync( - { - blueprintType: BlueprintManifestType.SHOWSTYLE, - _id: { $in: _.compact(showStyles.map((st) => st.blueprintId)) }, - }, - { - fields: { - _id: 1, - showStyleConfigPresets: 1, - showStyleConfigSchema: 1, - blueprintHash: 1, - }, - } - )) as Array - - // Check each studio - const blueprintsMap = normalizeArrayToMap( - showStyleBlueprints.map((doc) => - literal({ - _id: doc._id, - configPresets: doc.showStyleConfigPresets, - configSchema: doc.showStyleConfigSchema, - blueprintHash: doc.blueprintHash, - }) - ), - '_id' - ) - for (const showStyle of showStyles) { - result.push({ - ...checkDocUpgradeStatus(blueprintsMap, showStyle), - showStyleBaseId: showStyle._id, - name: showStyle.name, - }) - } - - return result -} - -type StudioForUpgradeCheck = Pick< - DBStudio, - '_id' | 'blueprintId' | 'blueprintConfigPresetId' | 'lastBlueprintConfig' | 'blueprintConfigWithOverrides' | 'name' -> -type ShowStyleBaseForUpgradeCheck = Pick< - DBShowStyleBase, - '_id' | 'blueprintId' | 'blueprintConfigPresetId' | 'lastBlueprintConfig' | 'blueprintConfigWithOverrides' | 'name' -> -type StudioBlueprintForUpgradeCheck = Pick< - Blueprint, - '_id' | 'studioConfigPresets' | 'studioConfigSchema' | 'blueprintHash' -> -type ShowStyleBlueprintForUpgradeCheck = Pick< - Blueprint, - '_id' | 'showStyleConfigPresets' | 'showStyleConfigSchema' | 'blueprintHash' -> - -interface BlueprintMapEntry { +export interface BlueprintMapEntry { _id: BlueprintId configPresets: Record | Record | undefined configSchema: JSONBlob | undefined blueprintHash: BlueprintHash | undefined } -function checkDocUpgradeStatus( +export function checkDocUpgradeStatus( blueprintMap: Map, - doc: StudioForUpgradeCheck | ShowStyleBaseForUpgradeCheck -): Pick { + doc: Pick | Pick +): Pick { // Check the blueprintId is valid const blueprint = doc.blueprintId ? blueprintMap.get(doc.blueprintId) : null if (!blueprint || !blueprint.configPresets) { @@ -288,7 +144,7 @@ function diffJsonSchemaObjects( generateTranslation( 'Config value "{{ name }}" has changed. From "{{ oldValue }}", to "{{ newValue }}"', { - name: (propSchema as any)['ui:title'] || propPath, + name: getSchemaUIField(propSchema, SchemaFormUIField.Title) || propPath, // Future: this is not pretty when it is an object oldValue: JSON.stringify(valueA) ?? '', newValue: JSON.stringify(valueB) ?? '', diff --git a/meteor/server/publications/blueprintUpgradeStatus/publication.ts b/meteor/server/publications/blueprintUpgradeStatus/publication.ts new file mode 100644 index 0000000000..553b1960c6 --- /dev/null +++ b/meteor/server/publications/blueprintUpgradeStatus/publication.ts @@ -0,0 +1,260 @@ +import { BlueprintId, ShowStyleBaseId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { MongoFieldSpecifierOnesStrict } from '@sofie-automation/corelib/dist/mongo' +import { ReadonlyDeep } from 'type-fest' +import { CustomCollectionName, PubSub } from '../../../lib/api/pubsub' +import { literal, ProtectedString, protectString } from '../../../lib/lib' +import { + CustomPublish, + CustomPublishCollection, + meteorCustomPublish, + setUpCollectionOptimizedObserver, + TriggerUpdate, +} from '../../lib/customPublication' +import { logger } from '../../logging' +import { resolveCredentials } from '../../security/lib/credentials' +import { NoSecurityReadAccess } from '../../security/noSecurity' +import { LiveQueryHandle } from '../../lib/lib' +import { ContentCache, createReactiveContentCache, ShowStyleBaseFields, StudioFields } from './reactiveContentCache' +import { UpgradesContentObserver } from './upgradesContentObserver' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { RundownPlaylists } from '../../collections' +import { BlueprintMapEntry, checkDocUpgradeStatus } from './checkStatus' +import { BlueprintManifestType } from '@sofie-automation/blueprints-integration' +import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { UIBlueprintUpgradeStatus, UIBlueprintUpgradeStatusId } from '../../../lib/api/upgradeStatus' + +type BlueprintUpgradeStatusArgs = Record + +export interface BlueprintUpgradeStatusState { + contentCache: ReadonlyDeep +} + +interface BlueprintUpgradeStatusUpdateProps { + newCache: ContentCache + + invalidateStudioIds: StudioId[] + invalidateShowStyleBaseIds: ShowStyleBaseId[] + invalidateBlueprintIds: BlueprintId[] +} + +type RundownPlaylistFields = '_id' | 'studioId' +const rundownPlaylistFieldSpecifier = literal< + MongoFieldSpecifierOnesStrict> +>({ + _id: 1, + studioId: 1, +}) + +async function setupBlueprintUpgradeStatusPublicationObservers( + args: ReadonlyDeep, + triggerUpdate: TriggerUpdate +): Promise { + const playlist = (await RundownPlaylists.findOneAsync(args.playlistId, { + projection: rundownPlaylistFieldSpecifier, + })) as Pick | undefined + if (!playlist) throw new Error(`RundownPlaylist "${args.playlistId}" not found!`) + + // TODO - can this be done cheaper? + const cache = createReactiveContentCache() + + // Push update + triggerUpdate({ newCache: cache }) + + const mongoObserver = new UpgradesContentObserver(cache) + + // Set up observers: + return [ + mongoObserver, + + cache.Studios.find({}).observeChanges({ + added: (id) => triggerUpdate({ invalidateStudioIds: [protectString(id)] }), + changed: (id) => triggerUpdate({ invalidateStudioIds: [protectString(id)] }), + removed: (id) => triggerUpdate({ invalidateStudioIds: [protectString(id)] }), + }), + cache.ShowStyleBases.find({}).observeChanges({ + added: (id) => triggerUpdate({ invalidateShowStyleBaseIds: [protectString(id)] }), + changed: (id) => triggerUpdate({ invalidateShowStyleBaseIds: [protectString(id)] }), + removed: (id) => triggerUpdate({ invalidateShowStyleBaseIds: [protectString(id)] }), + }), + cache.Blueprints.find({}).observeChanges({ + added: (id) => triggerUpdate({ invalidateBlueprintIds: [protectString(id)] }), + changed: (id) => triggerUpdate({ invalidateBlueprintIds: [protectString(id)] }), + removed: (id) => triggerUpdate({ invalidateBlueprintIds: [protectString(id)] }), + }), + ] +} + +function getDocumentId(type: 'studio' | 'showStyle', id: ProtectedString): UIBlueprintUpgradeStatusId { + return protectString(`${type}:${id}`) +} + +export async function manipulateBlueprintUpgradeStatusPublicationData( + _args: BlueprintUpgradeStatusArgs, + state: Partial, + collection: CustomPublishCollection, + updateProps: Partial> | undefined +): Promise { + // Prepare data for publication: + + // We know that `collection` does diffing when 'commiting' all of the changes we have made + // meaning that for anything we will call `replace()` on, we can `remove()` it first for no extra cost + + if (updateProps?.newCache !== undefined) { + state.contentCache = updateProps.newCache ?? undefined + } + + if (!state.contentCache) { + // Remove all the notes + collection.remove(null) + + return + } + + const studioBlueprintsMap = new Map() + const showStyleBlueprintsMap = new Map() + state.contentCache.Blueprints.find({}).forEach((blueprint) => { + switch (blueprint.blueprintType) { + case BlueprintManifestType.SHOWSTYLE: + showStyleBlueprintsMap.set(blueprint._id, { + _id: blueprint._id, + configPresets: blueprint.showStyleConfigPresets, + configSchema: blueprint.showStyleConfigSchema, + blueprintHash: blueprint.blueprintHash, + }) + break + case BlueprintManifestType.STUDIO: + studioBlueprintsMap.set(blueprint._id, { + _id: blueprint._id, + configPresets: blueprint.studioConfigPresets, + configSchema: blueprint.studioConfigSchema, + blueprintHash: blueprint.blueprintHash, + }) + break + // TODO - default? + } + }) + + const updateAll = !updateProps || !!updateProps?.newCache + if (updateAll) { + // Remove all the notes + collection.remove(null) + + state.contentCache.Studios.find({}).forEach((studio) => { + updateStudioUpgradeStatus(collection, studioBlueprintsMap, studio) + }) + + state.contentCache.ShowStyleBases.find({}).forEach((showStyleBase) => { + updateShowStyleUpgradeStatus(collection, showStyleBlueprintsMap, showStyleBase) + }) + } else { + const regenerateForStudioIds = new Set(updateProps.invalidateStudioIds) + const regenerateForShowStyleBaseIds = new Set(updateProps.invalidateShowStyleBaseIds) + + if (updateProps.invalidateBlueprintIds) { + // Find Studios whose blueprint triggered an invalidation + const invalidatedStudios = state.contentCache.Studios.find({ + blueprintId: { $in: updateProps.invalidateBlueprintIds }, + }) + for (const studio of invalidatedStudios) { + regenerateForStudioIds.add(studio._id) + } + + // Find ShowStyleBases whose blueprint triggered an invalidation + const invalidatedShowStyles = state.contentCache.ShowStyleBases.find({ + blueprintId: { $in: updateProps.invalidateBlueprintIds }, + }) + for (const showStyle of invalidatedShowStyles) { + regenerateForShowStyleBaseIds.add(showStyle._id) + } + } + + // Regenerate Studios + for (const studioId of regenerateForStudioIds) { + const studio = state.contentCache.Studios.findOne(studioId) + + if (studio) { + updateStudioUpgradeStatus(collection, studioBlueprintsMap, studio) + } else { + // Has already been removed + collection.remove(getDocumentId('studio', studioId)) + } + } + + // Regenerate ShowStyles + for (const showStyleBaseId of regenerateForShowStyleBaseIds) { + const showStyleBase = state.contentCache.ShowStyleBases.findOne(showStyleBaseId) + + if (showStyleBase) { + updateShowStyleUpgradeStatus(collection, showStyleBlueprintsMap, showStyleBase) + } else { + // Has already been removed + collection.remove(getDocumentId('showStyle', showStyleBaseId)) + } + } + } +} + +function updateStudioUpgradeStatus( + collection: CustomPublishCollection, + blueprintsMap: Map, + studio: Pick +) { + const status = checkDocUpgradeStatus(blueprintsMap, studio) + + collection.replace({ + ...status, + _id: getDocumentId('studio', studio._id), + documentType: 'studio', + documentId: studio._id, + name: studio.name, + }) +} + +function updateShowStyleUpgradeStatus( + collection: CustomPublishCollection, + blueprintsMap: Map, + showStyleBase: Pick +) { + const status = checkDocUpgradeStatus(blueprintsMap, showStyleBase) + + collection.replace({ + ...status, + _id: getDocumentId('showStyle', showStyleBase._id), + documentType: 'showStyle', + documentId: showStyleBase._id, + name: showStyleBase.name, + }) +} + +export async function createBlueprintUpgradeStatusSubscriptionHandle( + pub: CustomPublish +): Promise { + await setUpCollectionOptimizedObserver< + UIBlueprintUpgradeStatus, + BlueprintUpgradeStatusArgs, + BlueprintUpgradeStatusState, + BlueprintUpgradeStatusUpdateProps + >( + `pub_${PubSub.uiBlueprintUpgradeStatuses}`, + {}, + setupBlueprintUpgradeStatusPublicationObservers, + manipulateBlueprintUpgradeStatusPublicationData, + pub, + 100 + ) +} + +meteorCustomPublish( + PubSub.uiBlueprintUpgradeStatuses, + CustomCollectionName.UIBlueprintUpgradeStatuses, + async function (pub) { + const cred = await resolveCredentials({ userId: this.userId, token: undefined }) + + if (!cred || NoSecurityReadAccess.any()) { + await createBlueprintUpgradeStatusSubscriptionHandle(pub) + } else { + logger.warn(`Pub.${CustomCollectionName.UIBlueprintUpgradeStatuses}: Not allowed`) + } + } +) diff --git a/meteor/server/publications/blueprintUpgradeStatus/reactiveContentCache.ts b/meteor/server/publications/blueprintUpgradeStatus/reactiveContentCache.ts new file mode 100644 index 0000000000..cda605ba80 --- /dev/null +++ b/meteor/server/publications/blueprintUpgradeStatus/reactiveContentCache.ts @@ -0,0 +1,74 @@ +import { ReactiveCacheCollection } from '../lib/ReactiveCacheCollection' +import { literal } from '@sofie-automation/corelib/dist/lib' +import { MongoFieldSpecifierOnesStrict } from '@sofie-automation/corelib/dist/mongo' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' +import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' + +export type StudioFields = + | '_id' + | 'blueprintId' + | 'blueprintConfigPresetId' + | 'lastBlueprintConfig' + | 'blueprintConfigWithOverrides' + | 'name' +export const studioFieldSpecifier = literal>>({ + _id: 1, + blueprintId: 1, + blueprintConfigPresetId: 1, + lastBlueprintConfig: 1, + blueprintConfigWithOverrides: 1, + name: 1, +}) + +export type ShowStyleBaseFields = + | '_id' + | 'blueprintId' + | 'blueprintConfigPresetId' + | 'lastBlueprintConfig' + | 'blueprintConfigWithOverrides' + | 'name' +export const showStyleFieldSpecifier = literal< + MongoFieldSpecifierOnesStrict> +>({ + _id: 1, + blueprintId: 1, + blueprintConfigPresetId: 1, + lastBlueprintConfig: 1, + blueprintConfigWithOverrides: 1, + name: 1, +}) + +export type BlueprintFields = + | '_id' + | 'studioConfigPresets' + | 'studioConfigSchema' + | 'showStyleConfigPresets' + | 'showStyleConfigSchema' + | 'blueprintHash' + | 'blueprintType' +export const blueprintFieldSpecifier = literal>>({ + _id: 1, + studioConfigPresets: 1, + studioConfigSchema: 1, + showStyleConfigPresets: 1, + showStyleConfigSchema: 1, + blueprintHash: 1, + blueprintType: 1, +}) + +export interface ContentCache { + Studios: ReactiveCacheCollection> + ShowStyleBases: ReactiveCacheCollection> + Blueprints: ReactiveCacheCollection> +} + +export function createReactiveContentCache(): ContentCache { + const cache: ContentCache = { + Studios: new ReactiveCacheCollection>('studios'), + ShowStyleBases: new ReactiveCacheCollection>('showStyleBases'), + Blueprints: new ReactiveCacheCollection>('blueprints'), + } + + return cache +} diff --git a/meteor/server/publications/blueprintUpgradeStatus/systemStatus.ts b/meteor/server/publications/blueprintUpgradeStatus/systemStatus.ts new file mode 100644 index 0000000000..f1df572202 --- /dev/null +++ b/meteor/server/publications/blueprintUpgradeStatus/systemStatus.ts @@ -0,0 +1,86 @@ +import { createManualPromise } from '@sofie-automation/corelib/dist/lib' +import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString' +import { Meteor } from 'meteor/meteor' +import { UIBlueprintUpgradeStatus } from '../../../lib/api/upgradeStatus' +import { CustomPublish, CustomPublishChanges } from '../../lib/customPublication' +import { createBlueprintUpgradeStatusSubscriptionHandle } from './publication' + +class CustomPublishToMap }> implements CustomPublish { + #isReady = false + #documents = new Map() + #readyPromise = createManualPromise() + + get isReady(): boolean { + return this.#isReady + } + + get documents(): DBObj[] { + return Array.from(this.#documents.values()) + } + + async waitForReady(): Promise { + return this.#readyPromise + } + + /** + * Register a function to be called when the subscriber unsubscribes + */ + onStop(_callback: () => void): void { + // Ignore, this publication never stops + } + + /** + * Send the intial documents to the subscriber + */ + init(docs: DBObj[]): void { + if (this.#isReady) throw new Meteor.Error(500, 'CustomPublishToMap has already been initialised') + + for (const doc of docs) { + this.#documents.set(doc._id, doc) + } + + this.#isReady = true + + Meteor.defer(() => this.#readyPromise.manualResolve()) + } + + /** + * Send a batch of changes to the subscriber + */ + changed(changes: CustomPublishChanges): void { + if (!this.#isReady) throw new Meteor.Error(500, 'CustomPublish has not been initialised') + + for (const doc of changes.added.values()) { + this.#documents.set(doc._id, doc) + } + + for (const doc of changes.changed.values()) { + const existingDoc = this.#documents.get(doc._id) + if (!existingDoc) continue // TODO - throw? + this.#documents.set(doc._id, { + ...existingDoc, + ...doc, + }) + } + + for (const id of changes.removed.values()) { + this.#documents.delete(id) + } + } +} + +const cachedPublisher = new CustomPublishToMap() +let existingPublicationSubscription: Promise | undefined + +export async function getServerBlueprintUpgradeStatuses(): Promise { + if (Meteor.isTest) throw new Meteor.Error(500, 'getServerBlueprintUpgradeStatuses is not allowed during tests') + + if (!existingPublicationSubscription) { + existingPublicationSubscription = createBlueprintUpgradeStatusSubscriptionHandle(cachedPublisher) + } + await existingPublicationSubscription + + await cachedPublisher.waitForReady() + + return cachedPublisher.documents +} diff --git a/meteor/server/publications/blueprintUpgradeStatus/upgradesContentObserver.ts b/meteor/server/publications/blueprintUpgradeStatus/upgradesContentObserver.ts new file mode 100644 index 0000000000..c26ea657e5 --- /dev/null +++ b/meteor/server/publications/blueprintUpgradeStatus/upgradesContentObserver.ts @@ -0,0 +1,39 @@ +import { Meteor } from 'meteor/meteor' +import { logger } from '../../logging' +import { + blueprintFieldSpecifier, + ContentCache, + showStyleFieldSpecifier, + studioFieldSpecifier, +} from './reactiveContentCache' +import { Blueprints, ShowStyleBases, Studios } from '../../collections' + +export class UpgradesContentObserver { + #observers: Meteor.LiveQueryHandle[] = [] + #cache: ContentCache + + constructor(cache: ContentCache) { + logger.silly(`Creating UpgradesContentObserver`) + this.#cache = cache + + this.#observers = [ + Studios.observeChanges({}, cache.Studios.link(), { + projection: studioFieldSpecifier, + }), + ShowStyleBases.observeChanges({}, cache.ShowStyleBases.link(), { + projection: showStyleFieldSpecifier, + }), + Blueprints.observeChanges({}, cache.Blueprints.link(), { + projection: blueprintFieldSpecifier, + }), + ] + } + + public get cache(): ContentCache { + return this.#cache + } + + public stop = (): void => { + this.#observers.forEach((observer) => observer.stop()) + } +} diff --git a/meteor/server/systemStatus/__tests__/api.test.ts b/meteor/server/systemStatus/__tests__/api.test.ts index 5eee2af3d7..cc73898b28 100644 --- a/meteor/server/systemStatus/__tests__/api.test.ts +++ b/meteor/server/systemStatus/__tests__/api.test.ts @@ -6,9 +6,9 @@ import { status2ExternalStatus, setSystemStatus } from '../systemStatus' import { StatusResponse } from '../../../lib/api/systemStatus' import { StatusCode } from '@sofie-automation/blueprints-integration' import { MeteorCall } from '../../../lib/api/methods' -import { GetUpgradeStatusResult } from '../../../lib/api/migration' import { callKoaRoute } from '../../../__mocks__/koa-util' import { healthRouter } from '../api' +import { UIBlueprintUpgradeStatus } from '../../../lib/api/upgradeStatus' // we don't want the deviceTriggers observer to start up at this time jest.mock('../../api/deviceTriggers/observer') @@ -17,14 +17,9 @@ require('../api') require('../../coreSystem/index') const PackageInfo = require('../../../package.json') -import * as checkUpgradeStatus from '../../migration/upgrades/checkStatus' -jest.spyOn(checkUpgradeStatus, 'getUpgradeStatus').mockReturnValue( - Promise.resolve( - literal({ - studios: [], - showStyleBases: [], - }) - ) +import * as getServerBlueprintUpgradeStatuses from '../../publications/blueprintUpgradeStatus/systemStatus' +jest.spyOn(getServerBlueprintUpgradeStatuses, 'getServerBlueprintUpgradeStatuses').mockReturnValue( + Promise.resolve(literal([])) ) describe('systemStatus API', () => { diff --git a/meteor/server/systemStatus/__tests__/systemStatus.test.ts b/meteor/server/systemStatus/__tests__/systemStatus.test.ts index c572ba7e06..102237759c 100644 --- a/meteor/server/systemStatus/__tests__/systemStatus.test.ts +++ b/meteor/server/systemStatus/__tests__/systemStatus.test.ts @@ -10,6 +10,8 @@ import semver from 'semver' import { StatusCode } from '@sofie-automation/blueprints-integration' import { MeteorCall } from '../../../lib/api/methods' import { PeripheralDeviceStatusObject } from '@sofie-automation/shared-lib/dist/peripheralDevice/peripheralDeviceAPI' +import { PeripheralDevices } from '../../collections' +import { UIBlueprintUpgradeStatus } from '../../../lib/api/upgradeStatus' // we don't want the deviceTriggers observer to start up at this time jest.mock('../../api/deviceTriggers/observer') @@ -17,24 +19,18 @@ jest.mock('../../api/deviceTriggers/observer') require('../api') const PackageInfo = require('../../../package.json') -import * as checkUpgradeStatus from '../../migration/upgrades/checkStatus' -import { GetUpgradeStatusResult } from '../../../lib/api/migration' -import { PeripheralDevices } from '../../collections' -const getUpgradeStatusMock = jest.spyOn(checkUpgradeStatus, 'getUpgradeStatus') +import * as getServerBlueprintUpgradeStatuses from '../../publications/blueprintUpgradeStatus/systemStatus' +const getServerBlueprintUpgradeStatusesMock = jest.spyOn( + getServerBlueprintUpgradeStatuses, + 'getServerBlueprintUpgradeStatuses' +) describe('systemStatus', () => { beforeEach(() => { - getUpgradeStatusMock.mockReturnValue( - Promise.resolve( - literal({ - studios: [], - showStyleBases: [], - }) - ) - ) + getServerBlueprintUpgradeStatusesMock.mockReturnValue(Promise.resolve(literal([]))) }) afterEach(() => { - getUpgradeStatusMock.mockReset() + getServerBlueprintUpgradeStatusesMock.mockReset() }) let env: DefaultEnvironment @@ -251,28 +247,29 @@ describe('systemStatus', () => { status: status2ExternalStatus(expectedStatus), _status: expectedStatus, }) - expect(getUpgradeStatusMock).toHaveBeenCalledTimes(1) + expect(getServerBlueprintUpgradeStatusesMock).toHaveBeenCalledTimes(1) } // Fake some studio upgrade errors - getUpgradeStatusMock.mockReturnValue( + getServerBlueprintUpgradeStatusesMock.mockReturnValue( Promise.resolve( - literal({ - studios: [ - { - studioId: protectString('studio0'), - name: 'Test Studio #0', - changes: [generateTranslation('something changed')], - }, - { - studioId: protectString('studio1'), - name: 'Test Studio #1', - invalidReason: generateTranslation('some invalid reason'), - changes: [], - }, - ], - showStyleBases: [], - }) + literal([ + { + _id: protectString('studio-studio0'), + documentType: 'studio', + documentId: protectString('studio0'), + name: 'Test Studio #0', + changes: [generateTranslation('something changed')], + }, + { + _id: protectString('studio-studio1'), + documentType: 'studio', + documentId: protectString('studio1'), + name: 'Test Studio #1', + invalidReason: generateTranslation('some invalid reason'), + changes: [], + }, + ]) ) ) @@ -284,27 +281,28 @@ describe('systemStatus', () => { status: status2ExternalStatus(expectedStatus), _status: expectedStatus, }) - expect(getUpgradeStatusMock).toHaveBeenCalledTimes(2) + expect(getServerBlueprintUpgradeStatusesMock).toHaveBeenCalledTimes(2) } // Just a minor studio warning - getUpgradeStatusMock.mockReturnValue( + getServerBlueprintUpgradeStatusesMock.mockReturnValue( Promise.resolve( - literal({ - studios: [ - { - studioId: protectString('studio0'), - name: 'Test Studio #0', - changes: [generateTranslation('something changed')], - }, - { - studioId: protectString('studio1'), - name: 'Test Studio #1', - changes: [], - }, - ], - showStyleBases: [], - }) + literal([ + { + _id: protectString('studio-studio0'), + documentType: 'studio', + documentId: protectString('studio0'), + name: 'Test Studio #0', + changes: [generateTranslation('something changed')], + }, + { + _id: protectString('studio-studio1'), + documentType: 'studio', + documentId: protectString('studio1'), + name: 'Test Studio #1', + changes: [], + }, + ]) ) ) @@ -316,27 +314,28 @@ describe('systemStatus', () => { status: status2ExternalStatus(expectedStatus), _status: expectedStatus, }) - expect(getUpgradeStatusMock).toHaveBeenCalledTimes(3) + expect(getServerBlueprintUpgradeStatusesMock).toHaveBeenCalledTimes(3) } // Nothing wrong with a studio - getUpgradeStatusMock.mockReturnValue( + getServerBlueprintUpgradeStatusesMock.mockReturnValue( Promise.resolve( - literal({ - studios: [ - { - studioId: protectString('studio0'), - name: 'Test Studio #0', - changes: [], - }, - { - studioId: protectString('studio1'), - name: 'Test Studio #1', - changes: [], - }, - ], - showStyleBases: [], - }) + literal([ + { + _id: protectString('studio-studio0'), + documentType: 'studio', + documentId: protectString('studio0'), + name: 'Test Studio #0', + changes: [], + }, + { + _id: protectString('studio-studio1'), + documentType: 'studio', + documentId: protectString('studio1'), + name: 'Test Studio #1', + changes: [], + }, + ]) ) ) @@ -348,28 +347,29 @@ describe('systemStatus', () => { status: status2ExternalStatus(expectedStatus), _status: expectedStatus, }) - expect(getUpgradeStatusMock).toHaveBeenCalledTimes(4) + expect(getServerBlueprintUpgradeStatusesMock).toHaveBeenCalledTimes(4) } // Fake some showStyleBase upgrade errors - getUpgradeStatusMock.mockReturnValue( + getServerBlueprintUpgradeStatusesMock.mockReturnValue( Promise.resolve( - literal({ - studios: [], - showStyleBases: [ - { - showStyleBaseId: protectString('showStyleBase0'), - name: 'Test Show Style Base #0', - changes: [generateTranslation('something changed')], - }, - { - showStyleBaseId: protectString('showStyleBase1'), - name: 'Test Show Style Base #1', - invalidReason: generateTranslation('some invalid reason'), - changes: [], - }, - ], - }) + literal([ + { + _id: protectString('showStyle-showStyleBase0'), + documentType: 'showStyle', + documentId: protectString('showStyleBase0'), + name: 'Test Show Style Base #0', + changes: [generateTranslation('something changed')], + }, + { + _id: protectString('showStyle-showStyleBase1'), + documentType: 'showStyle', + documentId: protectString('showStyleBase1'), + name: 'Test Show Style Base #1', + invalidReason: generateTranslation('some invalid reason'), + changes: [], + }, + ]) ) ) @@ -381,27 +381,28 @@ describe('systemStatus', () => { status: status2ExternalStatus(expectedStatus), _status: expectedStatus, }) - expect(getUpgradeStatusMock).toHaveBeenCalledTimes(5) + expect(getServerBlueprintUpgradeStatusesMock).toHaveBeenCalledTimes(5) } // Just a minor showStyleBase warning - getUpgradeStatusMock.mockReturnValue( + getServerBlueprintUpgradeStatusesMock.mockReturnValue( Promise.resolve( - literal({ - studios: [], - showStyleBases: [ - { - showStyleBaseId: protectString('showStyleBase0'), - name: 'Test Show Style Base #0', - changes: [generateTranslation('something changed')], - }, - { - showStyleBaseId: protectString('showStyleBase1'), - name: 'Test Show Style Base #1', - changes: [], - }, - ], - }) + literal([ + { + _id: protectString('showStyle-showStyleBase0'), + documentType: 'showStyle', + documentId: protectString('showStyleBase0'), + name: 'Test Show Style Base #0', + changes: [generateTranslation('something changed')], + }, + { + _id: protectString('showStyle-showStyleBase1'), + documentType: 'showStyle', + documentId: protectString('showStyleBase1'), + name: 'Test Show Style Base #1', + changes: [], + }, + ]) ) ) @@ -413,27 +414,28 @@ describe('systemStatus', () => { status: status2ExternalStatus(expectedStatus), _status: expectedStatus, }) - expect(getUpgradeStatusMock).toHaveBeenCalledTimes(6) + expect(getServerBlueprintUpgradeStatusesMock).toHaveBeenCalledTimes(6) } // Nothing wrong with a showStyleBase - getUpgradeStatusMock.mockReturnValue( + getServerBlueprintUpgradeStatusesMock.mockReturnValue( Promise.resolve( - literal({ - studios: [], - showStyleBases: [ - { - showStyleBaseId: protectString('showStyleBase0'), - name: 'Test Show Style Base #0', - changes: [], - }, - { - showStyleBaseId: protectString('showStyleBase1'), - name: 'Test Show Style Base #1', - changes: [], - }, - ], - }) + literal([ + { + _id: protectString('showStyle-showStyleBase0'), + documentType: 'showStyle', + documentId: protectString('showStyleBase0'), + name: 'Test Show Style Base #0', + changes: [], + }, + { + _id: protectString('showStyle-showStyleBase1'), + documentType: 'showStyle', + documentId: protectString('showStyleBase1'), + name: 'Test Show Style Base #1', + changes: [], + }, + ]) ) ) @@ -445,7 +447,7 @@ describe('systemStatus', () => { status: status2ExternalStatus(expectedStatus), _status: expectedStatus, }) - expect(getUpgradeStatusMock).toHaveBeenCalledTimes(7) + expect(getServerBlueprintUpgradeStatusesMock).toHaveBeenCalledTimes(7) } }) }) diff --git a/meteor/server/systemStatus/blueprintUpgradeStatus.ts b/meteor/server/systemStatus/blueprintUpgradeStatus.ts new file mode 100644 index 0000000000..0506da5215 --- /dev/null +++ b/meteor/server/systemStatus/blueprintUpgradeStatus.ts @@ -0,0 +1,38 @@ +import { StatusCode } from '@sofie-automation/blueprints-integration' +import { literal } from '@sofie-automation/corelib/dist/lib' +import { Component } from '../../lib/api/systemStatus' +import { status2ExternalStatus } from './systemStatus' +import { getServerBlueprintUpgradeStatuses } from '../publications/blueprintUpgradeStatus/systemStatus' + +export async function getUpgradeSystemStatusMessages(): Promise { + const result: Component[] = [] + + const upgradeStatus = await getServerBlueprintUpgradeStatuses() + for (const statusDocument of upgradeStatus) { + let status = StatusCode.GOOD + const messages: string[] = [] + if (statusDocument.invalidReason) { + status = StatusCode.WARNING_MAJOR + messages.push('Invalid configuration') + } else if (statusDocument.changes.length) { + status = StatusCode.WARNING_MINOR + messages.push('Configuration changed') + } + + result.push( + literal({ + name: `blueprints-upgrade-${statusDocument._id}`, + status: status2ExternalStatus(status), + updated: new Date().toISOString(), + _status: status, + _internal: { + statusCodeString: StatusCode[status], + messages: messages, + versions: {}, + }, + }) + ) + } + + return result +} diff --git a/meteor/server/systemStatus/systemStatus.ts b/meteor/server/systemStatus/systemStatus.ts index c329dfa622..50c7002aa7 100644 --- a/meteor/server/systemStatus/systemStatus.ts +++ b/meteor/server/systemStatus/systemStatus.ts @@ -24,12 +24,12 @@ import { resolveCredentials, Credentials } from '../security/lib/credentials' import { SystemReadAccess } from '../security/system' import { StatusCode } from '@sofie-automation/blueprints-integration' import { PeripheralDevices, Workers, WorkerThreadStatuses } from '../collections' -import { getUpgradeSystemStatusMessages } from '../migration/upgrades' import { PeripheralDeviceId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { ServerPeripheralDeviceAPI } from '../api/peripheralDevice' import { PeripheralDeviceContentWriteAccess } from '../security/peripheralDevice' import { MethodContext } from '../../lib/api/methods' import { getBlueprintVersions } from './blueprintVersions' +import { getUpgradeSystemStatusMessages } from './blueprintUpgradeStatus' const PackageInfo = require('../../package.json') const integrationVersionRange = parseCoreIntegrationCompatabilityRange(PackageInfo.version)