import { Button } from '@dynatrace/strato-components/buttons'; import { EmptyState, MessageContainer, ProgressCircle, } from '@dynatrace/strato-components/content'; import { AppRoot } from '@dynatrace/strato-components/core'; import { FormField, Label, NumberInputV2, Select, Switch, TextInput, } from '@dynatrace/strato-components/forms'; import { Divider, Flex } from '@dynatrace/strato-components/layouts'; import { showToast, ToastContainer, } from '@dynatrace/strato-components/notifications'; import { Heading, Paragraph, Text, } from '@dynatrace/strato-components/typography'; import { appSettingsObjectsClient, isErrorResponseError, } from '@dynatrace-sdk/client-app-settings-v2'; import React, { useEffect, useState } from 'react'; import { createRoot } from 'react-dom/client'; // Matches the schemaId generated by "dt-app setting create" (-connection). const SCHEMA_ID = 'setting-name-connection'; const FORBIDDEN_STATUS = 403; // Schema not yet registered on this environment (app not yet deployed). const NOT_FOUND_STATUS = 404; type Environment = 'development' | 'staging' | 'production'; interface SettingsValues { name: string; enabled: boolean; environment: Environment; retentionDays: number; } const DEFAULT_VALUES: SettingsValues = { name: '', enabled: true, environment: 'production', retentionDays: 30, }; // `undefined` means "still loading" — no indicator is rendered for that state. type SaveState = 'saving' | 'saved' | 'unsavedChanges' | undefined; /** Section block: title + optional description line + divider, then fields. */ const SettingsSection = ({ title, description, children, }: { title: string; description?: string; children: React.ReactNode; }) => ( {title} {description && {description}} {children} ); const SettingNameSettingsWidget = () => { const [values, setValues] = useState(DEFAULT_VALUES); const [savedValues, setSavedValues] = useState(DEFAULT_VALUES); const [objectId, setObjectId] = useState(); const [version, setVersion] = useState(); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const [loadError, setLoadError] = useState(false); const [hasReadAccess, setHasReadAccess] = useState(true); const [isReadonly, setIsReadonly] = useState(false); useEffect(() => { loadSettings(); }, []); const loadSettings = async () => { setIsLoading(true); setLoadError(false); try { // Default response fields are only "objectId" and "version" — addFields opts in the rest. const { items } = await appSettingsObjectsClient.getAppSettingsObjects({ schemaId: SCHEMA_ID, addFields: 'value,resourceContext', }); // "multiObject": false means at most one object is ever persisted for this schema. const [object] = items; if (object) { const loadedValues = { ...DEFAULT_VALUES, ...(object.value as Partial), }; setValues(loadedValues); setSavedValues(loadedValues); setObjectId(object.objectId); setVersion(object.version); setIsReadonly( object.resourceContext?.operations.includes('write') === false, ); } } catch (error) { if ( isErrorResponseError(error) && error.response.status === FORBIDDEN_STATUS ) { setHasReadAccess(false); } else if ( isErrorResponseError(error) && error.response.status === NOT_FOUND_STATUS ) { // Schema not registered yet — treat as no object; first Save will fail until deployed. } else { setLoadError(true); } } finally { setIsLoading(false); } }; const isDirty = JSON.stringify(values) !== JSON.stringify(savedValues); const saveState: SaveState = isLoading ? undefined : isSaving ? 'saving' : isDirty ? 'unsavedChanges' : 'saved'; const save = async () => { setIsSaving(true); try { if (objectId) { // The locking token is the object's "version" field — app-settings-v2 has no "updateToken". const updated = await appSettingsObjectsClient.putAppSettingsObjectByObjectId({ objectId, optimisticLockingVersion: version ?? '', body: { value: values }, }); setVersion(updated.version); } else { // No object exists yet for this schema — create instead of update. const created = await appSettingsObjectsClient.postAppSettingsObject({ body: { schemaId: SCHEMA_ID, value: values }, }); if (created) { setObjectId(created.objectId); setVersion(created.version); } } setSavedValues(values); showToast({ title: 'Settings saved', type: 'success' }); } catch { // Keep the typed values — losing input on a transient save failure is the worst outcome. showToast({ title: 'Failed to save settings', type: 'critical' }); } finally { setIsSaving(false); } }; const discard = () => setValues(savedValues); return ( // Dev-mode padding prevents the widget from sticking to the window edges during local dev. [Settings Template] This is a template for new settings widgets. Replace this text with a description of what these settings control. {!hasReadAccess ? ( Missing permission You do not have permission to view these settings. ) : ( {isReadonly && ( Read-only You do not have permission to change these settings. )} {isLoading ? ( ) : loadError ? ( Unable to load settings Reload the widget to try again. No changes have been saved. ) : ( {isDirty && !isReadonly && ( Unsaved changes Save your changes or discard them. )} {/* Sections are separated by a larger gap to create visual breathing room. */} setValues({ ...values, name: value }) } disabled={isReadonly} placeholder="Enter a name" /> setValues({ ...values, enabled: checked }) } disabled={isReadonly} /> setValues({ ...values, retentionDays: value ?? DEFAULT_VALUES.retentionDays, }) } disabled={isReadonly} min={0} step={1} /> )} )} ); }; /** Maps the computed save state to the title bar indicator; suppressed entirely when read-only. */ const SaveIndicator = ({ saveState, isReadonly, }: { saveState: SaveState; isReadonly: boolean; }) => { if (isReadonly || saveState === undefined) { return undefined; } if (saveState === 'saving') { return ( Saving… ); } return ( {saveState === 'saved' ? 'All changes saved' : 'Unsaved changes'} ); }; // Settings widgets are their own entry point — they mount themselves into `#root`. // The guard keeps the module importable from tests, where no `#root` element exists. const rootElement = document.getElementById('root'); if (rootElement) { createRoot(rootElement).render( , ); } export default SettingNameSettingsWidget;