diff --git a/ui/frontend/.eslintrc.js b/ui/frontend/.eslintrc.js index dc315f458..56b1de6eb 100644 --- a/ui/frontend/.eslintrc.js +++ b/ui/frontend/.eslintrc.js @@ -82,6 +82,7 @@ module.exports = { 'reducers/output/meta.ts', 'reducers/output/mir.ts', 'reducers/output/wasm.ts', + 'reducers/versions.ts', 'reducers/websocket.ts', 'websocketActions.ts', 'websocketMiddleware.ts', diff --git a/ui/frontend/.prettierignore b/ui/frontend/.prettierignore index 8fc4d87ca..10ea115b3 100644 --- a/ui/frontend/.prettierignore +++ b/ui/frontend/.prettierignore @@ -33,6 +33,7 @@ node_modules !reducers/output/meta.ts !reducers/output/mir.ts !reducers/output/wasm.ts +!reducers/versions.ts !reducers/websocket.ts !websocketActions.ts !websocketMiddleware.ts diff --git a/ui/frontend/actions.ts b/ui/frontend/actions.ts index be97f7c3e..31d27f355 100644 --- a/ui/frontend/actions.ts +++ b/ui/frontend/actions.ts @@ -27,7 +27,6 @@ import { ProcessAssembly, Position, makePosition, - Version, Crate, } from './types'; @@ -48,14 +47,7 @@ export const routes = { macroExpansion: '/macro-expansion', meta: { crates: '/meta/crates', - version: { - stable: '/meta/version/stable', - beta: '/meta/version/beta', - nightly: '/meta/version/nightly', - rustfmt: '/meta/version/rustfmt', - clippy: '/meta/version/clippy', - miri: '/meta/version/miri', - }, + versions: '/meta/versions', gistSave: '/meta/gist', gistLoad: '/meta/gist/id', }, @@ -103,8 +95,6 @@ export enum ActionType { MacroExpansionFailed = 'MACRO_EXPANSION_FAILED', RequestCratesLoad = 'REQUEST_CRATES_LOAD', CratesLoadSucceeded = 'CRATES_LOAD_SUCCEEDED', - RequestVersionsLoad = 'REQUEST_VERSIONS_LOAD', - VersionsLoadSucceeded = 'VERSIONS_LOAD_SUCCEEDED', NotificationSeen = 'NOTIFICATION_SEEN', BrowserWidthChanged = 'BROWSER_WIDTH_CHANGED', } @@ -488,42 +478,6 @@ export function performCratesLoad(): ThunkAction { }; } -const requestVersionsLoad = () => - createAction(ActionType.RequestVersionsLoad); - -const receiveVersionsLoadSuccess = ({ - stable, beta, nightly, rustfmt, clippy, miri, -}: { - stable: Version, beta: Version, nightly: Version, rustfmt: Version, clippy: Version, miri: Version, -}) => - createAction(ActionType.VersionsLoadSucceeded, { stable, beta, nightly, rustfmt, clippy, miri }); - -export function performVersionsLoad(): ThunkAction { - return function(dispatch) { - dispatch(requestVersionsLoad()); - - const stable = jsonGet(routes.meta.version.stable); - const beta = jsonGet(routes.meta.version.beta); - const nightly = jsonGet(routes.meta.version.nightly); - const rustfmt = jsonGet(routes.meta.version.rustfmt); - const clippy = jsonGet(routes.meta.version.clippy); - const miri = jsonGet(routes.meta.version.miri); - - const all = Promise.all([stable, beta, nightly, rustfmt, clippy, miri]); - - return all - .then(([stable, beta, nightly, rustfmt, clippy, miri]) => dispatch(receiveVersionsLoadSuccess({ - stable, - beta, - nightly, - rustfmt, - clippy, - miri, - }))); - // TODO: Failure case - }; -} - const notificationSeen = (notification: Notification) => createAction(ActionType.NotificationSeen, { notification }); @@ -654,8 +608,6 @@ export type Action = | ReturnType | ReturnType | ReturnType - | ReturnType - | ReturnType | ReturnType | ReturnType | ReturnType diff --git a/ui/frontend/index.tsx b/ui/frontend/index.tsx index e489ff2ec..e1fda5f26 100644 --- a/ui/frontend/index.tsx +++ b/ui/frontend/index.tsx @@ -15,7 +15,6 @@ import { selectText, addImport, performCratesLoad, - performVersionsLoad, reExecuteWithBacktrace, browserWidthChanged, } from './actions'; @@ -27,6 +26,7 @@ import { featureFlagsForceDisableAll, featureFlagsForceEnableAll } from './reduc import { disableSyncChangesToStorage } from './reducers/globalConfiguration'; import Router from './Router'; import configureStore from './configureStore'; +import { performVersionsLoad } from './reducers/versions'; const store = configureStore(window); diff --git a/ui/frontend/reducers/versions.ts b/ui/frontend/reducers/versions.ts index 75add115f..acf8bad89 100644 --- a/ui/frontend/reducers/versions.ts +++ b/ui/frontend/reducers/versions.ts @@ -1,25 +1,37 @@ -import { Action, ActionType } from '../actions'; -import { Version } from '../types'; - -const DEFAULT: State = { -}; - -export interface State { - stable?: Version; - beta?: Version; - nightly?: Version; - rustfmt?: Version; - clippy?: Version; - miri?: Version; -} - -export default function crates(state = DEFAULT, action: Action) { - switch (action.type) { - case ActionType.VersionsLoadSucceeded: { - const { stable, beta, nightly, rustfmt, clippy, miri } = action; - return { stable, beta, nightly, rustfmt, clippy, miri }; - } - default: - return state; - } -} +import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; +import * as z from 'zod'; + +import { adaptFetchError, jsonGet, routes } from '../actions'; +import { ChannelVersion } from '../types'; + +const sliceName = 'versions'; + +const initialState: State = {}; + +type State = Partial; + +const Response = z.object({ + stable: ChannelVersion, + beta: ChannelVersion, + nightly: ChannelVersion, +}); + +type Response = z.infer; + +export const performVersionsLoad = createAsyncThunk(sliceName, async () => { + const d = await adaptFetchError(() => jsonGet(routes.meta.versions)); + return Response.parseAsync(d); +}); + +const slice = createSlice({ + name: sliceName, + initialState, + reducers: {}, + extraReducers: (builder) => { + builder.addCase(performVersionsLoad.fulfilled, (state, versions) => { + Object.assign(state, versions.payload); + }); + }, +}); + +export default slice.reducer; diff --git a/ui/frontend/selectors/index.ts b/ui/frontend/selectors/index.ts index 73dfd3344..a0abe35ea 100644 --- a/ui/frontend/selectors/index.ts +++ b/ui/frontend/selectors/index.ts @@ -99,12 +99,12 @@ const LABELS: { [index in PrimaryActionCore]: string } = { export const getExecutionLabel = createSelector(primaryActionSelector, primaryAction => LABELS[primaryAction]); -const getStable = (state: State) => state.versions?.stable; -const getBeta = (state: State) => state.versions?.beta; -const getNightly = (state: State) => state.versions?.nightly; -const getRustfmt = (state: State) => state.versions?.rustfmt; -const getClippy = (state: State) => state.versions?.clippy; -const getMiri = (state: State) => state.versions?.miri; +const getStable = (state: State) => state.versions.stable?.rustc; +const getBeta = (state: State) => state.versions.beta?.rustc; +const getNightly = (state: State) => state.versions.nightly?.rustc; +const getRustfmt = (state: State) => state.versions.nightly?.rustfmt; +const getClippy = (state: State) => state.versions.nightly?.clippy; +const getMiri = (state: State) => state.versions?.nightly?.miri; const versionNumber = (v: Version | undefined) => v ? v.version : ''; export const stableVersionText = createSelector(getStable, versionNumber); diff --git a/ui/frontend/types.ts b/ui/frontend/types.ts index 50beb8850..eaf2739f6 100644 --- a/ui/frontend/types.ts +++ b/ui/frontend/types.ts @@ -1,3 +1,5 @@ +import * as z from 'zod'; + export type Page = 'index' | 'help'; export interface Position { @@ -19,11 +21,22 @@ export interface Crate { version: string; } -export interface Version { - version: string; - hash: string; - date: string; -} +export const Version = z.object({ + version: z.string(), + hash: z.string(), + date: z.string(), +}); + +export type Version = z.infer; + +export const ChannelVersion = z.object({ + rustc: Version, + rustfmt: Version, + clippy: Version, + miri: Version.optional(), +}); + +export type ChannelVersion = z.infer; export interface CommonEditorProps { code: string; diff --git a/ui/src/main.rs b/ui/src/main.rs index 870a2d106..184765b3a 100644 --- a/ui/src/main.rs +++ b/ui/src/main.rs @@ -429,6 +429,22 @@ struct MetaCratesResponse { crates: Arc<[CrateInformation]>, } +#[derive(Debug, Clone, PartialEq, Serialize)] +struct MetaVersionsResponse { + stable: MetaChannelVersionResponse, + beta: MetaChannelVersionResponse, + nightly: MetaChannelVersionResponse, +} + +#[derive(Debug, Clone, PartialEq, Serialize)] +struct MetaChannelVersionResponse { + rustc: MetaVersionResponse, + rustfmt: MetaVersionResponse, + clippy: MetaVersionResponse, + #[serde(skip_serializing_if = "Option::is_none")] + miri: Option, +} + #[derive(Debug, Clone, PartialEq, Serialize)] struct MetaVersionResponse { version: Arc, diff --git a/ui/src/metrics.rs b/ui/src/metrics.rs index 239014144..6e007b362 100644 --- a/ui/src/metrics.rs +++ b/ui/src/metrics.rs @@ -56,6 +56,7 @@ pub(crate) enum Endpoint { Clippy, MacroExpansion, MetaCrates, + MetaVersions, MetaVersionStable, MetaVersionBeta, MetaVersionNightly, diff --git a/ui/src/server_axum.rs b/ui/src/server_axum.rs index bd86b6a25..1341a4570 100644 --- a/ui/src/server_axum.rs +++ b/ui/src/server_axum.rs @@ -10,8 +10,8 @@ use crate::{ ExecuteRequest, ExecuteResponse, ExecuteSnafu, FormatRequest, FormatResponse, FormatSnafu, GhToken, GistCreationSnafu, GistLoadingSnafu, MacroExpansionRequest, MacroExpansionResponse, MacroExpansionSnafu, MetaCratesResponse, MetaGistCreateRequest, MetaGistResponse, - MetaVersionResponse, MetricsToken, MiriRequest, MiriResponse, MiriSnafu, MiriVersionSnafu, - Result, ShutdownCoordinatorSnafu, TimeoutSnafu, VersionsSnafu, + MetaVersionResponse, MetaVersionsResponse, MetricsToken, MiriRequest, MiriResponse, MiriSnafu, + MiriVersionSnafu, Result, ShutdownCoordinatorSnafu, TimeoutSnafu, VersionsSnafu, }; use async_trait::async_trait; use axum::{ @@ -76,6 +76,7 @@ pub(crate) async fn serve(config: Config) { .route("/miri", post(miri)) .route("/macro-expansion", post(macro_expansion)) .route("/meta/crates", get_or_post(meta_crates)) + .route("/meta/versions", get(meta_versions)) .route("/meta/version/stable", get_or_post(meta_version_stable)) .route("/meta/version/beta", get_or_post(meta_version_beta)) .route("/meta/version/nightly", get_or_post(meta_version_nightly)) @@ -351,6 +352,14 @@ async fn meta_crates( apply_timestamped_caching(value, if_none_match) } +async fn meta_versions( + Extension(cache): Extension>, + if_none_match: Option>, +) -> Result { + let value = track_metric_no_request_async(Endpoint::MetaVersions, || cache.versions()).await?; + apply_timestamped_caching(value, if_none_match) +} + async fn meta_version_stable( Extension(cache): Extension>, if_none_match: Option>, @@ -555,7 +564,8 @@ type Stamped = (T, SystemTime); #[derive(Debug, Default)] struct SandboxCache { crates: CacheOne, - versions: CacheOne>, + versions: CacheOne, + raw_versions: CacheOne>, } impl SandboxCache { @@ -566,10 +576,18 @@ impl SandboxCache { .await } - async fn versions(&self) -> Result>> { + async fn versions(&self) -> Result> { let coordinator = Coordinator::new_docker().await; self.versions + .fetch(|| async { Ok(coordinator.versions().await.context(VersionsSnafu)?.into()) }) + .await + } + + async fn raw_versions(&self) -> Result>> { + let coordinator = Coordinator::new_docker().await; + + self.raw_versions .fetch(|| async { Ok(Arc::new( coordinator.versions().await.context(VersionsSnafu)?, @@ -579,37 +597,37 @@ impl SandboxCache { } async fn version_stable(&self) -> Result> { - let (v, t) = self.versions().await?; + let (v, t) = self.raw_versions().await?; let v = (&v.stable.rustc).into(); Ok((v, t)) } async fn version_beta(&self) -> Result> { - let (v, t) = self.versions().await?; + let (v, t) = self.raw_versions().await?; let v = (&v.beta.rustc).into(); Ok((v, t)) } async fn version_nightly(&self) -> Result> { - let (v, t) = self.versions().await?; + let (v, t) = self.raw_versions().await?; let v = (&v.nightly.rustc).into(); Ok((v, t)) } async fn version_rustfmt(&self) -> Result> { - let (v, t) = self.versions().await?; + let (v, t) = self.raw_versions().await?; let v = (&v.nightly.rustfmt).into(); Ok((v, t)) } async fn version_clippy(&self) -> Result> { - let (v, t) = self.versions().await?; + let (v, t) = self.raw_versions().await?; let v = (&v.nightly.clippy).into(); Ok((v, t)) } async fn version_miri(&self) -> Result> { - let (v, t) = self.versions().await?; + let (v, t) = self.raw_versions().await?; let v = v.nightly.miri.as_ref().context(MiriVersionSnafu)?; let v = v.into(); Ok((v, t)) @@ -769,6 +787,41 @@ pub(crate) mod api_orchestrator_integration_impls { } } + impl From for crate::MetaVersionsResponse { + fn from(other: Versions) -> Self { + let Versions { + stable, + beta, + nightly, + } = other; + let [stable, beta, nightly] = [stable, beta, nightly].map(Into::into); + Self { + stable, + beta, + nightly, + } + } + } + + impl From for crate::MetaChannelVersionResponse { + fn from(other: ChannelVersions) -> Self { + let ChannelVersions { + rustc, + rustfmt, + clippy, + miri, + } = other; + let [rustc, rustfmt, clippy] = [rustc, rustfmt, clippy].map(|v| (&v).into()); + let miri = miri.map(|v| (&v).into()); + Self { + rustc, + rustfmt, + clippy, + miri, + } + } + } + impl From<&Version> for crate::MetaVersionResponse { fn from(other: &Version) -> Self { Self {