Create a dashboard

Create a Dashboard

Though you created a Dashboard page when starting with this tutorial, that page is still empty. It is time to create a dashboard with some information and charts.

đź“”

You can continue working with the code you completed in the previous tutorial section. Alternatively, you can download the code from here and carry on.

A Rudimentary Dashboard

The dashboard will display indicative numbers of significant review counts. These indicators will be housed in interactive cards. By clicking on a card, the app will navigate to the list of the corresponding contacts.

Create a new file, Dashboard.xmlui within the components folder:

Dashboard.xmlui
<Component name="Dashboard">
  <DataSource id="contactCounts" url="/api/contactcounts" />
  <FlowLayout var.counts="{contactCounts.value}">
    <H2>Overdue Reviews: {counts.overdue}</H2>
    <H2>Today's Reviews: {counts.today}</H2>
    <H2>Upcoming Reviews: {counts.upcoming}</H2>
    <H2>Completed Reviews: {counts.completed}</H2>
  </FlowLayout>
</Component>

The Dashboard component uses a DataSource with the /api/contactcounts endpoint (you used it earlier) to get some counter values to display. The counts variable allows referencing the individual counters shorter, for example, using counts.overdue instead of contactCounts.value.overdue.

Use this new component instead of the placeholder text within Main.xmlui:

Main.xmlui
<App layout="vertical-full-header">
  <!-- Omitted -->
  <Pages>
    <Page url="/">
      <Dashboard />
    </Page>
    <!-- Omitted -->
</App>

Earlier, you created a DataSource with the contactCounts ID, which retrieves several counters in an object. The dashboard displays this data.

When you navigate to the Dashboard page, it displays a some numeric information:


Rudimentary dashboard

Cards with Numbers

This user interface needs to look more exciting and helpful. Your next task will be to create a card that highlights useful numerical information. Start with the InfoCard component by adding the InfoCard.xmlui file to the components folder with the following code:

InfoCard.xmlui
<Component name="InfoCard">
  <Card height="{$props.height}">
    <Text variant="subtitle" value="{$props.title}"/>
    <CVStack width="100%">
      <H2 color="{$props.color}" fontSize="4em">
        {$props.isLoading ? "..." : $props.value}
      </H2>
    </CVStack>
  </Card>
</Component>

InforCard allows you to set its title text, color, and number to display. It also receives an isLoading property to display "..." while the counter data is being fetched from the backend.

Change the dashboard to use the InfoCard component:

Dashboard.xmlui
<Component name="Dashboard">
  <DataSource id="contactCounts" url="/api/contactcounts" />
  <FlowLayout 
    var.counts="{contactCounts.value}"
    var.isLoading="{contactCounts.inProgress}">
    <InfoCard 
      width="25%" title="Reviews Overdue" 
      isLoading="{isLoading}" 
      value="{counts.overdue}" 
      color="$color-danger" />
    <InfoCard 
      width="25%" 
      title="Today's Reviews" 
      isLoading="{isLoading}" 
      value="{counts.today}" 
      color="$color-warning" />
    <InfoCard 
      width="25%" 
      title="Upcoming Reviews" 
      isLoading="{isLoading}" 
      value="{counts.upcoming}" />
    <InfoCard 
      width="25%" 
      title="Completed Reviews" 
      isLoading="{isLoading}" 
      value="{counts.completed}"
      color="$color-valid" />
  </FlowLayout>
</Component>

Each InfoCard displays its associated value. The FlowLayout component and setting a width of 25% ensures items placed inside will use 25% of the available width of available space:


Dashboard numbers

Adding Navigation

The dashboard is just a summary of essential numbers. To let the user investiage what is behind the number, you need to let them navigate to the list of corresponding contacts.

Prepare the InfoCard component to receive a link property and navigate to that link when it is clicked:

InfoCard.xmlui
<Component name="InfoCard">
  <Card 
    height="{$props.height}"
    onClick="{Actions.navigate('/contacts/' + $props.link)}">
    <Text variant="subtitle" value="{$props.title}"/>
    <CVStack width="100%">
      <H2 color="{$props.color}" fontSize="4em">
        {$props.isLoading ? "..." : $props.value}
      </H2>
    </CVStack>
  </Card>
</Component>

The Actions.navigate method visits the specified link. Remember, the <NavPanel> section of Main.xmlui contains the links in the menu; all of them start with /contacts.

Set the link property's value for each InfoCard on the dashboard:

Dashboard.xmlui
<Component name="Dashboard">
  <DataSource id="contactCounts" url="/api/contactcounts" />
  <FlowLayout 
    var.counts="{contactCounts.value}"
    var.isLoading="{contactCounts.inProgress}">
    <InfoCard 
      width="25%" title="Reviews Overdue" 
      isLoading="{isLoading}" 
      value="{counts.overdue}"
      link="overdue" 
      color="$color-danger" />
    <InfoCard 
      width="25%" 
      title="Today's Reviews" 
      isLoading="{isLoading}" 
      value="{counts.today}" 
      link="today"
      color="$color-warning" />
    <InfoCard 
      width="25%" 
      title="Upcoming Reviews" 
      isLoading="{isLoading}" 
      link="upcoming"
      value="{counts.upcoming}" />
    <InfoCard 
      width="25%" 
      title="Completed Reviews" 
      isLoading="{isLoading}" 
      value="{counts.completed}"
      link="completed"
      color="$color-valid" />
  </FlowLayout>
</Component>

When you click a dashboard item, the UI logic will take you to the corresponding contact list.

Recent Contacts

It would be great to see the recently added contacts on the dashboard. In this section, you will add a table displaying the last five contacts added in descending order.

Update the Dashboard.xmlui file by appending a new Card component after the last InfoCard:

Dashboard.xmlui
<Component 
  name="Dashboard" 
  var.counts="{$props.contactCountsSource.value}"
  var.isLoading="{$props.contactCountsSource.inProgress}">
      <!-- Unchanged -->
      <Card width="50%" height="350px">
        <H4>Recent Contacts</H4>
        <Table data="/api/contacts-recent" height="*">
          <Column header="Category" width="80px">
            <Text>{$item.categoryId}</Text>
          </Column>
          <Column header="FullName" width="*">
            <Text maxLines="1">{$item.fullName}</Text>
          </Column>
        </Table>
      </Card>
    </FlowLayout>
  </VStack>
</Component>

This code renders a Card (the same layout container type used within InfoCard) and a Table bound to the GET /api/contacts-recent endpoint. The Table has two column definition items (Column) to name the columns to display in the table:


Initial table

Category IDs do not give you a hint about a particular category. It would be great if the first column displayed the category's name with a color badge as in the contact list.

Create a new component, CategoryBadge.xmlui (within the components folder):

CategoryBadge.xmlui
<Component 
  name="CategoryBadge"
  var.categoryName="{getCategoryNameById($props.categoryId)}">
  <DataSource id="categories" url="/api/categories" />
  <Badge when="{categoryName}" value="{categoryName}" colorMap="{categoriesColorMap}"/>
</Component>

Add a code-behind file, CategoryBadge.xmlui.xs, to the new component using the similar technique you learned in the Adding a Badge section:

CategoryBadge.xmlui.xs
// Create a color map for all categories
var categoriesColorMap = toHashObject(categories.value, "name", "color");
 
// Resolve category name by id
function getCategoryNameById(categoryId) {
  const category = findByField(categories.value, "id", categoryId);
  return category ? category.name : "";
}

Change the Text element within the first Columnn to the new CategoryBadge element and update the width of the Category column:

Dashboard.xmlui
<Component name="Dashboard">
  <DataSource id="contactCounts" url="/api/contactcounts" />
  <FlowLayout 
    var.counts="{contactCounts.value}"
    var.isLoading="{contactCounts.inProgress}">
    <!-- Unchanged -->
    <Card width="50%" height="350px">
        <H4>Recent Contacts</H4>
        <Table data="/api/contacts-recent" height="*">
          <Column header="Category" width="200px">
            <CategoryBadge categoryId="{$item.categoryId}" />
          </Column>
          <Column header="FullName" width="*">
            <Text maxLines="1">{$item.fullName}</Text>
          </Column>
        </Table>
      </Card>      
  </FlowLayout>
</Component>

Table with badge

The new CategoryBadge component can replace the previous markup to display the category in Contact.xmlui. Change Badge to CategoryBadge:

Contact.xmlui
<Component name="Contacts">
  <ContactForm id="contactForm" />
  <DataSource id="categories" url="/api/categories" />
  <VStack>
    <HStack horizontalAlignment="end">
      <Button icon="plus" onClick="contactForm.open()">New Contact</Button>
    </HStack>
    <List data="{$props.data.map(extendWithSection)}"
      defaultGroups="{['Overdue', 'Today', 'Upcoming', 'No Due Date', 'Completed']}"
      groupBy="section">
      <!-- Unchanged -->
      <Card var.categoryName="{getCategoryNameById($item.categoryId)}">
          <!-- Unchanged -->
          <HStack verticalAlignment="center" horizontalAlignment="end">
            <CategoryBadge categoryId="{$item.categoryId}" />
            <Text>{smartFormatDate($item.reviewDueDate)}</Text>
          </HStack>
          <!-- Unchanged -->
      </Card>
    </List>
  </VStack>
</Component>

You do not need the categories DataSource in Contacts, as ContactRow no longer uses it. You can remove it from Contacts.xmlui.

Conclusion

This concludes the step-by-step tutorial.

See the How to section for shorter, concise code snippets showcasing common UI building patterns. To see the basic use case of different components found in XMLUI, see the Basic Components article.

đź“”

You can download the code of the completed tutorial from here.