Revert "Revert "Rolling TexLive builds experiment"" (#28603)

* Revert "Revert "Rolling TexLive builds experiment""

* adding rolling image to allowed ones in staging (#28588)

GitOrigin-RevId: f377cacd6e4811a4bd9116c793ecbb29a7b72c33
This commit is contained in:
Jimmy Domagala-Tang
2025-09-22 08:05:53 +00:00
committed by Copybot
parent 1726d9d0a2
commit d5f11f54be
14 changed files with 172 additions and 63 deletions
@@ -513,7 +513,7 @@ const _ProjectController = {
req,
projectId
)
const allowedImageNames = ProjectHelper.getAllowedImagesForUser(user)
const imageNames = ProjectHelper.getAllowedImagesForUser(user)
const privilegeLevel =
await AuthorizationManager.promises.getPrivilegeLevelForProject(
@@ -847,7 +847,7 @@ const _ProjectController = {
maxReconnectGracefullyIntervalMs:
Settings.maxReconnectGracefullyIntervalMs,
brandVariation,
allowedImageNames,
imageNames,
gitBridgePublicBaseUrl: Settings.gitBridgePublicBaseUrl,
gitBridgeEnabled: Features.hasFeature('git-bridge'),
wsUrl,
@@ -148,11 +148,27 @@ function _addNumericSuffixToProjectName(name, allProjectNames, maxLength) {
return null
}
function getAllowedImagesForUser(user) {
const images = Settings.allowedImageNames || []
if (user?.alphaProgram) {
return images
} else {
return images.filter(image => !image.alphaOnly)
function _imageAllowed(user, image) {
if (image.alphaOnly) {
return Boolean(user?.alphaProgram)
}
if (image.monthlyExperimental) {
return Boolean(
user?.labsProgram && user.labsExperiments.includes('monthly-texlive')
)
}
return true
}
function getAllowedImagesForUser(user) {
let images = Settings.allowedImageNames || []
images = images.map(image => {
return {
...image,
allowed: _imageAllowed(user, image),
}
})
return images
}
+12 -12
View File
@@ -1,6 +1,6 @@
meta(name="ol-project_id" content=project_id)
meta(name="ol-projectName" content=projectName)
meta(name="ol-projectOwnerHasPremiumOnPageLoad" data-type="boolean" content=projectOwnerHasPremiumOnPageLoad)
meta(name="ol-projectOwnerHasPremiumOnPageLoad" content=projectOwnerHasPremiumOnPageLoad)
meta(name="ol-userSettings" data-type="json" content=userSettings)
meta(name="ol-user" data-type="json" content=user)
meta(name="ol-labsExperiments" data-type="json" content=labsExperiments)
@@ -26,16 +26,16 @@ meta(name="ol-showSymbolPalette" data-type="boolean" content=showSymbolPalette)
meta(name="ol-symbolPaletteAvailable" data-type="boolean" content=symbolPaletteAvailable)
meta(name="ol-showAiErrorAssistant" data-type="boolean" content=showAiErrorAssistant)
meta(name="ol-detachRole" data-type="string" content=detachRole)
meta(name="ol-allowedImageNames" data-type="json" content=allowedImageNames)
meta(name="ol-imageNames" data-type="json" content=imageNames)
meta(name="ol-languages" data-type="json" content=languages)
meta(name="ol-editorThemes" data-type="json" content=editorThemes)
meta(name="ol-legacyEditorThemes" data-type="json" content=legacyEditorThemes)
meta(name="ol-showUpgradePrompt" data-type="boolean" content=showUpgradePrompt)
meta(name="ol-showSupport", data-type="boolean" content=showSupport)
meta(name="ol-showTemplatesServerPro", data-type="boolean" content=showTemplatesServerPro)
meta(name="ol-hasTrackChangesFeature", data-type="boolean" content=hasTrackChangesFeature)
meta(name="ol-otMigrationStage", data-type="number" content=otMigrationStage)
meta(name="ol-inactiveTutorials", data-type="json" content=user.inactiveTutorials)
meta(name="ol-showSupport" data-type="boolean" content=showSupport)
meta(name="ol-showTemplatesServerPro" data-type="boolean" content=showTemplatesServerPro)
meta(name="ol-hasTrackChangesFeature" data-type="boolean" content=hasTrackChangesFeature)
meta(name="ol-otMigrationStage" data-type="number" content=otMigrationStage)
meta(name="ol-inactiveTutorials" data-type="json" content=user.inactiveTutorials)
meta(name="ol-projectTags" data-type="json" content=projectTags)
meta(name="ol-ro-mirror-on-client-no-local-storage" data-type="boolean" content=roMirrorOnClientNoLocalStorage)
meta(name="ol-isSaas" data-type="boolean" content=isSaas)
@@ -44,12 +44,12 @@ meta(name='ol-customerIoEnabled' data-type="boolean" content=customerIoEnabled)
meta(name='ol-compileSettings' data-type="json" content=compileSettings)
if(isOverleafAssistBundleEnabled)
//- expose plans info to show prices in paywall-change-compile-timeout test
meta(name="ol-addonPrices", data-type="json" content=addonPrices)
meta(name="ol-addonPrices" data-type="json" content=addonPrices)
// translations for the loading page, before i18n has loaded in the client
meta(name="ol-loadingText", data-type="string" content=translate("loading"))
meta(name="ol-translationIoNotLoaded", data-type="string" content=translate("could_not_connect_to_websocket_server"))
meta(name="ol-translationLoadErrorMessage", data-type="string" content=translate("could_not_load_translations"))
meta(name="ol-translationUnableToJoin", data-type="string" content=translate("could_not_connect_to_collaboration_server"))
meta(name="ol-loadingText" data-type="string" content=translate("loading"))
meta(name="ol-translationIoNotLoaded" data-type="string" content=translate("could_not_connect_to_websocket_server"))
meta(name="ol-translationLoadErrorMessage" data-type="string" content=translate("could_not_load_translations"))
meta(name="ol-translationUnableToJoin" data-type="string" content=translate("could_not_connect_to_collaboration_server"))
if (settings.overleaf != null)
meta(name="ol-overallThemes" data-type="json" content=overallThemes)
@@ -1484,6 +1484,7 @@
"revoke_invite": "",
"right": "",
"role": "",
"rolling_texlive_build": "",
"saml_auth_error": "",
"saml_identity_exists_error": "",
"saml_invalid_signature_error": "",
@@ -1778,6 +1779,7 @@
"test": "",
"test_configuration": "",
"test_configuration_successful": "",
"test_more_recent_versions_of_texlive": "",
"tex_live_version": "",
"texgpt": "",
"thank_you": "",
@@ -11,18 +11,18 @@ export default function SettingsImageName() {
const { imageName, setImageName } = useProjectSettingsContext()
const { write } = usePermissionsContext()
const allowedImageNames = useMemo(
() => getMeta('ol-allowedImageNames') || [],
[]
)
const allowedImageNames = useMemo(() => getMeta('ol-imageNames') || [], [])
const options: Array<Option> = useMemo(
() =>
allowedImageNames.map(({ imageName, imageDesc }) => ({
value: imageName,
label: imageDesc,
})),
[allowedImageNames]
allowedImageNames
// filter out images that aren't allowed, unless thats the current image the project is on
.filter(image => image.allowed || image.imageName === imageName)
.map(({ imageName, imageDesc }) => ({
value: imageName,
label: imageDesc,
})),
[allowedImageNames, imageName]
)
if (allowedImageNames.length === 0) {
@@ -11,21 +11,21 @@ export default function ImageNameSetting() {
const { t } = useTranslation()
const { write } = usePermissionsContext()
const allowedImageNames = useMemo(
() => getMeta('ol-allowedImageNames') || [],
[]
)
const imageNames = useMemo(() => getMeta('ol-imageNames') || [], [])
const options: Array<Option> = useMemo(
() =>
allowedImageNames.map(({ imageName, imageDesc }) => ({
value: imageName,
label: imageDesc,
})),
[allowedImageNames]
imageNames
// filter out images that aren't allowed, unless thats the current image the project is on
.filter(image => image.allowed || image.imageName === imageName)
.map(({ imageName, imageDesc }) => ({
value: imageName,
label: imageDesc,
})),
[imageNames, imageName]
)
if (allowedImageNames.length === 0) {
if (imageNames.length === 0) {
return null
}
@@ -0,0 +1,44 @@
import { useState } from 'react'
import LabsExperimentWidget from '../../shared/components/labs/labs-experiments-widget'
import { isInExperiment } from '@/utils/labs-utils'
import { useTranslation } from 'react-i18next'
import MaterialIcon from '@/shared/components/material-icon'
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
const MonthlyTexliveLabsWidget = ({
labsProgram,
setErrorMessage,
}: {
labsProgram: boolean
setErrorMessage: (err: string) => void
}) => {
const { t } = useTranslation()
const [optedIn, setOptedIn] = useState(isInExperiment('monthly-texlive'))
const monthlyTexLiveSplitTestEnabled = isSplitTestEnabled('monthly-texlive')
if (!monthlyTexLiveSplitTestEnabled) {
return null
}
const logo = (
<MaterialIcon
type="construction"
size="2x"
className="rounded bg-primary-subtle"
/>
)
return (
<LabsExperimentWidget
description={t('test_more_recent_versions_of_texlive')}
experimentName="monthly-texlive"
logo={logo}
labsEnabled={labsProgram}
setErrorMessage={setErrorMessage}
optedIn={optedIn}
setOptedIn={setOptedIn}
title={t('rolling_texlive_build')}
/>
)
}
export default MonthlyTexliveLabsWidget
+1 -1
View File
@@ -2,7 +2,7 @@ import getMeta from './meta'
// Should be `never` when no experiments are active. Otherwise it should be a
// union of active experiment names e.g. `'experiment1' | 'experiment2'`
export type ActiveExperiment = never
export type ActiveExperiment = 'monthly-texlive'
export const isInExperiment = (experiment: ActiveExperiment): boolean => {
const experiments = getMeta('ol-labsExperiments')
+2 -2
View File
@@ -5,7 +5,7 @@ import { UserSettings } from '../../../types/user-settings'
import { OAuthProviders } from '../../../types/oauth-providers'
import { ExposedSettings } from '../../../types/exposed-settings'
import {
type AllowedImageName,
type ImageName,
OverallThemeMeta,
type SpellCheckLanguage,
} from '../../../types/project-settings'
@@ -80,7 +80,6 @@ export interface Meta {
'ol-algolia': AlgoliaConfig | undefined
'ol-allInReconfirmNotificationPeriods': UserEmailData[]
'ol-allowedExperiments': string[]
'ol-allowedImageNames': AllowedImageName[]
'ol-anonymous': boolean
'ol-baseAssetPath': string
'ol-brandVariation': Record<string, any>
@@ -163,6 +162,7 @@ export interface Meta {
owned?: boolean
}[]
'ol-i18n': { currentLangCode: string }
'ol-imageNames': ImageName[]
'ol-inactiveTutorials': string[]
'ol-institutionEmailNonCanonical': string | undefined
'ol-institutionLinked': InstitutionLink | undefined
+2
View File
@@ -1935,6 +1935,7 @@
"right": "Right",
"ro": "Romanian",
"role": "Role",
"rolling_texlive_build": "Rolling TexLive Build",
"ru": "Russian",
"saml": "SAML",
"saml_auth_error": "Sorry, your identity provider responded with an error. Please contact your administrator for more information.",
@@ -2289,6 +2290,7 @@
"test": "Test",
"test_configuration": "Test configuration",
"test_configuration_successful": "Test configuration successful",
"test_more_recent_versions_of_texlive": "Test more recent versions of TexLive before they get turned into our annual release. This experiment adds the compiler option for a monthly build of the current TexLive version. This experimental version is primarily for testing changes to TexLive, packages, and previewing new accessibility features before theyre compiled into our annual TexLive release. Note that these images change month to month, and are untested by our team. Important projects should continue to be built on our yearly release to avoid issues that may occur when the monthly build changes.",
"tex_live_version": "TeX Live version",
"texgpt": "TeXGPT",
"text": "Text",
@@ -1,6 +1,6 @@
import EditorLeftMenu from '../../../../frontend/js/features/editor-left-menu/components/editor-left-menu'
import {
AllowedImageName,
ImageName,
OverallThemeMeta,
SpellCheckLanguage,
} from '../../../../types/project-settings'
@@ -35,20 +35,22 @@ describe('<EditorLeftMenu />', function () {
},
]
const allowedImageNames: AllowedImageName[] = [
const ImageNames: ImageName[] = [
{
imageDesc: 'Image 1',
imageName: 'img-1',
allowed: true,
},
{
imageDesc: 'Image 2',
imageName: 'img-2',
allowed: true,
},
]
beforeEach(function () {
window.metaAttributesCache.set('ol-overallThemes', overallThemes)
window.metaAttributesCache.set('ol-allowedImageNames', allowedImageNames)
window.metaAttributesCache.set('ol-imageNames', ImageNames)
window.metaAttributesCache.set('ol-anonymous', false)
window.metaAttributesCache.set('ol-gitBridgeEnabled', true)
window.metaAttributesCache.set('ol-showSupport', true)
@@ -2,24 +2,26 @@ import { screen, within, render } from '@testing-library/react'
import { expect } from 'chai'
import fetchMock from 'fetch-mock'
import SettingsImageName from '../../../../../../frontend/js/features/editor-left-menu/components/settings/settings-image-name'
import type { AllowedImageName } from '../../../../../../types/project-settings'
import type { ImageName } from '../../../../../../types/project-settings'
import { EditorLeftMenuProvider } from '@/features/editor-left-menu/components/editor-left-menu-context'
import { EditorProviders } from '../../../../helpers/editor-providers'
describe('<SettingsImageName />', function () {
const allowedImageNames: AllowedImageName[] = [
const imageNames: ImageName[] = [
{
imageDesc: 'Image 1',
imageName: 'img-1',
allowed: true,
},
{
imageDesc: 'Image 2',
imageName: 'img-2',
allowed: true,
},
]
beforeEach(function () {
window.metaAttributesCache.set('ol-allowedImageNames', allowedImageNames)
window.metaAttributesCache.set('ol-imageNames', imageNames)
})
afterEach(function () {
@@ -37,7 +39,7 @@ describe('<SettingsImageName />', function () {
const select = screen.getByLabelText('TeX Live version')
for (const { imageName, imageDesc } of allowedImageNames) {
for (const { imageName, imageDesc } of imageNames) {
const option = within(select).getByText(imageDesc)
expect(option.getAttribute('value')).to.equal(imageName)
}
@@ -4,6 +4,12 @@ const { ObjectId } = require('mongodb-legacy')
const MODULE_PATH = '../../../../app/src/Features/Project/ProjectHelper.js'
function _mapToAllowed(images) {
return images.map(image => {
return { imageName: image.imageName, allowed: image.allowed }
})
}
describe('ProjectHelper', function () {
beforeEach(function () {
this.project = {
@@ -14,6 +20,8 @@ describe('ProjectHelper', function () {
_id: '588f3ddae8ebc1bac07c9fa4',
first_name: 'bjkdsjfk',
features: {},
labsProgram: true,
labsExperiments: ['monthly-texlive'],
}
this.adminUser = {
@@ -32,6 +40,11 @@ describe('ProjectHelper', function () {
imageDesc: 'TeX Live 2020',
alphaOnly: true,
},
{
imageName: 'texlive-full:2021.1',
imageDesc: 'TeX Live 2021',
monthlyExperimental: true,
},
],
}
@@ -128,31 +141,58 @@ describe('ProjectHelper', function () {
})
describe('getAllowedImagesForUser', function () {
it('filters out alpha-only images when the user is anonymous', function () {
it('marks alpha only images as not allowed when the user is anonymous', function () {
const images = this.ProjectHelper.getAllowedImagesForUser(null)
const imageNames = images.map(image => image.imageName)
const imageNames = _mapToAllowed(images)
expect(imageNames).to.deep.equal([
'texlive-full:2018.1',
'texlive-full:2019.1',
{ imageName: 'texlive-full:2018.1', allowed: true },
{ imageName: 'texlive-full:2019.1', allowed: true },
{ imageName: 'texlive-full:2020.1', allowed: false },
{ imageName: 'texlive-full:2021.1', allowed: false },
])
})
it('filters out alpha-only images when the user is not admin', function () {
const images = this.ProjectHelper.getAllowedImagesForUser(this.user)
const imageNames = images.map(image => image.imageName)
it('marks monthly labs images as not allowed when the user is anonymous', function () {
const images = this.ProjectHelper.getAllowedImagesForUser(null)
const imageNames = _mapToAllowed(images)
expect(imageNames).to.deep.equal([
'texlive-full:2018.1',
'texlive-full:2019.1',
{ imageName: 'texlive-full:2018.1', allowed: true },
{ imageName: 'texlive-full:2019.1', allowed: true },
{ imageName: 'texlive-full:2020.1', allowed: false },
{ imageName: 'texlive-full:2021.1', allowed: false },
])
})
it('marks monthly labs images as allowed when the user is enrolled', function () {
const images = this.ProjectHelper.getAllowedImagesForUser(this.user)
const imageNames = _mapToAllowed(images)
expect(imageNames).to.deep.equal([
{ imageName: 'texlive-full:2018.1', allowed: true },
{ imageName: 'texlive-full:2019.1', allowed: true },
{ imageName: 'texlive-full:2020.1', allowed: false },
{ imageName: 'texlive-full:2021.1', allowed: true },
])
})
it('marks alpha only images as not allowed when when the user is not admin', function () {
const images = this.ProjectHelper.getAllowedImagesForUser(this.user)
const imageNames = _mapToAllowed(images)
expect(imageNames).to.deep.equal([
{ imageName: 'texlive-full:2018.1', allowed: true },
{ imageName: 'texlive-full:2019.1', allowed: true },
{ imageName: 'texlive-full:2020.1', allowed: false },
{ imageName: 'texlive-full:2021.1', allowed: true },
])
})
it('returns all images when the user is admin', function () {
const images = this.ProjectHelper.getAllowedImagesForUser(this.adminUser)
const imageNames = images.map(image => image.imageName)
const imageNames = _mapToAllowed(images)
expect(imageNames).to.deep.equal([
'texlive-full:2018.1',
'texlive-full:2019.1',
'texlive-full:2020.1',
{ imageName: 'texlive-full:2018.1', allowed: true },
{ imageName: 'texlive-full:2019.1', allowed: true },
{ imageName: 'texlive-full:2020.1', allowed: true },
{ imageName: 'texlive-full:2021.1', allowed: false },
])
})
})
+2 -1
View File
@@ -1,9 +1,10 @@
import { Brand } from './helpers/brand'
import { OverallTheme } from '@/shared/utils/styles'
export type AllowedImageName = {
export type ImageName = {
imageDesc: string
imageName: string
allowed: boolean
}
export type DocId = Brand<string, 'DocId'>