From b38741402cfba1338061e0f07faec03a93a1842f Mon Sep 17 00:00:00 2001 From: Andrew Ballantyne Date: Thu, 3 Oct 2024 16:39:29 -0400 Subject: [PATCH] Support for Trusty DB fields --- frontend/src/api/trustyai/k8s.ts | 26 ++-- .../components/FieldGroupHelpLabelIcon.tsx | 16 +++ .../k8s/ResourceNameDefinitionTooltip.tsx | 13 +- frontend/src/concepts/k8s/utils.ts | 15 ++- frontend/src/concepts/trustyai/const.ts | 6 +- .../content/InstallTrustyAICheckbox.tsx | 62 ---------- .../trustyai/content/InstallTrustyModal.tsx | 110 +++++++++++++++++ .../content/TrustyAIServerTimedOutError.tsx | 42 ------- .../content/TrustyAIServiceControl.tsx | 69 ----------- .../content/TrustyAIServiceNotification.tsx | 74 ----------- .../content/TrustyDBExistingSecretField.tsx | 75 +++++++++++ .../trustyai/content/TrustyDBSecretFields.tsx | 100 +++++++++++++++ .../statusStates/TrustyAIInstalledState.tsx | 31 +++++ .../statusStates/TrustyAIUninstalledState.tsx | 36 ++++++ .../content/useTrustyBrowserStorage.ts | 58 +++++++++ .../trustyai/content/useTrustyCRState.tsx | 95 ++++++++++++++ .../useTrustyExistingSecretValidation.ts | 37 ++++++ .../content/useTrustyInstallModalData.tsx | 116 ++++++++++++++++++ .../trustyai/context/TrustyAIContext.tsx | 36 ++---- .../context/useDoesTrustyAICRExist.ts | 5 +- .../trustyai/context/useModelBiasData.ts | 18 +-- frontend/src/concepts/trustyai/types.ts | 28 +++++ .../concepts/trustyai/useManageTrustyAICR.ts | 110 +++++++++-------- .../trustyai/useTrustyAINamespaceCR.ts | 42 ++----- frontend/src/concepts/trustyai/utils.ts | 67 +++++++++- frontend/src/k8sTypes.ts | 17 ++- .../screens/metrics/MetricsPage.tsx | 8 +- .../screens/metrics/MetricsPageTabs.tsx | 5 +- .../BiasConfigurationPage.tsx | 10 +- .../metrics/bias/BiasMetricConfigSelector.tsx | 8 +- .../screens/metrics/bias/BiasTab.tsx | 9 +- .../projectSettings/ModelBiasSettingsCard.tsx | 40 +++--- .../pages/projects/projectSettings/const.ts | 5 - 33 files changed, 950 insertions(+), 439 deletions(-) create mode 100644 frontend/src/components/FieldGroupHelpLabelIcon.tsx delete mode 100644 frontend/src/concepts/trustyai/content/InstallTrustyAICheckbox.tsx create mode 100644 frontend/src/concepts/trustyai/content/InstallTrustyModal.tsx delete mode 100644 frontend/src/concepts/trustyai/content/TrustyAIServerTimedOutError.tsx delete mode 100644 frontend/src/concepts/trustyai/content/TrustyAIServiceControl.tsx delete mode 100644 frontend/src/concepts/trustyai/content/TrustyAIServiceNotification.tsx create mode 100644 frontend/src/concepts/trustyai/content/TrustyDBExistingSecretField.tsx create mode 100644 frontend/src/concepts/trustyai/content/TrustyDBSecretFields.tsx create mode 100644 frontend/src/concepts/trustyai/content/statusStates/TrustyAIInstalledState.tsx create mode 100644 frontend/src/concepts/trustyai/content/statusStates/TrustyAIUninstalledState.tsx create mode 100644 frontend/src/concepts/trustyai/content/useTrustyBrowserStorage.ts create mode 100644 frontend/src/concepts/trustyai/content/useTrustyCRState.tsx create mode 100644 frontend/src/concepts/trustyai/content/useTrustyExistingSecretValidation.ts create mode 100644 frontend/src/concepts/trustyai/content/useTrustyInstallModalData.tsx delete mode 100644 frontend/src/pages/projects/projectSettings/const.ts diff --git a/frontend/src/api/trustyai/k8s.ts b/frontend/src/api/trustyai/k8s.ts index 91bd5b9e4d..9f9b745e8a 100644 --- a/frontend/src/api/trustyai/k8s.ts +++ b/frontend/src/api/trustyai/k8s.ts @@ -9,21 +9,6 @@ import { TRUSTYAI_DEFINITION_NAME } from '~/concepts/trustyai/const'; import { applyK8sAPIOptions } from '~/api/apiMergeUtils'; import { TrustyAIApplicationsModel } from '~/api/models/trustyai'; -const trustyAIDefaultCRSpec: TrustyAIKind['spec'] = { - storage: { - format: 'PVC', - folder: '/inputs', - size: '1Gi', - }, - data: { - filename: 'data.csv', - format: 'CSV', - }, - metrics: { - schedule: '5s', - }, -}; - export const getTrustyAICR = async ( namespace: string, opts?: K8sAPIOptions, @@ -43,6 +28,7 @@ export const getTrustyAICR = async ( export const createTrustyAICR = async ( namespace: string, + secretName: string, opts?: K8sAPIOptions, ): Promise => { const resource: TrustyAIKind = { @@ -52,7 +38,15 @@ export const createTrustyAICR = async ( name: TRUSTYAI_DEFINITION_NAME, namespace, }, - spec: trustyAIDefaultCRSpec, + spec: { + storage: { + format: 'DATABASE', + databaseConfigurations: secretName, + }, + metrics: { + schedule: '5s', + }, + }, }; return k8sCreateResource( diff --git a/frontend/src/components/FieldGroupHelpLabelIcon.tsx b/frontend/src/components/FieldGroupHelpLabelIcon.tsx new file mode 100644 index 0000000000..24503a0286 --- /dev/null +++ b/frontend/src/components/FieldGroupHelpLabelIcon.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; +import { Popover } from '@patternfly/react-core'; +import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons'; +import DashboardPopupIconButton from '~/concepts/dashboard/DashboardPopupIconButton'; + +type FieldGroupHelpLabelIconProps = { + content: React.ComponentProps['bodyContent']; +}; + +const FieldGroupHelpLabelIcon: React.FC = ({ content }) => ( + + } aria-label="More info" /> + +); + +export default FieldGroupHelpLabelIcon; diff --git a/frontend/src/concepts/k8s/ResourceNameDefinitionTooltip.tsx b/frontend/src/concepts/k8s/ResourceNameDefinitionTooltip.tsx index d8d58577eb..1c596b7aa0 100644 --- a/frontend/src/concepts/k8s/ResourceNameDefinitionTooltip.tsx +++ b/frontend/src/concepts/k8s/ResourceNameDefinitionTooltip.tsx @@ -1,19 +1,16 @@ import * as React from 'react'; -import { Popover, Stack, StackItem } from '@patternfly/react-core'; -import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons'; -import DashboardPopupIconButton from '~/concepts/dashboard/DashboardPopupIconButton'; +import { Stack, StackItem } from '@patternfly/react-core'; +import FieldGroupHelpLabelIcon from '~/components/FieldGroupHelpLabelIcon'; const ResourceNameDefinitionTooltip: React.FC = () => ( - Resource names are what your resources are labeled in OpenShift. Resource names are not editable after creation. } - > - } aria-label="More info" /> - + /> ); export default ResourceNameDefinitionTooltip; diff --git a/frontend/src/concepts/k8s/utils.ts b/frontend/src/concepts/k8s/utils.ts index 6a710d1e34..5d4db8d69b 100644 --- a/frontend/src/concepts/k8s/utils.ts +++ b/frontend/src/concepts/k8s/utils.ts @@ -1,5 +1,5 @@ import { K8sResourceCommon } from '@openshift/dynamic-plugin-sdk-utils'; -import { K8sDSGResource } from '~/k8sTypes'; +import { K8sCondition, K8sDSGResource } from '~/k8sTypes'; import { genRandomChars } from '~/utilities/string'; export const PreInstalledName = 'Pre-installed'; @@ -108,3 +108,16 @@ export const translateDisplayNameForK8s = ( export const isValidK8sName = (name?: string): boolean => name === undefined || (name.length > 0 && /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(name)); + +type ResourceWithConditions = K8sResourceCommon & { status?: { conditions?: K8sCondition[] } }; + +export const getConditionForType = ( + resource: ResourceWithConditions, + type: string, +): K8sCondition | undefined => resource.status?.conditions?.find((c) => c.type === type); + +export const isConditionInStatus = ( + resource: ResourceWithConditions, + type: string, + status: string, +): boolean => getConditionForType(resource, type)?.status === status; diff --git a/frontend/src/concepts/trustyai/const.ts b/frontend/src/concepts/trustyai/const.ts index fd807c97eb..094ac3909e 100644 --- a/frontend/src/concepts/trustyai/const.ts +++ b/frontend/src/concepts/trustyai/const.ts @@ -1,3 +1,5 @@ -export const TRUSTYAI_ROUTE_NAME = 'trustyai-service'; - export const TRUSTYAI_DEFINITION_NAME = 'trustyai-service'; + +export const TRUSTYAI_SECRET_NAME = 'trustyai-db-secret'; + +export const TRUSTYAI_INSTALL_MODAL_TEST_ID = 'trusty-db-config'; diff --git a/frontend/src/concepts/trustyai/content/InstallTrustyAICheckbox.tsx b/frontend/src/concepts/trustyai/content/InstallTrustyAICheckbox.tsx deleted file mode 100644 index c51553f6e1..0000000000 --- a/frontend/src/concepts/trustyai/content/InstallTrustyAICheckbox.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React from 'react'; -import { Checkbox, HelperText, HelperTextItem } from '@patternfly/react-core'; -import { TRUSTYAI_TOOLTIP_TEXT } from '~/pages/projects/projectSettings/const'; -import DeleteTrustyAIModal from '~/concepts/trustyai/content/DeleteTrustyAIModal'; - -type InstallTrustyAICheckboxProps = { - isAvailable: boolean; - isProgressing: boolean; - onInstall: () => Promise; - onDelete: () => Promise; - disabled: boolean; - disabledReason?: string; -}; -const InstallTrustyAICheckbox: React.FC = ({ - isAvailable, - isProgressing, - onInstall, - onDelete, - disabled, - disabledReason, -}) => { - const [open, setOpen] = React.useState(false); - const [userHasChecked, setUserHasChecked] = React.useState(false); - - const helperText = disabled ? disabledReason : TRUSTYAI_TOOLTIP_TEXT; - - return ( - <> - - {helperText} - - } - isChecked={!disabled && isAvailable} - isDisabled={disabled || userHasChecked || isProgressing} - onChange={(e, checked) => { - if (checked) { - setUserHasChecked(true); - onInstall().finally(() => setUserHasChecked(false)); - } else { - setOpen(true); - } - }} - id="trustyai-service-installation" - data-testid="trustyai-service-installation" - name="TrustyAI service installation status" - /> - {open ? ( - { - setOpen(false); - }} - /> - ) : null} - - ); -}; - -export default InstallTrustyAICheckbox; diff --git a/frontend/src/concepts/trustyai/content/InstallTrustyModal.tsx b/frontend/src/concepts/trustyai/content/InstallTrustyModal.tsx new file mode 100644 index 0000000000..01cb036ca5 --- /dev/null +++ b/frontend/src/concepts/trustyai/content/InstallTrustyModal.tsx @@ -0,0 +1,110 @@ +import * as React from 'react'; +import { Form, Modal, Radio } from '@patternfly/react-core'; +import DashboardModalFooter from '~/concepts/dashboard/DashboardModalFooter'; +import TrustyDBSecretFields from '~/concepts/trustyai/content/TrustyDBSecretFields'; +import useTrustyInstallModalData, { + TrustyInstallModalFormType, +} from '~/concepts/trustyai/content/useTrustyInstallModalData'; +import { UseManageTrustyAICRReturnType } from '~/concepts/trustyai/useManageTrustyAICR'; +import FieldGroupHelpLabelIcon from '~/components/FieldGroupHelpLabelIcon'; +import TrustyDBExistingSecretField from '~/concepts/trustyai/content/TrustyDBExistingSecretField'; + +type InstallTrustyModalProps = { + onClose: () => void; + namespace: string; + onInstallExistingDB: UseManageTrustyAICRReturnType['installCRForExistingDB']; + onInstallNewDB: UseManageTrustyAICRReturnType['installCRForNewDB']; +}; + +const InstallTrustyModal: React.FC = ({ + onClose, + namespace, + onInstallNewDB, + onInstallExistingDB, +}) => { + const [submitting, setSubmitting] = React.useState(false); + const [installError, setInstallError] = React.useState(); + const formData = useTrustyInstallModalData(namespace); + + return ( + { + let promise: Promise; + if (formData.type === TrustyInstallModalFormType.EXISTING) { + promise = onInstallExistingDB(formData.data); + } else { + promise = onInstallNewDB(formData.data); + } + setSubmitting(true); + setInstallError(undefined); + promise + .then(() => { + onClose(); + }) + .catch((e) => { + setInstallError(e); + }) + .finally(() => { + setSubmitting(false); + }); + }} + submitLabel="Configure" + isSubmitLoading={submitting} + isSubmitDisabled={!formData.canSubmit || submitting} + error={installError} + alertTitle="Install error" + /> + } + > +
{ + e.preventDefault(); + }} + > + + Specify an existing secret{' '} + + + } + name="secret-value" + isChecked={formData.type === TrustyInstallModalFormType.EXISTING} + onChange={() => formData.onModeChange(TrustyInstallModalFormType.EXISTING)} + body={ + formData.type === TrustyInstallModalFormType.EXISTING && ( + + ) + } + /> + + Create a new secret{' '} + + + } + name="secret-value" + isChecked={formData.type === TrustyInstallModalFormType.NEW} + onChange={() => formData.onModeChange(TrustyInstallModalFormType.NEW)} + body={ + formData.type === TrustyInstallModalFormType.NEW && ( + + ) + } + /> + +
+ ); +}; + +export default InstallTrustyModal; diff --git a/frontend/src/concepts/trustyai/content/TrustyAIServerTimedOutError.tsx b/frontend/src/concepts/trustyai/content/TrustyAIServerTimedOutError.tsx deleted file mode 100644 index a53d472cf9..0000000000 --- a/frontend/src/concepts/trustyai/content/TrustyAIServerTimedOutError.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import * as React from 'react'; -import { - Alert, - AlertActionCloseButton, - AlertActionLink, - Stack, - StackItem, -} from '@patternfly/react-core'; - -type TrustyAITimedOutErrorProps = { - ignoreTimedOut: () => void; - deleteCR: () => Promise; -}; -const TrustyAITimedOutError: React.FC = ({ - ignoreTimedOut, - deleteCR, -}) => ( - ignoreTimedOut()} />} - actionLinks={ - <> - deleteCR()}>Uninstall TrustyAI - ignoreTimedOut()}>Close - - } - > - - - An error occurred while installing or loading TrustyAI. To continue, uninstall and reinstall - TrustyAI. Uninstalling this service will delete all of its resources, including model bias - configurations. - - For help, contact your administrator. - - -); - -export default TrustyAITimedOutError; diff --git a/frontend/src/concepts/trustyai/content/TrustyAIServiceControl.tsx b/frontend/src/concepts/trustyai/content/TrustyAIServiceControl.tsx deleted file mode 100644 index 5f0fe2de14..0000000000 --- a/frontend/src/concepts/trustyai/content/TrustyAIServiceControl.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { Bullseye, Spinner, Stack, StackItem } from '@patternfly/react-core'; -import React from 'react'; -import useManageTrustyAICR from '~/concepts/trustyai/useManageTrustyAICR'; -import TrustyAIServiceNotification from '~/concepts/trustyai/content/TrustyAIServiceNotification'; -import InstallTrustyAICheckbox from './InstallTrustyAICheckbox'; - -type TrustyAIServiceControlProps = { - namespace: string; - disabled: boolean; - disabledReason?: string; -}; -const TrustyAIServiceControl: React.FC = ({ - namespace, - disabled, - disabledReason, -}) => { - const { - isAvailable, - isProgressing, - showSuccess, - installCR, - deleteCR, - error, - isSettled, - serverTimedOut, - ignoreTimedOut, - } = useManageTrustyAICR(namespace); - - const [userStartedInstall, setUserStartedInstall] = React.useState(false); - - if (!disabled && !isSettled) { - return ( - - - - ); - } - - return ( - - - { - setUserStartedInstall(true); - return installCR().finally(() => setUserStartedInstall(false)); - }} - onDelete={deleteCR} - /> - - - - - - ); -}; - -export default TrustyAIServiceControl; diff --git a/frontend/src/concepts/trustyai/content/TrustyAIServiceNotification.tsx b/frontend/src/concepts/trustyai/content/TrustyAIServiceNotification.tsx deleted file mode 100644 index dbe1e7f6ac..0000000000 --- a/frontend/src/concepts/trustyai/content/TrustyAIServiceNotification.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { Alert, AlertActionCloseButton, Bullseye, Spinner } from '@patternfly/react-core'; -import React from 'react'; -import TrustyAITimedOutError from '~/concepts/trustyai/content/TrustyAIServerTimedOutError'; - -type TrustyAIServiceNotificationProps = { - error?: Error; - isAvailable: boolean; - showSuccess: boolean; - loading: boolean; - timedOut: boolean; - ignoreTimedOut: () => void; - deleteCR: () => Promise; -}; - -const TrustyAIServiceNotification: React.FC = ({ - error, - isAvailable, - showSuccess, - loading, - timedOut, - ignoreTimedOut, - deleteCR, -}) => { - const [dismissSuccess, setDismissSuccess] = React.useState(false); - - React.useEffect(() => { - if (loading) { - setDismissSuccess(false); - } - }, [loading]); - - if (timedOut) { - return ; - } - - if (loading) { - return ( - - - - ); - } - - if (!dismissSuccess && showSuccess && isAvailable) { - return ( - setDismissSuccess(true)} />} - isLiveRegion - isInline - /> - ); - } - - if (error) { - return ( - - {error.message} - - ); - } - - return null; -}; - -export default TrustyAIServiceNotification; diff --git a/frontend/src/concepts/trustyai/content/TrustyDBExistingSecretField.tsx b/frontend/src/concepts/trustyai/content/TrustyDBExistingSecretField.tsx new file mode 100644 index 0000000000..eb31059b25 --- /dev/null +++ b/frontend/src/concepts/trustyai/content/TrustyDBExistingSecretField.tsx @@ -0,0 +1,75 @@ +import * as React from 'react'; +import { HelperText, HelperTextItem, TextInput } from '@patternfly/react-core'; +import { + TrustyInstallModalFormExistingState, + UseTrustyInstallModalDataExisting, +} from '~/concepts/trustyai/content/useTrustyInstallModalData'; +import { TRUSTYAI_INSTALL_MODAL_TEST_ID } from '~/concepts/trustyai/const'; +import useTrustyExistingSecretValidation from '~/concepts/trustyai/content/useTrustyExistingSecretValidation'; + +type TrustyDBExistingSecretFieldProps = { + formData: UseTrustyInstallModalDataExisting; +}; + +const TrustyDBExistingSecretField: React.FC = ({ formData }) => { + const { data, onDataChange, state } = formData; + const { onBlur } = useTrustyExistingSecretValidation(formData); + + let helperText: + | { + message: string; + variant: React.ComponentProps['variant']; + } + | undefined; + let inputState: React.ComponentProps['validated']; + switch (state) { + case TrustyInstallModalFormExistingState.NOT_FOUND: + helperText = { + message: 'No match found. Check your entry and try again.', + variant: 'error', + }; + inputState = 'error'; + break; + case TrustyInstallModalFormExistingState.EXISTING: + inputState = 'success'; + break; + case TrustyInstallModalFormExistingState.CHECKING: + helperText = { + message: 'Checking for secret...', + variant: 'default', + }; + break; + case TrustyInstallModalFormExistingState.UNSURE: + helperText = { + message: 'Unable to validate the secret', + variant: 'warning', + }; + inputState = 'warning'; + break; + case TrustyInstallModalFormExistingState.UNKNOWN: + default: + inputState = 'default'; + } + + return ( + <> + onDataChange(value)} + onBlur={onBlur} + validated={inputState} + /> + {helperText && ( + + {helperText.message} + + )} + + ); +}; + +export default TrustyDBExistingSecretField; diff --git a/frontend/src/concepts/trustyai/content/TrustyDBSecretFields.tsx b/frontend/src/concepts/trustyai/content/TrustyDBSecretFields.tsx new file mode 100644 index 0000000000..f126c0d9ef --- /dev/null +++ b/frontend/src/concepts/trustyai/content/TrustyDBSecretFields.tsx @@ -0,0 +1,100 @@ +import * as React from 'react'; +import { FormGroup, FormSection, TextInput } from '@patternfly/react-core'; +import { UpdateObjectAtPropAndValue } from '~/pages/projects/types'; +import { TrustyDBData } from '~/concepts/trustyai/types'; +import FieldGroupHelpLabelIcon from '~/components/FieldGroupHelpLabelIcon'; +import { TRUSTYAI_INSTALL_MODAL_TEST_ID } from '~/concepts/trustyai/const'; +import PasswordInput from '~/components/PasswordInput'; + +type TrustyDBSecretFieldsProps = { + data: TrustyDBData; + onDataChange: UpdateObjectAtPropAndValue; +}; + +const TrustyField: React.FC<{ + data: TrustyDBData; + id: keyof TrustyDBData; + label: string; + labelTooltip: string; + onChange: TrustyDBSecretFieldsProps['onDataChange']; +}> = ({ data, id, label, labelTooltip, onChange }) => { + const value = data[id]; + const Component = id === 'databasePassword' ? PasswordInput : TextInput; + + return ( + } + isRequired + fieldId={`${TRUSTYAI_INSTALL_MODAL_TEST_ID}-${id}`} + > + { + onChange(id, newValue); + }} + /> + + ); +}; + +const TrustyDBSecretFields: React.FC = ({ data, onDataChange }) => ( + + + + + + + + + +); + +export default TrustyDBSecretFields; diff --git a/frontend/src/concepts/trustyai/content/statusStates/TrustyAIInstalledState.tsx b/frontend/src/concepts/trustyai/content/statusStates/TrustyAIInstalledState.tsx new file mode 100644 index 0000000000..6919c9f116 --- /dev/null +++ b/frontend/src/concepts/trustyai/content/statusStates/TrustyAIInstalledState.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; +import { Button } from '@patternfly/react-core'; +import DeleteTrustyAIModal from '~/concepts/trustyai/content/DeleteTrustyAIModal'; + +type TrustyAIInstalledStateProps = { + uninstalling?: boolean; + onDelete: () => Promise; +}; + +const TrustyAIInstalledState: React.FC = ({ + uninstalling, + onDelete, +}) => { + const [openModal, setOpenModal] = React.useState(false); + + return ( + <> + + {openModal && setOpenModal(false)} />} + + ); +}; + +export default TrustyAIInstalledState; diff --git a/frontend/src/concepts/trustyai/content/statusStates/TrustyAIUninstalledState.tsx b/frontend/src/concepts/trustyai/content/statusStates/TrustyAIUninstalledState.tsx new file mode 100644 index 0000000000..5085e5ee20 --- /dev/null +++ b/frontend/src/concepts/trustyai/content/statusStates/TrustyAIUninstalledState.tsx @@ -0,0 +1,36 @@ +import * as React from 'react'; +import { Button } from '@patternfly/react-core'; +import InstallTrustyModal from '~/concepts/trustyai/content/InstallTrustyModal'; +import { UseManageTrustyAICRReturnType } from '~/concepts/trustyai/useManageTrustyAICR'; + +type TrustyAIUninstalledStateProps = { + namespace: string; + onInstallExistingDB: UseManageTrustyAICRReturnType['installCRForExistingDB']; + onInstallNewDB: UseManageTrustyAICRReturnType['installCRForNewDB']; +}; + +const TrustyAIUninstalledState: React.FC = ({ + namespace, + onInstallExistingDB, + onInstallNewDB, +}) => { + const [openModal, setOpenModal] = React.useState(false); + + return ( + <> + + {openModal && ( + setOpenModal(false)} + /> + )} + + ); +}; + +export default TrustyAIUninstalledState; diff --git a/frontend/src/concepts/trustyai/content/useTrustyBrowserStorage.ts b/frontend/src/concepts/trustyai/content/useTrustyBrowserStorage.ts new file mode 100644 index 0000000000..a4c7a8e224 --- /dev/null +++ b/frontend/src/concepts/trustyai/content/useTrustyBrowserStorage.ts @@ -0,0 +1,58 @@ +import * as React from 'react'; +import { useBrowserStorage } from '~/components/browserStorage'; +import { TrustyAIKind } from '~/k8sTypes'; + +/** { [namespace]: uid } */ +type TrustyStorageState = Record; + +export type UseTrustyBrowserStorage = { + showSuccess: boolean; + onDismissSuccess: () => void; +}; + +const useTrustyBrowserStorage = (cr?: TrustyAIKind | null): UseTrustyBrowserStorage => { + const [trustyStorageState, setTrustyStorageState] = useBrowserStorage( + 'odh.dashboard.project.trusty', + {}, + ); + const [showSuccess, setShowSuccess] = React.useState(false); + + const { namespace, uid } = cr?.metadata || {}; + + // Ignore watching this value for changes in the hooks + const storageRef = React.useRef(trustyStorageState); + storageRef.current = trustyStorageState; + + React.useEffect(() => { + if (namespace && uid) { + const matchingUID = storageRef.current[namespace]; + if (matchingUID !== uid) { + // Don't have a dismiss state or it does not match the current instance + setShowSuccess(true); + } else if (uid) { + // If somehow it's showing and dismissed, hide it + setShowSuccess(false); + } + } + }, [namespace, uid]); + + const onDismissSuccess = React.useCallback(() => { + // Immediate feedback + setShowSuccess(false); + + if (!namespace) { + // Likely improperly called -- shouldn't be able to dismiss a situation without namespace + return; + } + + // Update the state + setTrustyStorageState({ ...storageRef.current, [namespace]: uid }); + }, [namespace, setTrustyStorageState, uid]); + + return { + showSuccess, + onDismissSuccess, + }; +}; + +export default useTrustyBrowserStorage; diff --git a/frontend/src/concepts/trustyai/content/useTrustyCRState.tsx b/frontend/src/concepts/trustyai/content/useTrustyCRState.tsx new file mode 100644 index 0000000000..3c4dce14e5 --- /dev/null +++ b/frontend/src/concepts/trustyai/content/useTrustyCRState.tsx @@ -0,0 +1,95 @@ +import * as React from 'react'; +import { + Alert, + AlertActionCloseButton, + Skeleton, + Spinner, + Split, + SplitItem, +} from '@patternfly/react-core'; +import useManageTrustyAICR from '~/concepts/trustyai/useManageTrustyAICR'; +import { TrustyInstallState } from '~/concepts/trustyai/types'; +import TrustyAIInstalledState from '~/concepts/trustyai/content/statusStates/TrustyAIInstalledState'; +import TrustyAIUninstalledState from '~/concepts/trustyai/content/statusStates/TrustyAIUninstalledState'; +import { ProjectKind } from '~/k8sTypes'; + +type UseTrustyCRState = { + action: React.ReactNode; + status?: React.ReactNode; +}; + +const useTrustyCRState = (project: ProjectKind): UseTrustyCRState => { + const namespace = project.metadata.name; + const { statusState, installCRForExistingDB, installCRForNewDB, deleteCR } = + useManageTrustyAICR(namespace); + + let action: React.ReactNode; + let status: React.ReactNode; + switch (statusState.type) { + case TrustyInstallState.ERROR: + action = ; + status = ( + +

{statusState.message}

+

Uninstall and try again, or contact your administrator.

+
+ ); + break; + case TrustyInstallState.INSTALLED: + action = ; + status = statusState.showSuccess && ( + + ) + } + > + You can view TrustyAI metrics in the model metrics screen. If you need to make changes, + delete the deployment and start over. + + ); + break; + case TrustyInstallState.LOADING_INITIAL_STATE: + action = ; + break; + case TrustyInstallState.INSTALLING: + action = ; + status = ( + + + + + Installing TrustyAI... + + ); + break; + case TrustyInstallState.UNINSTALLING: + action = ; + break; + case TrustyInstallState.UNINSTALLED: + default: + action = ( + + ); + } + + return { action, status }; +}; + +export default useTrustyCRState; diff --git a/frontend/src/concepts/trustyai/content/useTrustyExistingSecretValidation.ts b/frontend/src/concepts/trustyai/content/useTrustyExistingSecretValidation.ts new file mode 100644 index 0000000000..272cece2ba --- /dev/null +++ b/frontend/src/concepts/trustyai/content/useTrustyExistingSecretValidation.ts @@ -0,0 +1,37 @@ +import * as React from 'react'; +import { UseTrustyInstallModalDataExisting } from '~/concepts/trustyai/content/useTrustyInstallModalData'; + +type UseTrustyExistingSecretValidation = { + onBlur: () => void; +}; + +const useTrustyExistingSecretValidation = ( + formData: UseTrustyInstallModalDataExisting, +): UseTrustyExistingSecretValidation => { + const { data, onCheckState } = formData; + + const timeoutRef = React.useRef(null); + + const clearCheckTimeout = React.useCallback(() => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }, []); + + React.useEffect(() => { + if (data) { + timeoutRef.current = setTimeout(onCheckState, 3000); + } + + return clearCheckTimeout; + }, [clearCheckTimeout, data, onCheckState]); + + return { + onBlur: React.useCallback(() => { + clearCheckTimeout(); + onCheckState(); + }, [clearCheckTimeout, onCheckState]), + }; +}; + +export default useTrustyExistingSecretValidation; diff --git a/frontend/src/concepts/trustyai/content/useTrustyInstallModalData.tsx b/frontend/src/concepts/trustyai/content/useTrustyInstallModalData.tsx new file mode 100644 index 0000000000..8dd5fdd706 --- /dev/null +++ b/frontend/src/concepts/trustyai/content/useTrustyInstallModalData.tsx @@ -0,0 +1,116 @@ +import * as React from 'react'; +import useGenericObjectState from '~/utilities/useGenericObjectState'; +import { TrustyDBData } from '~/concepts/trustyai/types'; +import { UpdateObjectAtPropAndValue } from '~/pages/projects/types'; +import { getSecret } from '~/api'; + +export enum TrustyInstallModalFormType { + EXISTING = 'existing', + NEW = 'new', +} + +export enum TrustyInstallModalFormExistingState { + EXISTING = 'valid', + NOT_FOUND = 'invalid', + CHECKING = 'checking', + UNSURE = 'unsure', + UNKNOWN = 'unknown', +} +const EXISTING_NAME_FAIL_STATES = [ + TrustyInstallModalFormExistingState.NOT_FOUND, + TrustyInstallModalFormExistingState.CHECKING, + TrustyInstallModalFormExistingState.UNKNOWN, +]; + +type UseTrustyInstallModalDataBase = { + canSubmit: boolean; + onModeChange: (mode: TrustyInstallModalFormType) => void; +}; +export type UseTrustyInstallModalDataNew = UseTrustyInstallModalDataBase & { + type: TrustyInstallModalFormType.NEW; + data: TrustyDBData; + onDataChange: UpdateObjectAtPropAndValue; +}; +export type UseTrustyInstallModalDataExisting = UseTrustyInstallModalDataBase & { + type: TrustyInstallModalFormType.EXISTING; + data: string; + onDataChange: (newValue: string) => void; + state: TrustyInstallModalFormExistingState; + onCheckState: () => void; +}; + +type UseTrustyInstallModalData = UseTrustyInstallModalDataNew | UseTrustyInstallModalDataExisting; + +const useTrustyInstallModalData = (namespace: string): UseTrustyInstallModalData => { + const [mode, setMode] = React.useState( + TrustyInstallModalFormType.EXISTING, + ); + + const [existingValue, setExistingValue] = React.useState(''); + const [existingValid, setExistingValid] = React.useState( + TrustyInstallModalFormExistingState.UNKNOWN, + ); + const onExistingChange = React.useCallback( + (value) => { + setExistingValue(value); + setExistingValid(TrustyInstallModalFormExistingState.UNKNOWN); + }, + [], + ); + const onCheckState = React.useCallback(() => { + if (existingValue) { + setExistingValid(TrustyInstallModalFormExistingState.CHECKING); + getSecret(namespace, existingValue) + .then(() => { + setExistingValid(TrustyInstallModalFormExistingState.EXISTING); + }) + .catch((e) => { + if (e.statusObject?.code === 404) { + setExistingValid(TrustyInstallModalFormExistingState.NOT_FOUND); + return; + } + + // eslint-disable-next-line no-console + console.error('TrustyAI: Unknown error while validating the secret', e); + setExistingValid(TrustyInstallModalFormExistingState.UNSURE); + }); + } else { + setExistingValid(TrustyInstallModalFormExistingState.UNKNOWN); + } + }, [existingValue, namespace]); + + const [dbFormData, onDbFormDataChange] = useGenericObjectState({ + databaseKind: 'mariadb', + databaseUsername: '', + databasePassword: '', + databaseService: 'mariadb', + databasePort: '3306', + databaseName: '', + databaseGeneration: 'update', + }); + + switch (mode) { + case TrustyInstallModalFormType.NEW: + return { + onModeChange: setMode, + canSubmit: Object.values(dbFormData).every((v) => !!v), + type: TrustyInstallModalFormType.NEW, + data: dbFormData, + onDataChange: onDbFormDataChange, + }; + case TrustyInstallModalFormType.EXISTING: + default: + return { + onModeChange: setMode, + canSubmit: + existingValue.trim().length > 0 && !EXISTING_NAME_FAIL_STATES.includes(existingValid), + type: TrustyInstallModalFormType.EXISTING, + data: existingValue, + onDataChange: onExistingChange, + state: existingValid, + onCheckState, + }; + } +}; + +export default useTrustyInstallModalData; diff --git a/frontend/src/concepts/trustyai/context/TrustyAIContext.tsx b/frontend/src/concepts/trustyai/context/TrustyAIContext.tsx index ba3e956397..6db6c9cd26 100644 --- a/frontend/src/concepts/trustyai/context/TrustyAIContext.tsx +++ b/frontend/src/concepts/trustyai/context/TrustyAIContext.tsx @@ -1,20 +1,16 @@ import React from 'react'; -import useTrustyAINamespaceCR, { - isTrustyAIAvailable, - taiHasServerTimedOut, -} from '~/concepts/trustyai/useTrustyAINamespaceCR'; +import useTrustyAINamespaceCR from '~/concepts/trustyai/useTrustyAINamespaceCR'; import useTrustyAIAPIState, { TrustyAPIState } from '~/concepts/trustyai/useTrustyAIAPIState'; import { TrustyAIContextData } from '~/concepts/trustyai/context/types'; import { DEFAULT_CONTEXT_DATA } from '~/concepts/trustyai/context/const'; import useFetchContextData from '~/concepts/trustyai/context/useFetchContextData'; +import { getTrustyStatusState } from '~/concepts/trustyai/utils'; +import { TrustyInstallState, TrustyStatusStates } from '~/concepts/trustyai/types'; +import { useDeepCompareMemoize } from '~/utilities/useDeepCompareMemoize'; type TrustyAIContextProps = { namespace: string; - hasCR: boolean; - crInitializing: boolean; - serverTimedOut: boolean; - serviceLoadError?: Error; - ignoreTimedOut: () => void; + statusState: TrustyStatusStates; refreshState: () => Promise; refreshAPIState: () => void; apiState: TrustyAPIState; @@ -23,10 +19,7 @@ type TrustyAIContextProps = { export const TrustyAIContext = React.createContext({ namespace: '', - hasCR: false, - crInitializing: false, - serverTimedOut: false, - ignoreTimedOut: () => undefined, + statusState: { type: TrustyInstallState.LOADING_INITIAL_STATE }, data: DEFAULT_CONTEXT_DATA, refreshState: async () => undefined, refreshAPIState: () => undefined, @@ -43,13 +36,8 @@ export const TrustyAIContextProvider: React.FC = ( namespace, }) => { const crState = useTrustyAINamespaceCR(namespace); - const [trustyNamespaceCR, crLoaded, crLoadError, refreshCR] = crState; - const isCRReady = isTrustyAIAvailable(crState); - const [disableTimeout, setDisableTimeout] = React.useState(false); - const serverTimedOut = !disableTimeout && taiHasServerTimedOut(crState, isCRReady); - const ignoreTimedOut = React.useCallback(() => { - setDisableTimeout(true); - }, []); + const [trustyNamespaceCR, crLoaded, , refreshCR] = crState; + const statusState = useDeepCompareMemoize(getTrustyStatusState(crState)); const taisName = trustyNamespaceCR?.metadata.name; @@ -66,9 +54,7 @@ export const TrustyAIContextProvider: React.FC = ( namespace, hasCR: !!trustyNamespaceCR, crInitializing: !crLoaded, - serverTimedOut, - ignoreTimedOut, - crLoadError, + statusState, refreshState, refreshAPIState, apiState, @@ -78,9 +64,7 @@ export const TrustyAIContextProvider: React.FC = ( namespace, trustyNamespaceCR, crLoaded, - serverTimedOut, - ignoreTimedOut, - crLoadError, + statusState, refreshState, refreshAPIState, apiState, diff --git a/frontend/src/concepts/trustyai/context/useDoesTrustyAICRExist.ts b/frontend/src/concepts/trustyai/context/useDoesTrustyAICRExist.ts index 3faac3cf49..7c591873bf 100644 --- a/frontend/src/concepts/trustyai/context/useDoesTrustyAICRExist.ts +++ b/frontend/src/concepts/trustyai/context/useDoesTrustyAICRExist.ts @@ -1,12 +1,13 @@ import React from 'react'; import { TrustyAIContext } from '~/concepts/trustyai/context/TrustyAIContext'; import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; +import { TrustyInstallState } from '~/concepts/trustyai/types'; const useDoesTrustyAICRExist = (): boolean[] => { const trustyAIAreaAvailable = useIsAreaAvailable(SupportedArea.TRUSTY_AI).status; - const { hasCR } = React.useContext(TrustyAIContext); + const { statusState } = React.useContext(TrustyAIContext); - return [trustyAIAreaAvailable && hasCR]; + return [trustyAIAreaAvailable && statusState.type === TrustyInstallState.INSTALLED]; }; export default useDoesTrustyAICRExist; diff --git a/frontend/src/concepts/trustyai/context/useModelBiasData.ts b/frontend/src/concepts/trustyai/context/useModelBiasData.ts index 353c52f527..4649bee87d 100644 --- a/frontend/src/concepts/trustyai/context/useModelBiasData.ts +++ b/frontend/src/concepts/trustyai/context/useModelBiasData.ts @@ -1,25 +1,17 @@ import React from 'react'; import { useParams } from 'react-router-dom'; -import { BiasMetricConfig } from '~/concepts/trustyai/types'; +import { BiasMetricConfig, TrustyStatusStates } from '~/concepts/trustyai/types'; import { TrustyAIContext } from '~/concepts/trustyai/context/TrustyAIContext'; export type ModelBiasData = { biasMetricConfigs: BiasMetricConfig[]; - serviceStatus: { initializing: boolean; installed: boolean; timedOut: boolean }; - loaded: boolean; - loadError?: Error; + statusState: TrustyStatusStates; refresh: () => Promise; }; export const useModelBiasData = (): ModelBiasData => { const { inferenceService } = useParams(); - const { data, crInitializing, hasCR, serverTimedOut } = React.useContext(TrustyAIContext); - - const serviceStatus: ModelBiasData['serviceStatus'] = { - initializing: crInitializing, - installed: hasCR, - timedOut: serverTimedOut, - }; + const { data, statusState } = React.useContext(TrustyAIContext); const biasMetricConfigs = React.useMemo(() => { let configs: BiasMetricConfig[] = []; @@ -32,10 +24,8 @@ export const useModelBiasData = (): ModelBiasData => { }, [data.biasMetricConfigs, data.loaded, inferenceService]); return { - serviceStatus, + statusState, biasMetricConfigs, - loaded: data.loaded, - loadError: data.error, refresh: data.refresh, }; }; diff --git a/frontend/src/concepts/trustyai/types.ts b/frontend/src/concepts/trustyai/types.ts index 990a50f146..c46bddc89a 100644 --- a/frontend/src/concepts/trustyai/types.ts +++ b/frontend/src/concepts/trustyai/types.ts @@ -6,6 +6,34 @@ import { } from '~/api'; import { K8sAPIOptions } from '~/k8sTypes'; +export enum TrustyInstallState { + UNINSTALLING = 'uninstalling', + INSTALLED = 'installed', + INSTALLING = 'installing', + ERROR = 'error', + UNINSTALLED = 'uninstalled', + LOADING_INITIAL_STATE = 'unknown', +} + +export type TrustyStatusStates = + | { type: TrustyInstallState.ERROR; message: string } + | { type: TrustyInstallState.LOADING_INITIAL_STATE } + | { type: TrustyInstallState.INSTALLED; showSuccess: boolean; onDismissSuccess?: () => void } + | { type: TrustyInstallState.INSTALLING } + | { type: TrustyInstallState.UNINSTALLING } + | { type: TrustyInstallState.UNINSTALLED }; + +/** Structure matches K8s Secret structure */ +export type TrustyDBData = { + databaseKind: string; + databaseUsername: string; + databasePassword: string; + databaseService: string; + databasePort: string; + databaseName: string; + databaseGeneration: string; +}; + export type ListRequests = (opts: K8sAPIOptions) => Promise; export type ListSpdRequests = (opts: K8sAPIOptions) => Promise; export type ListDirRequests = (opts: K8sAPIOptions) => Promise; diff --git a/frontend/src/concepts/trustyai/useManageTrustyAICR.ts b/frontend/src/concepts/trustyai/useManageTrustyAICR.ts index e4034d397d..5d7e576e04 100644 --- a/frontend/src/concepts/trustyai/useManageTrustyAICR.ts +++ b/frontend/src/concepts/trustyai/useManageTrustyAICR.ts @@ -1,64 +1,78 @@ import React from 'react'; -import useTrustyAINamespaceCR, { - isTrustyAIAvailable, - taiHasServerTimedOut, -} from '~/concepts/trustyai/useTrustyAINamespaceCR'; -import { createTrustyAICR, deleteTrustyAICR } from '~/api'; +import useTrustyAINamespaceCR from '~/concepts/trustyai/useTrustyAINamespaceCR'; +import { + assembleSecret, + createSecret, + createTrustyAICR, + deleteSecret, + deleteTrustyAICR, +} from '~/api'; +import { getTrustyStatusState } from '~/concepts/trustyai/utils'; +import { TRUSTYAI_SECRET_NAME } from '~/concepts/trustyai/const'; +import useTrustyBrowserStorage from '~/concepts/trustyai/content/useTrustyBrowserStorage'; +import { TrustyDBData, TrustyStatusStates } from './types'; + +export type UseManageTrustyAICRReturnType = { + statusState: TrustyStatusStates; + installCRForNewDB: (secretData: TrustyDBData) => Promise; + installCRForExistingDB: (secretName: string) => Promise; + deleteCR: () => Promise; +}; const useManageTrustyAICR = (namespace: string): UseManageTrustyAICRReturnType => { const state = useTrustyAINamespaceCR(namespace); - const [cr, loaded, serviceError, refresh] = state; - - const [installReqError, setInstallReqError] = React.useState(); + const [cr, , , refresh] = state; + const successDetails = useTrustyBrowserStorage(cr); - const isAvailable = isTrustyAIAvailable(state); - const isProgressing = loaded && !!cr && !isAvailable; - const error = installReqError || serviceError; + const statusState = getTrustyStatusState(state, successDetails); - const [disableTimeout, setDisableTimeout] = React.useState(false); - const serverTimedOut = !disableTimeout && taiHasServerTimedOut(state, isAvailable); - const ignoreTimedOut = React.useCallback(() => { - setDisableTimeout(true); - }, []); + const installCRForExistingDB = React.useCallback< + UseManageTrustyAICRReturnType['installCRForExistingDB'] + >( + async (secretName) => { + await createTrustyAICR(namespace, secretName).then(refresh); + }, + [namespace, refresh], + ); + const installCRForNewDB = React.useCallback( + async (data) => { + const submitNewSecret = async (dryRun: boolean) => { + await Promise.all([ + createSecret(assembleSecret(namespace, data, 'generic', TRUSTYAI_SECRET_NAME), { + dryRun, + }), + createTrustyAICR(namespace, TRUSTYAI_SECRET_NAME, { dryRun }), + ]); + }; - const showSuccess = React.useRef(false); - if (isProgressing) { - showSuccess.current = true; - } + await submitNewSecret(true); + await submitNewSecret(false); + await refresh(); + }, + [namespace, refresh], + ); - const installCR = React.useCallback(async () => { - await createTrustyAICR(namespace) - .then(refresh) - .catch((e) => setInstallReqError(e)); - }, [namespace, refresh]); + const deleteCR = React.useCallback(async () => { + let deleteGeneratedSecret = false; + if (cr?.spec.storage.format === 'DATABASE') { + if (cr.spec.storage.databaseConfigurations === TRUSTYAI_SECRET_NAME) { + deleteGeneratedSecret = true; + } + } - const deleteCR = React.useCallback(async () => { - await deleteTrustyAICR(namespace).then(refresh); - }, [namespace, refresh]); + await deleteTrustyAICR(namespace); + if (deleteGeneratedSecret) { + await deleteSecret(namespace, TRUSTYAI_SECRET_NAME); + } + await refresh(); + }, [cr, namespace, refresh]); return { - error, - isProgressing, - isAvailable, - showSuccess: showSuccess.current, - isSettled: loaded, - serverTimedOut, - ignoreTimedOut, - installCR, + statusState, + installCRForExistingDB, + installCRForNewDB, deleteCR, }; }; export default useManageTrustyAICR; - -type UseManageTrustyAICRReturnType = { - error: Error | undefined; - isProgressing: boolean; - isAvailable: boolean; - showSuccess: boolean; - isSettled: boolean; - serverTimedOut: boolean; - ignoreTimedOut: () => void; - installCR: () => Promise; - deleteCR: () => Promise; -}; diff --git a/frontend/src/concepts/trustyai/useTrustyAINamespaceCR.ts b/frontend/src/concepts/trustyai/useTrustyAINamespaceCR.ts index f8d1182200..4db5aa1856 100644 --- a/frontend/src/concepts/trustyai/useTrustyAINamespaceCR.ts +++ b/frontend/src/concepts/trustyai/useTrustyAINamespaceCR.ts @@ -6,33 +6,13 @@ import useFetchState, { } from '~/utilities/useFetchState'; import { TrustyAIKind } from '~/k8sTypes'; import { getTrustyAICR } from '~/api'; -import { FAST_POLL_INTERVAL, SERVER_TIMEOUT } from '~/utilities/const'; +import { FAST_POLL_INTERVAL } from '~/utilities/const'; import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; +import { getTrustyStatusState } from '~/concepts/trustyai/utils'; +import { TrustyInstallState } from '~/concepts/trustyai/types'; type State = TrustyAIKind | null; -export const isTrustyCRStatusAvailable = (cr: TrustyAIKind): boolean => - !!cr.status?.conditions?.find((c) => c.type === 'Available' && c.status === 'True'); - -export const isTrustyAIAvailable = ([state, loaded]: FetchState): boolean => - loaded && !!state && isTrustyCRStatusAvailable(state); - -export const taiHasServerTimedOut = ( - [state, loaded]: FetchState, - isLoaded: boolean, -): boolean => { - if (!state || !loaded || isLoaded) { - return false; - } - - const createTime = state.metadata.creationTimestamp; - if (!createTime) { - return false; - } - // If we are here, and 5 mins have past, we are having issues - return Date.now() - new Date(createTime).getTime() > SERVER_TIMEOUT; -}; - const useTrustyAINamespaceCR = (namespace: string): FetchState => { const trustyAIAreaAvailable = useIsAreaAvailable(SupportedArea.TRUSTY_AI).status; @@ -53,18 +33,22 @@ const useTrustyAINamespaceCR = (namespace: string): FetchState => { [namespace, trustyAIAreaAvailable], ); - const [isStarting, setIsStarting] = React.useState(false); + const [needFastRefresh, setNeedFastRefresh] = React.useState(false); const state = useFetchState(callback, null, { initialPromisePurity: true, - refreshRate: isStarting ? FAST_POLL_INTERVAL : undefined, + refreshRate: needFastRefresh ? FAST_POLL_INTERVAL : undefined, }); - const resourceLoaded = state[1] && !!state[0]; - const hasStatus = isTrustyAIAvailable(state); + const installState = getTrustyStatusState(state); + const isProgressing = [ + TrustyInstallState.INSTALLING, + TrustyInstallState.UNINSTALLING, + TrustyInstallState.ERROR, + ].includes(installState.type); React.useEffect(() => { - setIsStarting(resourceLoaded && !hasStatus); - }, [hasStatus, resourceLoaded]); + setNeedFastRefresh(isProgressing); + }, [isProgressing]); return state; }; diff --git a/frontend/src/concepts/trustyai/utils.ts b/frontend/src/concepts/trustyai/utils.ts index e160bd8e94..af15cdfb69 100644 --- a/frontend/src/concepts/trustyai/utils.ts +++ b/frontend/src/concepts/trustyai/utils.ts @@ -1,5 +1,13 @@ import { BaseMetricListResponse } from '~/api'; -import { BiasMetricConfig } from '~/concepts/trustyai/types'; +import { + BiasMetricConfig, + TrustyInstallState, + TrustyStatusStates, +} from '~/concepts/trustyai/types'; +import { FetchState } from '~/utilities/useFetchState'; +import { TrustyAIKind } from '~/k8sTypes'; +import { getConditionForType } from '~/concepts/k8s/utils'; +import { UseTrustyBrowserStorage } from '~/concepts/trustyai/content/useTrustyBrowserStorage'; export const formatListResponse = (x: BaseMetricListResponse): BiasMetricConfig[] => x.requests.map((m) => ({ @@ -15,3 +23,60 @@ export const formatListResponse = (x: BaseMetricListResponse): BiasMetricConfig[ thresholdDelta: m.request.thresholdDelta, unprivilegedAttribute: m.request.unprivilegedAttribute.value, })); + +export const getTrustyStatusState = ( + crFetchState: FetchState, + successDetails?: UseTrustyBrowserStorage, +): TrustyStatusStates => { + const [cr, loaded, error] = crFetchState; + + if (error) { + return { type: TrustyInstallState.ERROR, message: error.message }; + } + + if (!loaded) { + return { type: TrustyInstallState.LOADING_INITIAL_STATE }; + } + + if (!cr) { + // No CR, uninstalled + return { type: TrustyInstallState.UNINSTALLED }; + } + + if (cr.metadata.deletionTimestamp) { + return { type: TrustyInstallState.UNINSTALLING }; + } + + // Have a CR, getting specific state + const availableCondition = getConditionForType(cr, 'Available'); + if (availableCondition?.status === 'True' && cr.status?.phase === 'Ready') { + // Installed and good to go + return { + type: TrustyInstallState.INSTALLED, + showSuccess: !!successDetails?.showSuccess, + onDismissSuccess: successDetails?.onDismissSuccess, + }; + } + + const dbAvailableCondition = getConditionForType(cr, 'DBAvailable'); + if (dbAvailableCondition?.status === 'False') { + // Some sort of DB error -- try to show specifically what it is + return { + type: TrustyInstallState.ERROR, + message: `${dbAvailableCondition.reason ?? 'Unknown reason'}: ${ + dbAvailableCondition.message ?? 'Unknown error' + }`, + }; + } + + if (availableCondition?.status === 'False') { + // Try to present the generic error as one last fallback + return { + type: TrustyInstallState.ERROR, + message: availableCondition.message ?? availableCondition.reason ?? 'Unknown available error', + }; + } + + // Not ready -- installing? -- wait for next update + return { type: TrustyInstallState.INSTALLING }; +}; diff --git a/frontend/src/k8sTypes.ts b/frontend/src/k8sTypes.ts index 4e9ecf35af..bcbab1b028 100644 --- a/frontend/src/k8sTypes.ts +++ b/frontend/src/k8sTypes.ts @@ -603,12 +603,17 @@ export type TrustyAIKind = K8sResourceCommon & { namespace: string; }; spec: { - storage: { - format: string; - folder: string; - size: string; - }; - data: { + storage: + | { + format: 'DATABASE'; + databaseConfigurations: string; + } + | { + format: 'PVC'; + folder: string; + size: string; + }; + data?: { filename: string; format: string; }; diff --git a/frontend/src/pages/modelServing/screens/metrics/MetricsPage.tsx b/frontend/src/pages/modelServing/screens/metrics/MetricsPage.tsx index ba4f0666a9..eddcf86fad 100644 --- a/frontend/src/pages/modelServing/screens/metrics/MetricsPage.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/MetricsPage.tsx @@ -10,6 +10,7 @@ import { PerformanceMetricType } from '~/pages/modelServing/screens/types'; import { TrustyAIContext } from '~/concepts/trustyai/context/TrustyAIContext'; import ServerMetricsPage from '~/pages/modelServing/screens/metrics/performance/ServerMetricsPage'; import { InferenceServiceKind } from '~/k8sTypes'; +import { TrustyInstallState } from '~/concepts/trustyai/types'; import { getBreadcrumbItemComponents } from './utils'; type MetricsPageProps = { @@ -23,10 +24,7 @@ const MetricsPage: React.FC = ({ title, breadcrumbItems, type, const { tab } = useParams(); const navigate = useNavigate(); - const { - hasCR, - apiState: { apiAvailable }, - } = React.useContext(TrustyAIContext); + const { statusState } = React.useContext(TrustyAIContext); return ( = ({ title, breadcrumbItems, type, headerAction={ tab === MetricsTabKeys.BIAS && ( } - loaded={loaded} + loaded={isInstalled} provideChildrenPadding empty={biasMetricConfigs.length === 0} emptyStatePage={ diff --git a/frontend/src/pages/modelServing/screens/metrics/bias/BiasMetricConfigSelector.tsx b/frontend/src/pages/modelServing/screens/metrics/bias/BiasMetricConfigSelector.tsx index 1327da0275..c1931c9bbf 100644 --- a/frontend/src/pages/modelServing/screens/metrics/bias/BiasMetricConfigSelector.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/bias/BiasMetricConfigSelector.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { useModelBiasData } from '~/concepts/trustyai/context/useModelBiasData'; -import { BiasMetricConfig } from '~/concepts/trustyai/types'; +import { BiasMetricConfig, TrustyInstallState } from '~/concepts/trustyai/types'; import { BiasMetricType } from '~/api'; import { MultiSelection, SelectionOptions } from '~/components/MultiSelection'; @@ -13,7 +13,7 @@ const BiasMetricConfigSelector: React.FC = ({ onChange, initialSelections, }) => { - const { biasMetricConfigs, loaded } = useModelBiasData(); + const { biasMetricConfigs, statusState } = useModelBiasData(); const [uiSelections, setUISelections] = React.useState(); const [currentSelections, setCurrentSelections] = React.useState(); const elementId = React.useId(); @@ -74,7 +74,9 @@ const BiasMetricConfigSelector: React.FC = ({ selectionRequired noSelectedOptionsMessage="One or more groups must be seleted" placeholder="Select a metric" - isDisabled={!(loaded && biasMetricConfigs.length > 0)} + isDisabled={ + !(statusState.type === TrustyInstallState.INSTALLED && biasMetricConfigs.length > 0) + } id="bias-metric-config-selector" toggleId="bias-metric-config-selector" /> diff --git a/frontend/src/pages/modelServing/screens/metrics/bias/BiasTab.tsx b/frontend/src/pages/modelServing/screens/metrics/bias/BiasTab.tsx index 7a862fa7df..f67f2cd1dc 100644 --- a/frontend/src/pages/modelServing/screens/metrics/bias/BiasTab.tsx +++ b/frontend/src/pages/modelServing/screens/metrics/bias/BiasTab.tsx @@ -25,17 +25,18 @@ import DashboardExpandableSection from '~/concepts/dashboard/DashboardExpandable import useBiasChartSelections from '~/pages/modelServing/screens/metrics/bias/useBiasChartSelections'; import { ModelMetricType } from '~/pages/modelServing/screens/metrics/ModelServingMetricsContext'; import EnsureMetricsAvailable from '~/pages/modelServing/screens/metrics/EnsureMetricsAvailable'; +import { TrustyInstallState } from '~/concepts/trustyai/types'; const OPEN_WRAPPER_STORAGE_KEY_PREFIX = `odh.dashboard.xai.bias_metric_chart_wrapper_open`; const BiasTab: React.FC = () => { - const { biasMetricConfigs, loaded, loadError } = useModelBiasData(); + const { biasMetricConfigs, statusState } = useModelBiasData(); const [selectedBiasConfigs, setSelectedBiasConfigs, settled] = useBiasChartSelections(biasMetricConfigs); - const ready = loaded && settled; + const ready = statusState.type === TrustyInstallState.INSTALLED && settled; - if (loadError) { + if (statusState.type === TrustyInstallState.ERROR) { return ( @@ -47,7 +48,7 @@ const BiasTab: React.FC = () => { We encountered an error accessing the TrustyAI service: - {loadError.message} + {statusState.message} diff --git a/frontend/src/pages/projects/projectSettings/ModelBiasSettingsCard.tsx b/frontend/src/pages/projects/projectSettings/ModelBiasSettingsCard.tsx index 4e16dbc1f3..a414fb7fc7 100644 --- a/frontend/src/pages/projects/projectSettings/ModelBiasSettingsCard.tsx +++ b/frontend/src/pages/projects/projectSettings/ModelBiasSettingsCard.tsx @@ -1,31 +1,39 @@ -import { Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core'; import React from 'react'; -import TrustyAIServiceControl from '~/concepts/trustyai/content/TrustyAIServiceControl'; -import { KnownLabels, ProjectKind } from '~/k8sTypes'; -import { TRUST_AI_NOT_SUPPORTED_TEXT } from '~/pages/projects/projectSettings/const'; +import { + Card, + CardBody, + CardFooter, + CardHeader, + CardTitle, + Stack, + StackItem, +} from '@patternfly/react-core'; +import { ProjectKind } from '~/k8sTypes'; +import useTrustyCRState from '~/concepts/trustyai/content/useTrustyCRState'; type ModelBiasSettingsCardProps = { project: ProjectKind; }; const ModelBiasSettingsCard: React.FC = ({ project }) => { - const namespace = project.metadata.name; - - const isTrustySupported = project.metadata.labels?.[KnownLabels.MODEL_SERVING_PROJECT] === 'true'; - - const disabledReason = isTrustySupported ? undefined : TRUST_AI_NOT_SUPPORTED_TEXT; + const { action, status } = useTrustyCRState(project); return ( - + - Model bias + Model monitoring bias - + To ensure that machine-learning models are transparent, fair, and reliable, data scientists + can use TrustyAI to monitor their data science models. TrustyAI is an open-source AI + Explainability (XAI) Toolkit that offers comprehensive explanations of predictive models in + both enterprise and data science applications. + + + {action} + {status && {status}} + + ); }; diff --git a/frontend/src/pages/projects/projectSettings/const.ts b/frontend/src/pages/projects/projectSettings/const.ts deleted file mode 100644 index 9cee5909a6..0000000000 --- a/frontend/src/pages/projects/projectSettings/const.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const TRUSTYAI_TOOLTIP_TEXT = - 'Install TrustyAI, which uses data from ModelMesh to calculate and display model bias over time, in your namespace.'; - -export const TRUST_AI_NOT_SUPPORTED_TEXT = - 'Model bias monitoring is only available when a multi-model serving platform is enabled for the project.';