Skip to main content

Build a settings form for your app

  • New
  • How-to guide
  • 10-min read

Give your app's users a settings form. The form renders inside the app and as a widget in Settings, the same component handles both, writing the UI once. The form uses the app settings React hooks from @dynatrace-sdk/react-hooks, which is already part of the app template and is built on the @dynatrace-sdk/client-app-settings-v2 SDK. You must add the app-settings:objects:read and app-settings:objects:write scopes to your app.

Prerequisites​

Tip

This how-to requires an existing app settings schema. To learn how schemas work, see Store app settings.

The form in this guide edits a single property, url, so the examples use a matching single-property schema. Save this as settings/schemas/my-example.schema.json:

settings/schemas/my-example.schema.json
{
"$schema": "https://developer.dynatrace.com/docs-assets/schema_strict_apps.json",
"dynatrace": "1",
"schemaId": "my-example",
"version": "1.0.0",
"displayName": "Messaging service connection",
"description": "",
"multiObject": false,
"maturity": "GENERAL_AVAILABILITY",
"ownerBasedAccessControl": false,
"properties": {
"url": {
"displayName": "URL",
"description": "The URL of the messaging service",
"type": "text",
"default": "",
"nullable": false
}
}
}

A write replaces the whole settings object, so the form must send a value for every nullable: false property the schema declares. A single url property keeps the form simple. For schemas with more properties, or when reusing a richer schema such as the full my-example schema from Store app settings, extend the form to match. Otherwise the save fails with 400 | The input is invalid.

The schema is identified in the SDK by its plain ID, my-example—the value set in the schema file. The SDK resolves it against the app, so the fully qualified app:<your-app-id>:my-example form is never needed.

Caution

Your schema file must live in the settings/schemas/ directory at the root of your app, and its name must end in .schema.json. The settings/schemas/ directory sits at the same level as ui/:

my-app/
├── settings/
│ └── schemas/
│ └── my-example.schema.json
└── ui/

A schema placed anywhere else, such as inside ui/, is never registered.

A schema also only exists once it's registered on the environment. While the app is at the local 0.0.0 version, reading or writing its settings fails with Schema '<id>' not found for app version '0.0.0'. To work on the form without deploying, mock the settings locally. Install @dynatrace-sdk/dt-app-plugin-client-app-settings-v2 as a dev dependency, add it to plugins in app.config.json, and start npx dt-app dev. The plugin then generates settings/local-mock-data/values.json, where the mock objects for the form are defined. Otherwise, deploy the app with npx dt-app deploy. For more information, see Local development.

Create the settings form​

The following component is a text field with a Save button, built with Strato components. The form has no connection to settings yet; the next steps add reading and writing.

ui/app/SettingsForm.tsx
import React, { useState } from 'react';
import { Button } from '@dynatrace/strato-components/buttons';
import { FormField, Label, TextInput } from '@dynatrace/strato-components/forms';
import { Flex } from '@dynatrace/strato-components/layouts';

export const SettingsForm = () => {
const [url, setUrl] = useState('');

return (
<Flex flexDirection="column" gap={16}>
<FormField>
<Label>Service URL</Label>
<TextInput value={url} onChange={setUrl} placeholder="https://example.com" />
</FormField>
<Button variant="accent">Save</Button>
</Flex>
);
};

To preview the form during development, render it on a page. For example, replace the contents of ui/app/pages/Home.tsx, which in a fresh app holds the generated welcome page:

ui/app/pages/Home.tsx
import React from 'react';
import { SettingsForm } from '../SettingsForm';

export const Home = () => {
return <SettingsForm />;
};

To see the form, run:

npx dt-app dev

Read the stored value​

The hooks come from @dynatrace-sdk/react-hooks, which is already part of the app template, so there's nothing to install. To call the client SDK directly instead, install it by running:

npm i @dynatrace-sdk/client-app-settings-v2

The form reads and writes settings, so declare the two app settings scopes in the app.config.json file. Keep any scopes the app already has, such as the storage:* scopes, from the default template:

app.config.json
"scopes": [
{ "name": "app-settings:objects:read", "comment": "Read app settings" },
{ "name": "app-settings:objects:write", "comment": "Write app settings" }
]

Use the useSettingsObjectsV2 hook from @dynatrace-sdk/react-hooks to fetch the stored objects for the schema. Include value in addFields so the response includes the stored values, and prefill the form once the data loads:

ui/app/SettingsForm.tsx
import React, { useEffect, useState } from 'react';
import { useSettingsObjectsV2 } from '@dynatrace-sdk/react-hooks';
import { Button } from '@dynatrace/strato-components/buttons';
import { ProgressCircle } from '@dynatrace/strato-components/content';
import { FormField, Label, TextInput } from '@dynatrace/strato-components/forms';
import { Flex } from '@dynatrace/strato-components/layouts';

const SCHEMA_ID = 'my-example';

export const SettingsForm = () => {
const { data, isLoading, error } = useSettingsObjectsV2({ schemaId: SCHEMA_ID, addFields: 'value' });
const settingsObject = data?.items[0];
const [url, setUrl] = useState('');

useEffect(() => {
setUrl((settingsObject?.value?.url as string) ?? '');
}, [settingsObject]);

if (isLoading) {
return <ProgressCircle />;
}

if (error) {
return <p>Error! {error.message}</p>;
}

return (
<Flex flexDirection="column" gap={16}>
<FormField>
<Label>Service URL</Label>
<TextInput value={url} onChange={setUrl} placeholder="https://example.com" />
</FormField>
<Button variant="accent">Save</Button>
</Flex>
);
};
Tip

This operation requires the scope app-settings:objects:read. Read more about scopes in this guide.

Note

To use await in React components, you need to wrap the asynchronous invocation in an async function. Read more about it in this guide.

Save changes​

There's no single save call. If the schema has no object yet, create one with useCreateSettingsV2. If an object exists, update it by objectId with useUpdateSettingsV2.

Updates require the object's current version as optimisticLockingVersion. If someone else saved the same object in the meantime, the call fails instead of overwriting their change. After each save, call refetch to get the new version before the next save.

ui/app/SettingsForm.tsx
import React, { useEffect, useState } from 'react';
import { useCreateSettingsV2, useSettingsObjectsV2, useUpdateSettingsV2 } from '@dynatrace-sdk/react-hooks';
import { Button } from '@dynatrace/strato-components/buttons';
import { ProgressCircle } from '@dynatrace/strato-components/content';
import { FormField, Label, TextInput } from '@dynatrace/strato-components/forms';
import { Flex } from '@dynatrace/strato-components/layouts';

const SCHEMA_ID = 'my-example';

export const SettingsForm = () => {
const { data, isLoading, error, refetch } = useSettingsObjectsV2({ schemaId: SCHEMA_ID, addFields: 'value' });
const { execute: createSettings } = useCreateSettingsV2();
const { execute: updateSettings } = useUpdateSettingsV2();
const settingsObject = data?.items[0];
const [url, setUrl] = useState('');

useEffect(() => {
setUrl((settingsObject?.value?.url as string) ?? '');
}, [settingsObject]);

const handleSave = async () => {
if (settingsObject) {
await updateSettings({
objectId: settingsObject.objectId,
optimisticLockingVersion: settingsObject.version,
body: { value: { url } },
});
} else {
await createSettings({ body: { schemaId: SCHEMA_ID, value: { url } } });
}

await refetch();
};

if (isLoading) {
return <ProgressCircle />;
}

if (error) {
return <p>Error! {error.message}</p>;
}

return (
<Flex flexDirection="column" gap={16}>
<FormField>
<Label>Service URL</Label>
<TextInput value={url} onChange={setUrl} placeholder="https://example.com" />
</FormField>
<Button variant="accent" onClick={() => void handleSave()}>
Save
</Button>
</Flex>
);
};
Tip

This operation requires the following scopes:

  • app-settings:objects:read
  • app-settings:objects:write
Read more about scopes in this guide.

Note

To use await in React components, you need to wrap the asynchronous invocation in an async function. Read more about it in this guide.

Note

useSettingsObjectsV2 returns only stored objects, which is what a form needs. To read a value elsewhere in the app, or get the schema defaults before anything is stored, use useSettingsV2. For more information, see how to Get effective settings value.

Disable the form for read-only users​

Not every user who can open the app has permission to change its settings. If the form stays active for them, they only find out when they fill it in, select Save, and the request fails. Detecting the read-only case upfront avoids that.

The standard pattern is to disable the form fields and the Save button, and display a message explaining that the user has read-only permissions.

Cover both cases:

  • When an object exists, request resourceContext alongside the value. Its operations array lists what the current user may do with the object. The user is read-only when it doesn't include write.
  • When no object exists yet, there's no resourceContext to read, so a check based on it alone would incorrectly disable the form and prevent the user from creating the first object. Fall back to the schema-level write permission through useEffectivePermissionsV2 instead. For more information, see how to Get effective settings permissions.

The complete form includes the permission check, the read-only messaging, and error handling on top of the save changes setup.

ui/app/SettingsForm.tsx
import React, { useEffect, useState } from 'react';
import {
useCreateSettingsV2,
useEffectivePermissionsV2,
useSettingsObjectsV2,
useUpdateSettingsV2,
} from '@dynatrace-sdk/react-hooks';
import { Button } from '@dynatrace/strato-components/buttons';
import { ProgressCircle } from '@dynatrace/strato-components/content';
import { FormField, Label, TextInput } from '@dynatrace/strato-components/forms';
import { Flex } from '@dynatrace/strato-components/layouts';
import { Text } from '@dynatrace/strato-components/typography';

const SCHEMA_ID = 'my-example';

export const SettingsForm = () => {
const { data, isLoading, error, refetch } = useSettingsObjectsV2({
schemaId: SCHEMA_ID,
addFields: 'value, resourceContext',
});
const { execute: createSettings } = useCreateSettingsV2();
const { execute: updateSettings } = useUpdateSettingsV2();
const settingsObject = data?.items[0];
const [url, setUrl] = useState('');
const [saveError, setSaveError] = useState<string>();

useEffect(() => {
setUrl((settingsObject?.value?.url as string) ?? '');
}, [settingsObject]);

const { data: permissions } = useEffectivePermissionsV2({
body: {
permissions: [{ permission: 'app-settings:objects:write', context: { schemaId: SCHEMA_ID } }],
},
});
const canCreate = permissions?.[0].granted !== 'false';
const isReadonly = settingsObject ? !settingsObject.resourceContext?.operations.includes('write') : !canCreate;

const handleSave = async () => {
setSaveError(undefined);
try {
if (settingsObject) {
await updateSettings({
objectId: settingsObject.objectId,
optimisticLockingVersion: settingsObject.version,
body: { value: { url } },
});
} else {
await createSettings({ body: { schemaId: SCHEMA_ID, value: { url } } });
}
await refetch();
} catch (e) {
setSaveError(e instanceof Error ? e.message : 'Could not save the settings.');
}
};

if (isLoading) {
return <ProgressCircle />;
}

if (error) {
return <p>Error! {error.message}</p>;
}

return (
<Flex flexDirection="column" gap={16}>
{isReadonly && <Text>You have read-only permissions for these settings.</Text>}
<FormField>
<Label>Service URL</Label>
<TextInput value={url} onChange={setUrl} placeholder="https://example.com" disabled={isReadonly} />
</FormField>
{saveError && <Text>{saveError}</Text>}
<Button variant="accent" onClick={() => void handleSave()} disabled={isReadonly}>
Save
</Button>
</Flex>
);
};
Note

Permissions aren't the only reason a save can fail. The schema can declare constraints, such as a required field or a value pattern, that the backend validates on write. A save can be rejected even for a user with write access. The try/catch around handleSave catches these rejections and shows the message instead of letting the save fail silently. In a real app, show it through a more prominent notification.

Expose the form in Settings​

Register the form as a settings widget so you can find it in Settings. The widget reuses the same component from the previous steps.

Add a settings block to the app.config.json file. The key is the widget ID, and schemaIds lists the plain schema IDs it edits. The scopes are the same ones declared earlier in this guide. Merge the highlighted settings block into the existing config rather than pasting the whole sample over it, to keep the other fields and scopes.

app.config.json
{
"environmentUrl": "<Your-Environment-URL>",
"app": {
"id": "<Your-App-ID>",
"name": "<Your-App-Name>",
"version": "0.0.0",
"description": "<Your-App-Description>",
"scopes": [
{ "name": "app-settings:objects:read", "comment": "Read app settings" },
{ "name": "app-settings:objects:write", "comment": "Write app settings" }
],
"settings": {
"my-example": {
"name": "Messaging service",
"description": "Configure the connection to your messaging service",
"schemaIds": ["my-example"]
}
}
}
}

Add a widget entry point in the root-level settings folder, named after the widget ID. The widget entry point renders the form component.

settings/my-example.widget.tsx
import { AppRoot } from '@dynatrace/strato-components/core';
import React from 'react';
import * as ReactDom from 'react-dom/client';
import { SettingsForm } from '../ui/app/SettingsForm';

const rootElement = document.getElementById('root');
if (rootElement) {
const root = ReactDom.createRoot(rootElement);

root.render(
<AppRoot>
<SettingsForm />
</AppRoot>,
);
} else {
console.error('The root element is not defined');
}

Run npx dt-app dev and confirm when the CLI offers to create the tsconfig.widget.json file.

Run npx dt-app deploy. The widget is detected on the filesystem and shipped with the app artifact.

Open Settings, select Apps, and select your widget.

Every registered widget appears under the top-level Apps category in Settings automatically. No additional placement configuration is needed.

Reference​

App settings are always environment-wide​

Every object the schema stores applies to the whole environment. App settings have no scope parameter and no per-entity variant. None of the client-app-settings-v2 operations take a scope, so there's nothing scope-related to handle in the form.

Your widget is also only ever reached at the environment scope. The Apps category in Settings appears only there, not when an entity such as a host is selected. A user can't open your widget against a narrower scope, so there's no cross-scope case to guard against. Entity-scoped configuration belongs to Dynatrace's own built-in settings schemas, which apps can't define.

Owner-based access control​

If the schema sets ownerBasedAccessControl to true, the user who first stores an object becomes its owner and, by default, is the only one who can read or write it. Objects the current user can't access don't appear in the response at all, which matters when the form lists multiple objects. To transfer ownership or grant access to other users and groups, see Owner-based access control.

Get ready for production​

The schema ships inside the app artifact and is registered automatically when the app is installed. Each app version carries a matching schema version—to ship a schema change, bump the app version in app.config.json.

During local development, the schema isn't registered yet, so reading or writing settings fails. Mock the values locally instead. For more information, see Local development.

Schemas with IN_DEVELOPMENT maturity are hidden from the Settings menu while you build, but your app can still read and write their objects. To make the widget visible to users, promote the schema to EARLY_ADOPTER or GENERAL_AVAILABILITY. See Schema maturity.

Still have questions?
Find answers in the Dynatrace Community