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.
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:
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:
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.
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:
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:
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:
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:
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.
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:
<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:
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:
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:
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:
<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:
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.
<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.
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
:
<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:
import {
AppHeader,
AppName,
Page,
Heading,
Grid,
Flex,
} from '@dynatrace/strato-components-preview';
The content within Page.Main
will look like this now:
<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.
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.