Skip to main content

Create a user interface

Prerequisites

To proceed, make sure that you have followed the previous steps and should now have:

  • A new project created with the "empty template"
  • Your local development server is running and serving the app.
  • You have opened your project within the IDE of your choice.
Video coming soon

Intro

You build a Dynatrace app UI with React and TypeScript. This tutorial doesn't explain all the needed concepts in detail. If you would like to learn more about these technologies, visit our learning resources on React and TypeScript. The Strato design system provides a React component library, which you can use to create user interfaces for Dynatrace Apps without building every component from scratch. For more details, visit our Design System reference page.

1. Set the stage

As a first step, change the generated code to match your requirements.

Go to the App.tsx file in src/app/, and change the text between <Heading> and </Heading> to Booking Overview and change the value of level to 1.

Remove the <Flex> and the <Paragraph> components and all unused imports; you don't need that for the app.

The local development server supports hot reload. Every change in the source code triggers a build of the app, and the browser window gets refreshed.

After this step, your file should look like the following:

src/app/App.tsx
import React from 'react';
import { AppHeader, AppName, Page, Heading } from '@dynatrace/strato-components-preview';

export const App = () => {
return (
<Page>
<Page.Header>
<AppHeader>
<AppName />
</AppHeader>
</Page.Header>
<Page.Main>
<Heading level={1}>Booking Overview</Heading>
</Page.Main>
</Page>
);
};

2. Create the Card component

You'll create a generic Card component based on the SingleValue component to render the queried data from Dynatrace.

To create a new component, create a new file first, Card.tsx. To follow best practices, create a components directory within src/app and place your file in it.

The Dynatrace platform uses functional components, meaning a React component is more or less just a JavaScript function returning JSX code that renders in your UI. Therefore, the skeleton of your new component looks like the following:

src/app/components/Card.tsx
import React from 'react';

export const Card = () => {
return <div></div>;
};

2.1. Add props to the Card component

You can specify so-called props (short for properties) to make a component dynamic. Props are arguments passed into React components. In this case, you need four props for the Card component:

  • value
  • chartLabel
  • chartPrecision
  • chartUnit (optional)

Every React component receives a props object as a parameter, including all the passed props as keys. Visit the React documentation to learn more about this concept.

As we use TypeScript in this app, create a CardProps interface to have type safety for all props passed into your component and get suggestions for props in your IDE, making every developer's life easier. And the interface definition is placed after the imports within the Card.tsx file.

src/app/components/Card.tsx
import { Unit } from '@dynatrace-sdk/units';

interface CardProps {
value: number;
chartLabel: string;
chartPrecision: number;
chartUnit?: Unit;
}

As soon as you've defined an interface, assign that interface to your props objects. Additionally, use the destructuring assignment syntax in this example, which is a JavaScript expression that makes it possible to unpack values from arrays, or properties from objects, into distinct variables:

src/app/components/Card.tsx
export const Card = ({ value, chartLabel, chartUnit, chartPrecision }: CardProps) => {
return <div>{value}</div>;
};

2.2. Adding SingleValue to the Card Component

The next step is to use the SingleValue component from the design system to render the data passed to the Card component via props. The first step is to import the component by adding the following line after the React import. Also, for styling later, import the Flex component:

src/app/components/Card.tsx
import { SingleValue, Flex } from '@dynatrace/strato-components-preview';

You can configure the SingleValue component by passing different props to it. Use the props of the Card component to pass them to the SingleValue. Since the chartUnit is optional, we set its default value so everything runs smoothly.

Add some conditional rendering logic to your component to only render the chart whenever the value is a valid number. You may embed expressions in JSX by wrapping them in curly braces, including the JavaScript logical && operator.

Finally, wrap everything in a Flex component, which we will use to style our Card component in the next step.

The Card component would then look like this:

src/app/components/Card.tsx
import { Unit, units } from '@dynatrace-sdk/units';

export const Card = ({ value, chartLabel, chartUnit = units.unspecified.none, chartPrecision }: CardProps) => {
const formatter = {
input: chartUnit,
maximumFractionDigits: chartPrecision,
};

return <Flex>{Number.isFinite(value) && <SingleValue data={value} label={chartLabel} formatter={formatter} />}</Flex>;
};

2.3. Style the Card component

Strato provides CSS Flex and Grid declarative components to help make layouts simple. The style prop on the Flex component will allow us to apply all kinds of CSS styling.

Add some styles via the style prop on the Flex component we added in the previous step. You can specify any CSS properties via the style prop. Use Strato design tokens and static values in this example. The provided design tokens give you a familiar look and feel for all your Dynatrace Apps.

Start by adding the following imports to your component:

src/app/components/Card.tsx
import { Borders, BoxShadows, Colors } from '@dynatrace/strato-design-tokens';

Add the following CSS properties to the Flex component to make it look like a card.

src/app/components/Card.tsx
export const Card = ({ value, chartLabel, chartUnit = units.unspecified.none, chartPrecision }: CardProps) => {
// ...

return (
<Flex
style={{
border: Colors.Border.Neutral.Default,
borderRadius: Borders.Radius.Container.Subdued,
background: Colors.Background.Surface.Default,
boxShadow: BoxShadows.Surface.Raised.Rest,
}}
>

</Flex>
);
};

Additionally, you need more CSS properties to define the size of the card and the positioning of the content in the card. Therefore, add width, height, alignItems, and justifyContent. After that, the Flex component will look like the following:

src/app/components/Card.tsx
<Flex
style={{
border: Colors.Border.Neutral.Default,
borderRadius: Borders.Radius.Container.Subdued,
background: Colors.Background.Surface.Default,
boxShadow: BoxShadows.Surface.Raised.Rest,
width: '400px',
height: '120px',
alignItems: 'center',
justifyContent: 'center',
}}
>

</Flex>

2.4. Render Card component in App.tsx

The first step is to import the Card component in the App.tsx file by adding the following line to the imports:

src/app/App.tsx
import { Card } from './components/Card';

To render the Card component, add it to the return statement of the App component. Pass some dummy values to the Card component to check if it renders correctly.

Your App.tsx file would now look like the following:

src/app/App.tsx
import React from 'react';
import { AppHeader, AppName, Page, Heading } from '@dynatrace/strato-components-preview';
import { Card } from './components/Card';

export const App = () => {
return (
<Page>
<Page.Header>
<AppHeader>
<AppName />
</AppHeader>
</Page.Header>
<Page.Main>
<Heading level={1}>Booking Overview</Heading>
<Card value={42} chartLabel="This is a label" chartPrecision={0} />
</Page.Main>
</Page>
);
};

export default App;

2.5. Add a Skeleton to the Card component

At this point, you should be able to see the data displayed on your Card component. Let's continue by adding a loading state.

The Skeleton component from the design system is the easiest way to achieve this. It indicates to users that fetched data is still loading. Instead of displaying the SingleValue, display the Skeleton while the data is still loading. As the Card component is only responsible for rendering data and has no information about the data fetching itself, you need to extend your CardProps with an isLoading prop, which will pass to the component from the outside.

To add a Skeleton to your Card component, adapt the CardProps interface and the function interface to look like the following:

src/app/components/Card.tsx
interface CardProps {
value: number;
chartLabel: string;
chartPrecision: number;
chartUnit?: Unit;
isLoading: boolean;
}

export const Card = ({
value,
chartLabel,
chartUnit = units.unspecified.none,
chartPrecision,
isLoading,
}: CardProps) => {
...
}

As this changed the API of your Card component, make sure to add the isLoading prop to the Card component in your App.tsx file. It would look like the following then:

src/app/App.tsx
<Card value={42} chartLabel="This is a label" chartPrecision={0} isLoading={false} />

In Card.tsx, you can now add the Skeleton component and render it only when needed. Start by adding it to the imports:

src/app/components/Card.tsx
import {
SingleValue,
Flex,
Skeleton,
} from '@dynatrace/strato-components-preview';

Next, add it to your component before the SingleValue. And don't forget to update the SingleValue condition to hide it while it's loading.

src/app/components/Card.tsx
<Flex
style={{
border: Colors.Border.Neutral.Default,
borderRadius: Borders.Radius.Container.Subdued,
background: Colors.Background.Surface.Default,
boxShadow: BoxShadows.Surface.Raised.Rest,
width: '400px',
height: '120px',
alignItems: 'center',
justifyContent: 'center',
}}
>
{isLoading && <Skeleton />}
{!isLoading && Number.isFinite(value) && <SingleValue data={value} label={chartLabel} formatter={formatter} />}
</Flex>

3. Render multiple Cards in your app

Within the last section of this page, render multiple instances of the Card component in your app. In this example, we will render four cards at this point, and add two more cards in the step Access external data. All in all, we will show six cards in a grid in the final state of your app.

Let's start by adding the design system's Grid component to your imports.

src/app/App.tsx
import {
AppHeader,
AppName,
Page,
Heading,
Grid,
} from '@dynatrace/strato-components-preview';

As the goal is to have two columns, you can specify this via the gridTemplateColumns prop. Now wrap your already existing Card component with a Grid component and add more instances of Card:

src/app/App.tsx
<Grid gap={32} gridTemplateColumns={'1fr 1fr'}>
<Card value={42} chartLabel="This is a label" chartPrecision={0} isLoading={false} />
<Card value={42} chartLabel="This is a label" chartPrecision={0} isLoading={false} />
<Card value={42} chartLabel="This is a label" chartPrecision={0} isLoading={false} />
<Card value={42} chartLabel="This is a label" chartPrecision={0} isLoading={false} />
</Grid>

To have all the content nicely centered, wrap all the content within Page.Main into a Flex component. Again, first import Flex from the design system:

src/app/App.tsx
import {
AppHeader,
AppName,
Page,
Heading,
Grid,
Flex,
} from '@dynatrace/strato-components-preview';

The content within Page.Main will look like this now:

src/app/App.tsx
<Flex gap={32} flexDirection="column" alignItems={'center'}>
<Heading level={1}>Booking Overview</Heading>
<Grid gap={32} gridTemplateColumns={'1fr 1fr'}>
<Card value={42} chartLabel="This is a label" chartPrecision={0} isLoading={false} />
<Card value={42} chartLabel="This is a label" chartPrecision={0} isLoading={false} />
<Card value={42} chartLabel="This is a label" chartPrecision={0} isLoading={false} />
<Card value={42} chartLabel="This is a label" chartPrecision={0} isLoading={false} />
</Grid>
</Flex>

Check grid or flex on MDN Web docs for further information on Grid and Flex and their behavior.

Checkpoint

For troubleshooting, visit checkpoint to see what your project should look like.

Summary and next steps

This section of the tutorial explained how you can build a basic UI on the Dynatrace platform with the help of Strato. In the next section, you'll learn how to fetch Dynatrace data and display it in your UI.

Still have questions?
Find answers in the Dynatrace Community