import { useReducer } from "react" ; import React from "react" ; import produce from "immer" ; import { AxiosError } from "axios" ; import { ServiceLogEntry } from "@garden-io/core/build/src/types/plugin/service/getServiceLogs" ; import { GraphOutput } from "@garden-io/core/build/src/commands/get/get-graph" ; import { ServiceStatus } from "@garden-io/core/build/src/types/service" ; import { ModuleConfig } from "@garden-io/core/build/src/config/module" ; import { PickFromUnion } from "@garden-io/core/build/src/util/util" ; import { ServiceConfig } from "@garden-io/core/build/src/config/service" ; import { RunStatus } from "@garden-io/core/build/src/types/plugin/base" ; import { TaskConfig } from "@garden-io/core/build/src/config/task" ; import { GetTaskResultCommandResult } from "@garden-io/core/build/src/commands/get/get-task-result" ; import { GetTestResultCommandResult } from "@garden-io/core/build/src/commands/get/get-test-result" ; import { TestConfig } from "@garden-io/core/build/src/config/test" ; import { EventName } from "@garden-io/core/build/src/events" ; import { EnvironmentStatusMap } from "@garden-io/core/build/src/types/plugin/provider/getEnvironmentStatus" ; import { isSupportedEvent , processWebSocketMessage } from "../api/ws" ; import { ServerWebsocketMessage } from "@garden-io/core/build/src/server/server" ; import { useWebsocket , useUiState } from "../hooks" ; import { ProviderMap } from "@garden-io/core/build/src/config/provider" ; import { DashboardPage } from "@garden-io/core/build/src/types/plugin/provider/getDashboardPage" ; export type SupportedEventName = PickFromUnion < EventName , "taskPending" | "taskProcessing" | "taskComplete" | "taskGraphComplete" | "taskError" | "taskCancelled" > ; export const supportedEventNames = new Set ( [ "taskPending" , "taskProcessing" , "taskComplete" , "taskGraphComplete" , "taskError" , "taskCancelled" , ] ) ; export type TaskState = PickFromUnion < SupportedEventName , "taskComplete" | "taskError" | "taskPending" | "taskProcessing" | "taskCancelled" > ; export const taskStates = [ "taskComplete" , "taskError" , "taskPending" , "taskProcessing" , "taskCancelled" ] ; export const defaultTaskState = "taskComplete" ; export const defaultServiceStatus = { state : "unknown" , detail : { } , } ; export const defaultRunStatus = { state : "outdated" , } ; export interface TestEntity { config : TestConfig & { moduleDisabled : boolean ; } ; status : RunStatus ; result : GetTestResultCommandResult ; taskState : TaskState ; } export interface TaskEntity { config : TaskConfig & { moduleDisabled : boolean ; } ; status : RunStatus ; result : GetTaskResultCommandResult ; taskState : TaskState ; } export type ModuleEntity = Pick < ModuleConfig , "name" | "type" | "path" | "repositoryUrl" | "description" | "disabled" > & { services : string [ ] ; tasks : string [ ] ; tests : string [ ] ; taskState : TaskState ; } ; export interface ServiceEntity { config : ServiceConfig & { moduleDisabled : boolean ; } ; status : ServiceStatus ; taskState : TaskState ; } export interface ProjectEntity { root : string ; taskGraphProcessing : boolean ; } export interface RequestState { pending : boolean ; initLoadComplete : boolean ; error ? : AxiosError ; } export interface Page extends DashboardPage { path : string ; } export interface ProviderPage extends Page { providerName : string ; } /**
 * The modules, services, tasks, tests, and tests entities are loaded when the app
 * is initialised and guaranteed to exist when the consumers receive the store.
 *
 * Other store data is loaded opportunistically.
 */ export interface Entities { project : { root : string ; taskGraphProcessing : boolean ; } ; modules : { [ moduleName ] : ModuleEntity ; } ; services : { [ serviceName ] : ServiceEntity ; } ; tasks : { [ taskName ] : TaskEntity ; } ; tests : { [ testKey ] : TestEntity ; } ; logs : { [ serviceName ] : ServiceLogEntry [ ] | undefined ; } ; graph : GraphOutput ; environmentStatuses : EnvironmentStatusMap ; providers : ProviderMap ; providerPages : ProviderPage [ ] ; } /**
 * The global API data store
 */ export interface Store { entities : Entities ; requestStates : { config : RequestState ; status : RequestState ; graph : RequestState ; logs : RequestState ; testResult : RequestState ; taskResult : RequestState ; } ; } export type RequestKey = keyof Store [ "requestStates" ] ; const requestKeys = [ "config" , "status" , "logs" , "testResult" , "taskResult" , "graph" ] ; type ProcessResults = ( entities : Entities ) => Entities ; interface ActionBase { type : "fetchStart" | "fetchSuccess" | "fetchFailure" | "wsMessageReceived" ; } interface ActionStart extends ActionBase { requestKey : RequestKey ; type : "fetchStart" ; } interface ActionSuccess extends ActionBase { requestKey : RequestKey ; type : "fetchSuccess" ; processResults : ProcessResults ; } interface ActionError extends ActionBase { requestKey : RequestKey ; type : "fetchFailure" ; error : AxiosError ; } interface WsMessageReceived extends ActionBase { type : "wsMessageReceived" ; processResults : ProcessResults ; } export type Action = ActionStart | ActionError | ActionSuccess | WsMessageReceived ; const initialRequestState = requestKeys . reduce ( ( acc , key ) => { acc [ key ] = { pending : false , initLoadComplete : false } ; return acc ; } , { } as { [ K in RequestKey ] : RequestState ; } ) ; const initialState = { entities : { project : { root : "" , taskGraphProcessing : false , } , modules : { } , services : { } , tasks : { } , tests : { } , logs : { } , graph : { nodes : [ ] , relationships : [ ] } , environmentStatuses : { } , providers : { } , providerPages : [ ] , } , requestStates : initialRequestState , } ; /**
 * The reducer for the useApiProvider hook. Sets the state for a given slice of the store on fetch events.
 */ const reducer = ( store , action ) => produce ( store , ( draft ) => { switch ( action . type ) { case "fetchStart" : draft . requestStates [ action . requestKey ] . pending = true ; break ; case "fetchSuccess" : draft . entities = action . processResults ( store . entities ) ; draft . requestStates [ action . requestKey ] . pending = false ; draft . requestStates [ action . requestKey ] . initLoadComplete = true ; break ; case "fetchFailure" : draft . requestStates [ action . requestKey ] . pending = false ; draft . requestStates [ action . requestKey ] . error = action . error ; break ; case "wsMessageReceived" : draft . entities = action . processResults ( store . entities ) ; break ; } } ) ; export type ApiDispatch = React . Dispatch < Action > ; type Context = { store : Store ; dispatch : ApiDispatch ; } ; export const ApiContext = React . createContext < Context > ( { } as Context ) ; /**
 * A Provider component that holds all data received from the core API and websocket connections.
 * The store and actions are accessed from components via the `useApi` function.
 */ export const ApiProvider = ( { children } ) => { const [ store , dispatch ] = useReducer ( reducer , initialState ) ; const { actions } = useUiState ( ) ; const handleWsMsg = ( wsMsg ) => { const parsedMsg = JSON . parse ( wsMsg . data ) as ServerWebsocketMessage ; if ( parsedMsg . type === "error" ) { console . error ( parsedMsg ) ; } if ( isSupportedEvent ( parsedMsg ) ) { const processResults = ( entities ) => processWebSocketMessage ( entities , parsedMsg ) ; dispatch ( { type : "wsMessageReceived" , processResults } ) ; } } ; const handleWsClosed = ( ) => { actions . showInfoBox ( "Lost connection to server. Attempting to reconnect..." ) ; } ; const handleWsOpened = ( ) => { actions . hideInfoBox ( ) ; } ; useWebsocket ( handleWsMsg , handleWsOpened , handleWsClosed ) ; return < ApiContext . Provider value = { { store , dispatch } } > { children } < /ApiContext.Provider>; } ;