Skip to content

Commit

Permalink
feat: Use feature flag for streaming updates - manually update projec…
Browse files Browse the repository at this point in the history
…t store (#9170)
  • Loading branch information
gt2345 authored Apr 18, 2024
1 parent dd7f4b5 commit 72344e0
Show file tree
Hide file tree
Showing 8 changed files with 105 additions and 33 deletions.
6 changes: 4 additions & 2 deletions webui/react/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import PageMessage from 'components/PageMessage';
import Router from 'components/Router';
import useUI, { Mode, ThemeProvider } from 'components/ThemeProvider';
import useAuthCheck from 'hooks/useAuthCheck';
import useFeature from 'hooks/useFeature';
import useKeyTracker from 'hooks/useKeyTracker';
import usePageVisibility from 'hooks/usePageVisibility';
import usePermissions from 'hooks/usePermissions';
Expand Down Expand Up @@ -61,6 +62,7 @@ const AppView: React.FC = () => {
const settings = useObservable(themeSetting);
const [isSettingsReady, setIsSettingsReady] = useState(false);
const { ui, actions: uiActions, theme, isDarkMode } = useUI();
const streamingUpdatesOn = useFeature().isOn('streaming_updates');

useEffect(() => {
if (isServerReachable) checkAuth();
Expand All @@ -71,8 +73,8 @@ const AppView: React.FC = () => {
useRouteTracker();

useEffect(() => {
return streamStore.on(projectStore);
}, []);
return streamingUpdatesOn ? streamStore.on(projectStore) : streamStore.off('projects');
}, [streamingUpdatesOn]);

useEffect(() => (isAuthenticated ? userStore.fetchCurrentUser() : undefined), [isAuthenticated]);
useEffect(() => (isAuthenticated ? clusterStore.startPolling() : undefined), [isAuthenticated]);
Expand Down
4 changes: 2 additions & 2 deletions webui/react/src/components/ProjectActionDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ interface Props {
direction?: 'vertical' | 'horizontal';
isContextMenu?: boolean;
onDelete?: () => void;
onEdit?: (name: string, archived: boolean) => void;
onEdit?: (name: string, archived: boolean, description?: string) => void;
onMove?: () => void;
project: Project;
showChildrenIfEmpty?: boolean;
Expand All @@ -29,7 +29,7 @@ interface Props {

interface ProjectMenuPropsIn {
onDelete?: () => void;
onEdit?: (name: string, archived: boolean) => void;
onEdit?: (name: string, archived: boolean, description?: string) => void;
onMove?: () => void;
project?: Project;
workspaceArchived?: boolean;
Expand Down
2 changes: 1 addition & 1 deletion webui/react/src/components/ProjectCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import css from './ProjectCard.module.scss';

interface Props {
hideActionMenu?: boolean;
onEdit?: (name: string, archived: boolean) => void;
onEdit?: (name: string, archived: boolean, description?: string) => void;
onRemove?: () => void;
project: Project;
showWorkspace?: boolean;
Expand Down
4 changes: 2 additions & 2 deletions webui/react/src/components/ProjectEditModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ interface FormInputs {
}

interface Props {
onEdit?: (name: string, archived: boolean) => void;
onEdit?: (name: string, archived: boolean, description?: string) => void;
project: Project;
}

Expand All @@ -31,7 +31,7 @@ const ProjectEditModalComponent: React.FC<Props> = ({ onEdit, project }: Props)

try {
await patchProject({ description, id: project.id, name });
onEdit?.(name, project.archived);
onEdit?.(name, project.archived, description);
} catch (e) {
handleError(e, {
level: ErrorLevel.Error,
Expand Down
12 changes: 11 additions & 1 deletion webui/react/src/hooks/useFeature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import determinedStore, { DeterminedInfo } from 'stores/determinedInfo';
import userSettings from 'stores/userSettings';

// add new feature switches here
export type ValidFeature = 'explist_v2' | 'rp_binding' | 'genai' | 'flat_runs';
export type ValidFeature =
| 'explist_v2'
| 'rp_binding'
| 'genai'
| 'flat_runs'
| 'streaming_updates';

type FeatureDescription = {
friendlyName: string;
Expand Down Expand Up @@ -37,6 +42,11 @@ export const FEATURES: Record<ValidFeature, FeatureDescription> = {
description: 'Allow resource pools to be assigned to workspaces',
friendlyName: 'Resource Pool Binding',
},
streaming_updates: {
defaultValue: false,
description: 'Allow streaming updates through websockets for better performance',
friendlyName: 'Streaming Updates',
},
} as const;
export const FEATURE_SETTINGS_PATH = 'global-features';

Expand Down
92 changes: 73 additions & 19 deletions webui/react/src/pages/WorkspaceDetails/WorkspaceProjects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
stateRenderer,
userRenderer,
} from 'components/Table/Table';
import useFeature from 'hooks/useFeature';
import usePermissions from 'hooks/usePermissions';
import usePrevious from 'hooks/usePrevious';
import { useSettings } from 'hooks/useSettings';
Expand Down Expand Up @@ -67,6 +68,7 @@ const WorkspaceProjects: React.FC<Props> = ({ workspace, id, pageRef }) => {
const ProjectCreateModal = useModal(ProjectCreateModalComponent);
const config = useMemo(() => configForWorkspace(id), [id]);
const { settings, updateSettings } = useSettings<WorkspaceDetailsSettings>(config);
const streamingUpdatesOn = useFeature().isOn('streaming_updates');

const loadableProjects: Loadable<List<Project>> = useObservable(
projectStore.getProjectsByWorkspace(id),
Expand Down Expand Up @@ -158,27 +160,65 @@ const WorkspaceProjects: React.FC<Props> = ({ workspace, id, pageRef }) => {
}
}, [currentUser, loadableUsers, prevWhose, settings.whose, updateSettings, users]);

const saveProjectDescription = useCallback(async (newDescription: string, projectId: number) => {
try {
await patchProject({ description: newDescription, id: projectId });
} catch (e) {
handleError(e, {
level: ErrorLevel.Error,
publicMessage: 'Please try again later.',
publicSubject: 'Unable to edit project.',
silent: false,
type: ErrorType.Server,
});
}
}, []);
const onEdit = useCallback(
(projectId: number, name: string, archived: boolean, description?: string) => {
if (!streamingUpdatesOn) {
const project = projects.find((p) => p.id === projectId);
project &&
projectStore.upsertProject({
...project,
archived,
description: description || project?.description,
name,
} as Project);
}
},
[streamingUpdatesOn, projects],
);

const onRemove = useCallback(
(projectId: number) => {
if (!streamingUpdatesOn) {
projectStore.delete(projectId);
}
},
[streamingUpdatesOn],
);

const saveProjectDescription = useCallback(
async (newDescription: string, projectId: number) => {
try {
await patchProject({ description: newDescription, id: projectId });
const project = projects.find((p) => p.id === projectId);
project && projectStore.upsertProject({ ...project, description: newDescription });
} catch (e) {
handleError(e, {
level: ErrorLevel.Error,
publicMessage: 'Please try again later.',
publicSubject: 'Unable to edit project.',
silent: false,
type: ErrorType.Server,
});
}
},
[projects],
);

const columns = useMemo(() => {
const projectNameRenderer = (value: string, record: Project) => (
<Link path={paths.projectDetails(record.id)}>{value}</Link>
);

const actionRenderer: GenericRenderer<Project> = (_, record) => (
<ProjectActionDropdown project={record} workspaceArchived={workspace?.archived} />
<ProjectActionDropdown
project={record}
workspaceArchived={workspace?.archived}
onDelete={() => onRemove(record.id)}
onEdit={(name: string, archived: boolean, description?: string) =>
onEdit(record.id, name, archived, description)
}
onMove={() => onRemove(record.id)}
/>
);

const descriptionRenderer = (value: string, record: Project) => (
Expand Down Expand Up @@ -262,7 +302,7 @@ const WorkspaceProjects: React.FC<Props> = ({ workspace, id, pageRef }) => {
title: '',
},
] as ColumnDef<Project>[];
}, [saveProjectDescription, workspace?.archived, users]);
}, [saveProjectDescription, workspace?.archived, users, onEdit, onRemove]);

const switchShowArchived = useCallback(
(showArchived: boolean) => {
Expand Down Expand Up @@ -303,12 +343,20 @@ const WorkspaceProjects: React.FC<Props> = ({ workspace, id, pageRef }) => {

const actionDropdown = useCallback(
({ record, children }: { children: React.ReactNode; record: Project }) => (
<ProjectActionDropdown isContextMenu project={record} workspaceArchived={workspace?.archived}>
<ProjectActionDropdown
isContextMenu
project={record}
workspaceArchived={workspace?.archived}
onDelete={() => onRemove(record.id)}
onEdit={(name: string, archived: boolean, description?: string) =>
onEdit(record.id, name, archived, description)
}
onMove={() => onRemove(record.id)}>
{children}
</ProjectActionDropdown>
),
// eslint-disable-next-line react-hooks/exhaustive-deps
[workspace?.archived],
[workspace?.archived, onEdit, onRemove],
);

const projectsList = useMemo(() => {
Expand All @@ -323,6 +371,10 @@ const WorkspaceProjects: React.FC<Props> = ({ workspace, id, pageRef }) => {
key={project.id}
project={project}
workspaceArchived={workspace?.archived}
onEdit={(name: string, archived: boolean, description?: string) =>
onEdit(project.id, name, archived, description)
}
onRemove={() => onRemove(project.id)}
/>
))}
</Card.Group>
Expand Down Expand Up @@ -359,11 +411,13 @@ const WorkspaceProjects: React.FC<Props> = ({ workspace, id, pageRef }) => {
settings,
updateSettings,
workspace?.archived,
onEdit,
onRemove,
]);

useEffect(() => {
projectStore.fetch(id);
}, [id]);
projectStore.fetch(id, canceler.signal, true);
}, [id, canceler]);

useEffect(() => {
return () => canceler.abort();
Expand Down
7 changes: 5 additions & 2 deletions webui/react/src/stores/projects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ export class ProjectStore implements StreamSubscriber {
}
}

public upsert(content: StreamContent): void {
this.upsertProject(mapStreamProject(content));
}

#upsert(p: Project, np: Project): Project {
p.name = np.name;
p.description = np.description;
Expand All @@ -98,8 +102,7 @@ export class ProjectStore implements StreamSubscriber {
return { ...p };
}

public upsert(content: StreamContent): void {
const p = mapStreamProject(content);
public upsertProject(p: Project): void {
let prevProjectWorkspaceId: number | undefined;

this.#projects.update((prev) =>
Expand Down
11 changes: 7 additions & 4 deletions webui/react/src/stores/stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ export interface StreamSubscriber {
}

class StreamStore {
#stream: Stream;
#stream?: Stream = undefined;
#subscribers: Partial<Record<Streamable, StreamSubscriber>> = {};

constructor() {
#opensocket() {
const socketUrl = new URL(serverAddress());
socketUrl.pathname = 'stream';
socketUrl.protocol = socketUrl.protocol.replace('http', 'ws');
Expand All @@ -41,22 +41,25 @@ class StreamStore {
}

public on(sub: StreamSubscriber): () => void {
if (!this.#stream) this.#opensocket();

this.#subscribers[sub.id()] = sub;

return () => this.#closeSocket.bind(this);
}

public off(key: Streamable) {
this.#closeSocket();
delete this.#subscribers[key];
}

public emit(spec: StreamSpec, id?: string) {
this.#stream.subscribe(spec, id);
this.#stream?.subscribe(spec, id);
}

#closeSocket() {
this.#subscribers = {};
this.#stream.close();
this.#stream?.close();
}
}

Expand Down

0 comments on commit 72344e0

Please sign in to comment.