Last active
December 5, 2022 08:40
-
-
Save NoriSte/f85fb95befa3cf1e26b6ea2e22375979 to your computer and use it in GitHub Desktop.
OpenTelemetry Toggle logics with a React hook and with XState (they can be not totally aligned)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// --------------------------------------------------------------- | |
// CONTEXT | |
export type Context = { | |
// The current status of OpenTelemetry | |
status: 'enabled' | 'disabled'; | |
// Whether the current metadata contains the configuration or not | |
metadataContainsConfig: boolean; | |
// Whether the current form state contains valid configuration or not | |
configFormIsValid: boolean; | |
// External callbacks | |
onEnable: () => Promise<void>; | |
onDisable: () => Promise<void>; | |
}; | |
// If the feature is already enabled, it means that metadata contains a valid configuration. It's | |
// not possible the metadata contains an invalid configuration since the server prevents it. | |
export type EnabledContext = { | |
status: 'enabled'; | |
metadataContainsConfig: true; | |
}; | |
// When Disabled, the metadata can contain a configuration or not yet. | |
export type DisabledContext = { | |
status: 'disabled'; | |
}; | |
// When the metadata does not contain a configuration, the users need to fill up the form before | |
// enabling OpenTelemetry. This can happen only the first time the feature is enabled because then | |
// the configuration cannot be removed/reset. | |
export type PleaseFillTheFormContext = { | |
status: 'disabled'; | |
configFormIsValid: false; | |
metadataContainsConfig: false; | |
}; | |
// When the metadata does not contain a configuration but fhe form contains is, the users need to | |
// submit the form before enabling OpenTelemetry. This can happen only the first time the feature is | |
// enabled because then the configuration cannot be removed/reset. | |
export type PleaseSubmitTheFormContext = { | |
status: 'disabled'; | |
configFormIsValid: true; | |
metadataContainsConfig: false; | |
}; | |
type CanBeEnabled = | |
| { configFormIsValid: true } | |
| { metadataContainsConfig: true }; | |
export type EnablingContext = { status: 'enabled' } & CanBeEnabled; | |
export type DisablingContext = { status: 'enabled' } & CanBeEnabled; | |
export const initialContext: Context = { | |
status: 'disabled', | |
metadataContainsConfig: false, | |
configFormIsValid: false, | |
onEnable: () => { | |
throw new Error('onEnable is not defined yet'); | |
}, | |
onDisable: () => { | |
throw new Error('onDisable is not defined yet'); | |
}, | |
}; | |
// --------------------------------------------------------------- | |
// EVENTS | |
export type Events = InternalEvents | UserEvents; | |
// INTERNAL EVENTS | |
/** | |
* Internal events, helpful for typing purposes. | |
*/ | |
type InternalEvents = SuccessEvent | FailureEvent; | |
export type SuccessEvent = { type: 'SUCCESS' }; | |
export type FailureEvent = { type: 'FAILURE' }; | |
// USER EVENTS | |
/** | |
* All the events triggered mostly by user inputs. | |
*/ | |
type UserEvents = ConfigFormSubmittedEvent | ToggleEvent; | |
export type ConfigFormSubmittedEvent = { type: 'CONFIG_FORM_SUBMITTED' }; | |
export type ToggleEvent = { type: 'TOGGLE' }; | |
// --------------------------------------------------------------- | |
// STATES | |
/** | |
* All the possible states of the machine. | |
*/ | |
export type States = | |
// INITIAL STATE | |
| { value: 'idle'; context: Context } | |
// INITIAL STATES | |
| { value: 'enabled'; context: Context & EnabledContext } | |
| { value: 'disabled'; context: Context & DisabledContext } | |
// ERROR STATES | |
| { | |
value: 'pleaseFillTheForm'; | |
context: Context & PleaseFillTheFormContext; | |
} | |
| { | |
value: 'pleaseSubmitTheForm'; | |
context: Context & PleaseSubmitTheFormContext; | |
} | |
// LOADING STATES | |
| { value: 'enabling'; context: Context & EnablingContext } | |
| { value: 'disabling'; context: Context & DisablingContext }; | |
export const toggleOpenTelemetryMachine = | |
/** @xstate-layout N4IgpgJg5mDOIC5QBcD2UoBswHkAOYAdgCpjYC2YyATgJ4B0AlhNgMQDaADALqKh6pYjZI1SE+IAB6IATAHYZ9AIwBOAKwBmJRoAcCnQDYdnACwAaELUQ6N9TvfsaVMzis6qZAX08W0GbPhEpBRUdEwsYBxKvEggAkIiYhLSCPKKqpraejKGxuaWiE4qdnJKJgZqnAYGChoG3r7oWLgEJGRglDQMEIywAIYARtgQrMQ4AOLjADIAolwx-ILCouKxKQYyxaZGKrpuBiY2Flapakr0KpeX8pw6Oi5GDSB+zYFtIV30Pf1DkKMT0zm0Qk8WWSTWiA2W3KOl2sKqhw0x0QJjUJjsDhMpjkmhUOKeLwCrWCHVC3V6g2G-0ms3YMgWcSWiVWoHWmzsMLh+0RyIQGk0GMcWiUShxMjUBKaRKC7U6YSIlMYhCgrAAygBVADCmpmqtV8xBTJWyUQSnFBnoBx0JnKOTRnHFvI0Tnozo0WLqphkBnckv8LRlH3lhEVytYADEAIIASSm6oASnMeIaEsaIQgVEo1K77nIVIYtFCZE6HfRKvYzZmHYY1PUfM8pQH3qTPgrfiMxjSkwzQcyTQgzdULldVKK0TIzby1HJ0Tj7JVpwZtKY-a9ibKyV8KUMlSqNdrdfrk7Fe2nWbIFMp1FpdPojKZeTbs5xSjCtPdOBovPXCU2SXLyR+TBdwjGM40TA0TyNcFz1SS8MhvbJcgfAoEAMdQywcXZSiXSpv0af03n-Tc8GwPpYDAcNGEwTBiAAC0o1BqHIVhNRwAA5cNo3GAB9cMcHjABZHiNQAIUE6NiGIGYABFIMWVMYKkRA5FKS1RSUD8dG0ExtF5FxznLec0WqXTqlXaVmwA+hSLAciwFVABXAZyGEejGOY1hBJmYhIxkyNfJ4tjOO4vifM1AAJWT5MZRSWWUhAcVsWtxUcLEXGdR9P0w+dnW0CcdAlJ5CFQCA4AkX8iI3LoUzBeKUgAWgMXkGuzK4rlcExdiMTSLL-aqwmYbBar7dNRVsdDNBkEwtHzdRi1QvYLlMbT1HcYw8xMPqqqDQDKUgEazwSmFlA05wtGmtQ1EfDQ5FdCcTBxXIcR9ORtvXXb6DbYDlUOpSUiUT9ik0A5+RmtQVHKOQnSzMsbCcDQHRB613sDFtg32iA-vq00sXOTYjDUeQym2aHUNrYGHDUGxtLkdwtp-RsdvRvad1+qC4v7EwFpOK16DnIGnqzXZUaskiyIoqiaPc8MmPIbH+0Bl0sw2ac6czVR9NRC4lGqapnB9dLReIz5bPspyXLchjZeYhWxqB5QUrV1wRRUR9Hp1moynQ71RW8bwgA */ | |
createMachine( | |
{ | |
context: initialContext, | |
tsTypes: {} as import('./toggleStateMachine.typegen').Typegen0, | |
schema: { context: {} as Context, events: {} as Events }, | |
id: 'toggleOpenTelemetry', | |
initial: 'idle', | |
states: { | |
idle: { | |
always: [ | |
{ | |
target: 'enabled', | |
cond: 'isEnabled', | |
}, | |
{ | |
target: 'disabled', | |
cond: 'isDisabled', | |
}, | |
], | |
}, | |
disabled: { | |
on: { | |
TOGGLE: [ | |
{ | |
target: 'pleaseFillTheForm', | |
cond: 'isEnablingWithoutConfiguration', | |
}, | |
{ | |
target: 'pleaseSubmitTheForm', | |
cond: 'isEnablingWithoutSubmittingForm', | |
}, | |
{ | |
target: 'enabling', | |
}, | |
], | |
}, | |
}, | |
enabling: { | |
invoke: { | |
src: 'callOnEnable', | |
}, | |
on: { | |
SUCCESS: { | |
target: 'enabled', | |
}, | |
FAILURE: { | |
target: 'disabled', | |
}, | |
}, | |
}, | |
enabled: { | |
on: { | |
TOGGLE: { | |
target: 'disabling', | |
}, | |
}, | |
}, | |
disabling: { | |
invoke: { | |
src: 'callOnDisable', | |
}, | |
on: { | |
SUCCESS: { | |
target: 'disabled', | |
}, | |
FAILURE: { | |
target: 'enabled', | |
}, | |
}, | |
}, | |
pleaseFillTheForm: { | |
on: { | |
CONFIG_FORM_SUBMITTED: { | |
target: 'disabled', | |
}, | |
}, | |
}, | |
pleaseSubmitTheForm: { | |
on: { | |
CONFIG_FORM_SUBMITTED: { | |
target: 'disabled', | |
}, | |
}, | |
}, | |
}, | |
}, | |
{ | |
services: { | |
callOnDisable: context => send => { | |
context | |
.onDisable() | |
.then(() => send({ type: 'SUCCESS' })) | |
.catch(() => send({ type: 'FAILURE' })); | |
}, | |
callOnEnable: context => send => { | |
context | |
.onEnable() | |
.then(() => send({ type: 'SUCCESS' })) | |
.catch(() => send({ type: 'FAILURE' })); | |
}, | |
}, | |
guards: { | |
isEnabled: context => context.status === 'enabled', | |
isDisabled: context => context.status === 'disabled', | |
isEnablingWithoutConfiguration: context => | |
context.metadataContainsConfig === false && | |
context.configFormIsValid === false, | |
isEnablingWithoutSubmittingForm: context => | |
context.metadataContainsConfig === false && | |
context.configFormIsValid === true, | |
}, | |
} | |
); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import type { ChangeEvent } from 'react'; | |
import { useRef, useState, useEffect } from 'react'; | |
import { useIsUnmounted } from '../useIsUnmounted'; | |
interface Params { | |
status: 'enabled' | 'disabled'; | |
onEnable: () => Promise<void>; | |
onDisable: () => Promise<void>; | |
// External conditions | |
formIsValid: boolean; | |
metadataContainsConfig: boolean; | |
} | |
export function useToggleState(params: Params) { | |
const { status, onEnable, onDisable, metadataContainsConfig, formIsValid } = | |
params; | |
const [checked, setChecked] = useState(status === 'enabled'); | |
const [submitting, setSubmitting] = useState<boolean>(false); | |
const [error, setError] = useState< | |
'noErrors' | 'setupConfigFirst' | 'submitConfigFormFirst' | |
>('noErrors'); | |
const isUnmounted = useIsUnmounted(); | |
// DISABLE OPENTELEMETRY | |
const disableOpenTelemetry = async () => { | |
// The toggle is controlled, hence we need to immediately updates the UI to reflect the users' action | |
setChecked(false); | |
setSubmitting(true); | |
try { | |
await onDisable(); | |
} catch { | |
if (isUnmounted()) return; | |
// In case of server errors while disabling OpenTelemetry, we need to revert the UI to the previous state | |
setChecked(true); | |
// Server errors are presented the users through notifications, no need to se them here | |
} | |
if (isUnmounted()) return; | |
setSubmitting(false); | |
}; | |
// ENABLE OPENTELEMETRY | |
const enableOpenTelemetry = async () => { | |
// The toggle is controlled, hence we need to immediately updates the UI to reflect the users' action | |
setChecked(true); | |
setSubmitting(true); | |
try { | |
await onEnable(); | |
} catch { | |
if (isUnmounted()) return; | |
// In case of server errors while disabling OpenTelemetry, we need to revert the UI to the previous state | |
setChecked(false); | |
// Server errors are presented the users through notifications, no need to se them here | |
} | |
if (isUnmounted()) return; | |
setSubmitting(false); | |
}; | |
// REACT TO TOGGLE CHANGES | |
function onChange(event: ChangeEvent<HTMLInputElement>) { | |
const isDisablingOpenTelemetry = event.target.checked === false; | |
// Disabling OpenTelemetry is always allowed because it's harmless (and that also means that | |
// previously it was enabled, hence the configuration was valid and already stored in the metadata) | |
if (isDisablingOpenTelemetry) { | |
disableOpenTelemetry(); | |
return; | |
} | |
if (!formIsValid && !metadataContainsConfig) { | |
// If the metadata does not contain a valid configuration (so it'f a first-time setup) and the | |
// config form is not valid, it means that the users have not filled the form yet and they must do | |
// it before enabling OpenTelemetry | |
setError('setupConfigFirst'); | |
setChecked(false); | |
return; | |
} | |
if (formIsValid && !metadataContainsConfig) { | |
// If the metadata does not contain a valid configuration (so it'f a first-time setup) and the | |
// config form is valid, it means that the users have already filled the form and they must submit | |
// it before enabling OpenTelemetry. Once submitted, the metadata will change | |
setError('submitConfigFormFirst'); | |
setChecked(false); | |
return; | |
} | |
// At this point, the metadata already contains a valid configuration, then we can enable OpenTelemetry. | |
// Please note that this conditions have been written to not cause problems in case of unmanaged | |
// edge cases. In fact, if something has not been managed correctly in the previous steps, we | |
// allow enabling open telemetry anyway! | |
enableOpenTelemetry(); | |
} | |
const externalConditionsRef = useRef({ metadataContainsConfig, formIsValid }); | |
// ERRORS RESETTING | |
useEffect(() => { | |
const { | |
metadataContainsConfig: prevMetadataContainsConfig, | |
formIsValid: prevFormIsValid, | |
} = externalConditionsRef.current; | |
// The opposite situation opposite should be impossible since at te moment the OT config cannot | |
// be removed/reset | |
if ( | |
prevMetadataContainsConfig === false && | |
metadataContainsConfig === true | |
) { | |
setError('noErrors'); | |
} | |
// For every changes happening in the form, we must reset the form-related error | |
if (prevFormIsValid !== formIsValid) { | |
setError('noErrors'); | |
} | |
// Update the ref after storing the previous values | |
externalConditionsRef.current = { metadataContainsConfig, formIsValid }; | |
}, [error, metadataContainsConfig, formIsValid]); | |
return { onChange, checked, submitting, error }; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment