Skip to content

Commit 5f9956f

Browse files
committed
make feature flags persistent and add change callback
add flag to pull language directory dynamically
1 parent 2b73d75 commit 5f9956f

File tree

18 files changed

+193
-16
lines changed

18 files changed

+193
-16
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
"js-slang": "^1.0.85",
6868
"js-yaml": "^4.1.0",
6969
"konva": "^9.2.0",
70-
"language-directory": "https://github.com/source-academy/language-directory.git",
70+
"language-directory": "https://github.com/source-academy/language-directory.git#0.0.4",
7171
"lodash": "^4.17.21",
7272
"lz-string": "^1.4.4",
7373
"mdast-util-from-markdown": "^2.0.0",

src/commons/application/ApplicationTypes.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { PlaybackStatus, RecordingStatus } from '../../features/sourceRecorder/S
88
import type { StoriesEnvState, StoriesState } from '../../features/stories/StoriesTypes';
99
import { freshSortState } from '../../pages/academy/grading/subcomponents/GradingSubmissionsTable';
1010
import { WORKSPACE_BASE_PATHS } from '../../pages/fileSystem/createInBrowserFileSystem';
11+
import { getSupportedLanguages, ILanguageDefinition } from '../directory/language';
1112
import { defaultFeatureFlags, FeatureFlagsState } from '../featureFlags';
1213
import type { FileSystemState } from '../fileSystem/FileSystemTypes';
1314
import type { SideContentManagerState, SideContentState } from '../sideContent/SideContentTypes';
@@ -369,9 +370,16 @@ const getDefaultLanguageConfig = (): SALanguage => {
369370
};
370371
export const defaultLanguageConfig: SALanguage = getDefaultLanguageConfig();
371372

373+
export const defaultConductorLanguage: ILanguageDefinition = getSupportedLanguages()[0] || {
374+
id: '',
375+
name: '',
376+
evaluators: []
377+
};
378+
372379
export const defaultPlayground: PlaygroundState = {
373380
githubSaveInfo: { repoName: '', filePath: '' },
374-
languageConfig: defaultLanguageConfig
381+
languageConfig: defaultLanguageConfig,
382+
conductorLanguage: defaultConductorLanguage
375383
};
376384

377385
export const defaultEditorValue = '// Type your program in here!';

src/commons/controlBar/ControlBarChapterSelect.tsx

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import { IconNames } from '@blueprintjs/icons';
33
import { ItemListRenderer, ItemRenderer, Select } from '@blueprintjs/select';
44
import { Chapter, Variant } from 'js-slang/dist/types';
55
import React from 'react';
6+
import { useDispatch } from 'react-redux';
7+
import { playgroundConductorEvaluator } from 'src/features/playground/PlaygroundActions';
68

9+
import { flagConductorEnable } from '../../features/conductor/flagConductorEnable';
710
import {
811
fullJSLanguage,
912
fullTSLanguage,
@@ -15,6 +18,8 @@ import {
1518
sourceLanguages,
1619
styliseSublanguage
1720
} from '../application/ApplicationTypes';
21+
import { IEvaluatorDefinition } from '../directory/language';
22+
import { useFeature } from '../featureFlags/useFeature';
1823
import Constants from '../utils/Constants';
1924
import { useTypedSelector } from '../utils/Hooks';
2025

@@ -69,7 +74,24 @@ const chapterRenderer: (isFolderModeEnabled: boolean) => ItemRenderer<SALanguage
6974
);
7075
};
7176

77+
const evaluatorListRenderer: ItemListRenderer<IEvaluatorDefinition> = ({
78+
itemsParentRef,
79+
renderItem,
80+
items
81+
}) => {
82+
return (
83+
<Menu ulRef={itemsParentRef} style={{ display: 'flex', flexDirection: 'column' }}>
84+
{items.map(renderItem)}
85+
</Menu>
86+
);
87+
};
88+
89+
const evaluatorRenderer: ItemRenderer<IEvaluatorDefinition> = (evaluator, { handleClick }) => {
90+
return <MenuItem onClick={handleClick} text={evaluator.name} />;
91+
};
92+
7293
const ChapterSelectComponent = Select.ofType<SALanguage>();
94+
const EvaluatorSelectComponent = Select<IEvaluatorDefinition>;
7395

7496
export const ControlBarChapterSelect: React.FC<ControlBarChapterSelectProps> = ({
7597
isFolderModeEnabled,
@@ -80,6 +102,34 @@ export const ControlBarChapterSelect: React.FC<ControlBarChapterSelectProps> = (
80102
}) => {
81103
const selectedLang = useTypedSelector(store => store.playground.languageConfig.mainLanguage);
82104

105+
const currentLang = useTypedSelector(store => store.playground.conductorLanguage);
106+
const currentEval = useTypedSelector(store => store.playground.conductorEvaluator);
107+
const dispatch = useDispatch();
108+
109+
const conductorEnabled = useFeature(flagConductorEnable);
110+
if (conductorEnabled) {
111+
const handleChapterSelect = (evaluator: IEvaluatorDefinition) => {
112+
dispatch(playgroundConductorEvaluator(evaluator));
113+
};
114+
return (
115+
<EvaluatorSelectComponent
116+
items={currentLang.evaluators}
117+
onItemSelect={handleChapterSelect}
118+
itemRenderer={evaluatorRenderer}
119+
itemListRenderer={evaluatorListRenderer}
120+
filterable={false}
121+
disabled={disabled}
122+
>
123+
<Button
124+
minimal
125+
text={currentEval?.name}
126+
rightIcon={disabled ? null : IconNames.DOUBLE_CARET_VERTICAL}
127+
disabled={disabled}
128+
/>
129+
</EvaluatorSelectComponent>
130+
);
131+
}
132+
83133
const choices = [
84134
...sourceLanguages,
85135
// Full JS/TS version uses eval(), which is a huge security risk, so we only enable
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { createFeatureFlag } from '../../commons/featureFlags';
2+
import { featureSelector } from '../../commons/featureFlags/featureSelector';
3+
import { updateSupportedLanguages } from './language';
4+
5+
export const flagLangDirUrl = createFeatureFlag(
6+
'langdir.url',
7+
'https://source-academy.github.io/language-directory/directory.json',
8+
'The URL where Source Academy may find the language directory.',
9+
newUrl => updateSupportedLanguages(newUrl)
10+
);
11+
12+
export const selectLangDirUrl = featureSelector(flagLangDirUrl);

src/commons/directory/language.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { ILanguageDefinition } from 'language-directory/dist/types';
2+
export type { IEvaluatorDefinition, ILanguageDefinition } from 'language-directory/dist/types';
3+
import { generateLanguageMap, getLanguageDefinition } from 'language-directory/dist/util';
4+
export { getEvaluatorDefinition } from 'language-directory/dist/util';
5+
6+
let languages: ILanguageDefinition[] = [];
7+
let languageMap = generateLanguageMap(languages);
8+
9+
export async function updateSupportedLanguages(url: string) {
10+
const response = await fetch(url);
11+
if (!response.ok) {
12+
throw new Error(`Can't retrieve language directory: ${response.status}`);
13+
}
14+
const result = (await response.json()) as ILanguageDefinition[];
15+
languages = result;
16+
languageMap = generateLanguageMap(languages);
17+
}
18+
19+
export function getSupportedLanguages() {
20+
return languages;
21+
}
22+
23+
export function getSupportedLanguageDefinition(languageId: string) {
24+
return getLanguageDefinition(languageMap, languageId);
25+
}

src/commons/featureFlags/FeatureFlag.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export class FeatureFlag<T> {
22
private readonly _flagName: string;
33
private readonly _defaultValue: T;
44
private readonly _flagDesc?: string;
5+
private readonly _callback?: Function;
56

67
get flagName(): string {
78
return this._flagName;
@@ -12,10 +13,20 @@ export class FeatureFlag<T> {
1213
get flagDesc(): string | undefined {
1314
return this._flagDesc;
1415
}
16+
onChange(newValue: T) {
17+
return this._callback?.(newValue);
18+
}
1519

16-
constructor(flagName: string, defaultValue: T, flagDesc?: string) {
20+
constructor(
21+
flagName: string,
22+
defaultValue: T,
23+
flagDesc?: string,
24+
callback?: (newValue: T) => void
25+
) {
1726
this._flagName = flagName;
1827
this._defaultValue = defaultValue;
1928
this._flagDesc = flagDesc;
29+
this._callback = callback;
30+
this.onChange(defaultValue);
2031
}
2132
}

src/commons/featureFlags/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ const featureFlagsSlice = createSlice({
1919
action: { payload: { featureFlag: FeatureFlag<T>; value: T } }
2020
) {
2121
state.modifiedFlags[action.payload.featureFlag.flagName] = action.payload.value;
22+
action.payload.featureFlag.onChange(action.payload.value);
2223
},
2324
resetFlag<T>(state: FeatureFlagsState, action: { payload: { featureFlag: FeatureFlag<T> } }) {
2425
delete state.modifiedFlags[action.payload.featureFlag.flagName];
26+
action.payload.featureFlag.onChange(action.payload.featureFlag.defaultValue);
2527
}
2628
}
2729
});
@@ -33,7 +35,8 @@ export const FeatureFlagsReducer = featureFlagsSlice.reducer;
3335
export function createFeatureFlag<T>(
3436
flagName: string,
3537
defaultValue: T,
36-
flagDesc?: string
38+
flagDesc?: string,
39+
callback?: (newValue: T) => void
3740
): FeatureFlag<T> {
38-
return new FeatureFlag<T>(flagName, defaultValue, flagDesc);
41+
return new FeatureFlag<T>(flagName, defaultValue, flagDesc, callback);
3942
}
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { flagConductorEnable } from '../../features/conductor/flagConductorEnable';
22
import { flagConductorEvaluatorUrl } from '../../features/conductor/flagConductorEvaluatorUrl';
3+
import { flagLangDirUrl } from '../directory/flagLangDirUrl';
34
import { FeatureFlag } from './FeatureFlag';
45

5-
export const publicFlags: FeatureFlag<any>[] = [flagConductorEnable, flagConductorEvaluatorUrl];
6+
export const publicFlags: FeatureFlag<any>[] = [
7+
flagConductorEnable,
8+
flagConductorEvaluatorUrl,
9+
flagLangDirUrl
10+
];

src/commons/navigationBar/subcomponents/NavigationBarLangSelectButton.tsx

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,17 @@ import {
1212
SUPPORTED_LANGUAGES,
1313
SupportedLanguage
1414
} from 'src/commons/application/ApplicationTypes';
15+
import { getSupportedLanguages, ILanguageDefinition } from 'src/commons/directory/language';
16+
import { useFeature } from 'src/commons/featureFlags/useFeature';
1517
import SimpleDropdown from 'src/commons/SimpleDropdown';
1618
import { useTypedSelector } from 'src/commons/utils/Hooks';
1719
import WorkspaceActions from 'src/commons/workspace/WorkspaceActions';
18-
import { playgroundConfigLanguage } from 'src/features/playground/PlaygroundActions';
20+
import { flagConductorEnable } from 'src/features/conductor/flagConductorEnable';
21+
import {
22+
playgroundConductorEvaluator,
23+
playgroundConductorLanguage,
24+
playgroundConfigLanguage
25+
} from 'src/features/playground/PlaygroundActions';
1926

2027
// TODO: Hardcoded to use the first sublanguage for each language
2128
const defaultSublanguages: {
@@ -32,6 +39,31 @@ const NavigationBarLangSelectButton = () => {
3239
const [isOpen, setIsOpen] = useState(false);
3340
const lang = useTypedSelector(store => store.playground.languageConfig.mainLanguage);
3441
const dispatch = useDispatch();
42+
43+
const conductorEnabled = useFeature(flagConductorEnable);
44+
const currentLang = useTypedSelector(store => store.playground.conductorLanguage);
45+
if (conductorEnabled) {
46+
const languages = getSupportedLanguages();
47+
const selectLang = (language: ILanguageDefinition) => {
48+
dispatch(playgroundConductorLanguage(language));
49+
dispatch(playgroundConductorEvaluator(language.evaluators[0]));
50+
setIsOpen(false);
51+
};
52+
return (
53+
<SimpleDropdown
54+
options={languages.map(lang => ({ value: lang, label: lang.name }))}
55+
onClick={selectLang}
56+
selectedValue={currentLang}
57+
popoverProps={{ position: Position.BOTTOM_RIGHT, onClose: () => setIsOpen(false), isOpen }}
58+
buttonProps={{
59+
rightIcon: 'caret-down',
60+
onClick: () => setIsOpen(true),
61+
'data-testid': 'NavigationBarLangSelectButton'
62+
}}
63+
/>
64+
);
65+
}
66+
3567
const selectLang = (language: SupportedLanguage) => {
3668
const { chapter, variant } = defaultSublanguages[language];
3769
dispatch(playgroundConfigLanguage(getLanguageConfig(chapter, variant)));

src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { call, cancel, cancelled, fork, put, race, select, take } from 'redux-sa
1111
import * as Sourceror from 'sourceror';
1212

1313
import InterpreterActions from '../../../../commons/application/actions/InterpreterActions';
14+
import { IEvaluatorDefinition } from '../../../../commons/directory/language';
1415
import { selectFeatureSaga } from '../../../../commons/featureFlags/selectFeatureSaga';
1516
import { makeCCompilerConfig, specialCReturnObject } from '../../../../commons/utils/CToWasmHelper';
1617
import { javaRun } from '../../../../commons/utils/JavaHelper';
@@ -463,9 +464,13 @@ export function* evalCodeConductorSaga(
463464
actionType: string,
464465
storyEnv?: string
465466
): SagaIterator {
467+
const evaluator: IEvaluatorDefinition | undefined = yield select(
468+
(state: OverallState) => state.playground.conductorEvaluator
469+
);
470+
if (!evaluator?.path) throw Error('no evaluator');
466471
const evaluatorResponse: Response = yield call(
467472
fetch,
468-
yield call(selectFeatureSaga, flagConductorEvaluatorUrl) // temporary evaluator
473+
evaluator.path // temporary evaluator
469474
);
470475
if (!evaluatorResponse.ok) throw Error("can't get evaluator");
471476
const evaluatorBlob: Blob = yield call([evaluatorResponse, 'blob']);

0 commit comments

Comments
 (0)