Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions meteor/server/api/rest/v1/studios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ class StudiosServerAPI implements StudiosRestAPI {
_event: string,
apiStudio: APIStudio
): Promise<ClientAPI.ClientResponse<string>> {
const studioCount = await Studios.countDocuments()
if (studioCount > 0) {
return ClientAPI.responseError(UserError.create(UserErrorMessage.SystemSingleStudio, {}, 400))
}

const blueprintConfigValidation = await validateAPIBlueprintConfigForStudio(apiStudio)
checkValidation(`addStudio`, blueprintConfigValidation)

Expand Down Expand Up @@ -156,6 +161,14 @@ class StudiosServerAPI implements StudiosRestAPI {
event: string,
studioId: StudioId
): Promise<ClientAPI.ClientResponse<void>> {
const studioCount = await Studios.countDocuments()
if (studioCount === 1) {
throw new Meteor.Error(
400,
`The last studio in the system cannot be deleted (there must be at least one studio)`
)
}

const existingStudio = await Studios.findOneAsync(studioId)
if (existingStudio) {
const playlists = (await RundownPlaylists.findFetchAsync(
Expand Down
16 changes: 16 additions & 0 deletions meteor/server/api/studio/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ async function insertStudio(context: MethodContext, newId?: StudioId): Promise<S
return insertStudioInner(null, newId)
}
export async function insertStudioInner(organizationId: OrganizationId | null, newId?: StudioId): Promise<StudioId> {
const studioCount = await Studios.countDocuments()
if (studioCount > 0) {
throw new Meteor.Error(
400,
`Only one studio is supported per installation (there are currently ${studioCount})`
)
}

return Studios.insertAsync(
literal<DBStudio>({
_id: newId || getRandomId(),
Expand Down Expand Up @@ -79,6 +87,14 @@ async function removeStudio(context: MethodContext, studioId: StudioId): Promise

assertConnectionHasOneOfPermissions(context.connection, ...PERMISSIONS_FOR_MANAGE_STUDIOS)

const studioCount = await Studios.countDocuments()
if (studioCount === 1) {
throw new Meteor.Error(
400,
`The last studio in the system cannot be deleted (there must be at least one studio)`
)
}

const studio = await Studios.findOneAsync(studioId)
if (!studio) throw new Meteor.Error(404, `Studio "${studioId}" not found`)

Expand Down
15 changes: 15 additions & 0 deletions meteor/server/migration/X_X_X.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { addMigrationSteps } from './databaseMigration'
import { CURRENT_SYSTEM_VERSION } from './currentSystemVersion'
import { MongoInternals } from 'meteor/mongo'
import { Studios } from '../collections'

/*
* **************************************************************************************
Expand Down Expand Up @@ -44,4 +45,18 @@ export const addSteps = addMigrationSteps(CURRENT_SYSTEM_VERSION, [
}
},
},

{
id: 'Ensure a single studio',
canBeRunAutomatically: true,
validate: async () => {
const studioCount = await Studios.countDocuments()
if (studioCount === 0) return `No studios found`
if (studioCount > 1) return `There are ${studioCount} studios, but only one is supported`
return false
},
migrate: async () => {
// Do nothing, the user will have to resolve this manually
},
},
])
2 changes: 2 additions & 0 deletions packages/corelib/src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export enum UserErrorMessage {
IdempotencyKeyMissing = 47,
IdempotencyKeyAlreadyUsed = 48,
RateLimitExceeded = 49,
SystemSingleStudio = 50,
}

const UserErrorMessagesTranslations: { [key in UserErrorMessage]: string } = {
Expand Down Expand Up @@ -124,6 +125,7 @@ const UserErrorMessagesTranslations: { [key in UserErrorMessage]: string } = {
[UserErrorMessage.IdempotencyKeyMissing]: t(`Idempotency-Key is missing`),
[UserErrorMessage.IdempotencyKeyAlreadyUsed]: t(`Idempotency-Key is already used`),
[UserErrorMessage.RateLimitExceeded]: t(`Rate limit exceeded`),
[UserErrorMessage.SystemSingleStudio]: t(`System must have exactly one studio`),
}

export interface SerializedUserError {
Expand Down
19 changes: 13 additions & 6 deletions packages/webui/src/client/ui/Settings/SettingsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,16 @@ function SettingsMenuStudios() {
MeteorCall.studio.insertStudio().catch(catchError('studio.insertStudio'))
}, [])

// An installation should have only one studio https://github.com/Sofie-Automation/sofie-core/issues/1450
const canAddStudio = studios.length === 0
const canDeleteStudio = studios.length > 1

return (
<>
<SectionHeading title={t('Studios')} addClick={onAddStudio} />
<SectionHeading title={t('Studios')} addClick={canAddStudio ? onAddStudio : undefined} />

{studios.map((studio) => (
<SettingsMenuStudio key={unprotectString(studio._id)} studio={studio} />
<SettingsMenuStudio key={unprotectString(studio._id)} studio={studio} canDelete={canDeleteStudio} />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that after resolving migration steps, canDelete will always be false? If that's the case, does it still make sense to propagate this property rather than removing the button altogether?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The migration relies on the user to 'manually' fixup the system if there is more than 1 studio.
This button is the easiest way to do that, the other being to do it directly in mongodb.

So at this stage of being a soft-enforcmenet of 1 studio, I thought this was best to make it easy to resolve.
But yes as some point when we it is strongly enforced that there is 1 studio, then yes this should go away

))}
</>
)
Expand Down Expand Up @@ -241,8 +245,9 @@ function SettingsCollapsibleGroup({

interface SettingsMenuStudioProps {
studio: DBStudio
canDelete: boolean
}
function SettingsMenuStudio({ studio }: Readonly<SettingsMenuStudioProps>) {
function SettingsMenuStudio({ studio, canDelete }: Readonly<SettingsMenuStudioProps>) {
const { t } = useTranslation()

const onDeleteStudio = React.useCallback(
Expand Down Expand Up @@ -291,9 +296,11 @@ function SettingsMenuStudio({ studio }: Readonly<SettingsMenuStudioProps>) {
<FontAwesomeIcon icon={faExclamationTriangle} />
</button>
) : null}
<button className="action-btn" onClick={onDeleteStudio}>
<FontAwesomeIcon icon={faTrash} />
</button>
{canDelete && (
<button className="action-btn" onClick={onDeleteStudio}>
<FontAwesomeIcon icon={faTrash} />
</button>
)}
</SettingsCollapsibleGroup>
)
}
Expand Down
Loading