A Deep Dive Into Cycle's Frontend Stack

Introduction


Hello World!

First and foremost, I am a Software Engineer here at Cycle with a primary focus on our frontend. We currently have a total of five applications:

  1. The Portal — Our core application
  2. Admin Portal — A more in-depth overview of the Portal
  3. Main Website — Marketing site
  4. Status — View the status of Cycle’s services, our Changelog, and any recent announcements
  5. Docs — An in-depth user guide for the Portal or the public API

With five applications to maintain and keep updated — some technical decisions had to be made in order to create an easy to maintain and scalable development environment, so let’s dive in!


If you’re new to Cycle or would like to recommend friends or colleagues to us, we have a fantastic introductory video below!


Larger Than Life

At Cycle’s inception, there were only 3 engineers on the team. Maintaining and adding to several frontend applications while keeping focus on our core offering was a tough feat. So how could we make it as easy as possible? How, as the company grows do we make the process easy for new additions to the team?

The answer was to develop each application almost identically. This meant that you could learn the codebase of the Portal and be an expert in any other of Cycle’s applications.

On a High level:

Both the Portal and Admin Portal are Create React Applications with the Portal being ejected. Our Website, Docs, and the Status page are all Gatsby static sites.

These ultimately beg several questions, why React over Vue or Angular? Why Gatsby over a WordPress solution or something like Hugo or Jekyll?

Framework vs. Framework


React

In writing this blog entry — I wanted to research the concepts that I would be explaining as they go further into detail than I will. I felt they were unbiased and and informative and definitely recommend checking them out.

The two most important points for, “Why React”:

  1. The importance placed on stability, which is especially important supporting consumer applications.
  2. Flexibility, which ties into React’s vast ecosystem. Both in its community engagement, support, and its ability to tie into other frameworks.

Which leads me to Gatsby.

Gatsby

Similarly, Gatsby’s community engagement, support and their plugin functionality provides near complete flexibility for anything we wish to do. Mainly though, it’s built on top of React. This meant we did not need to worry about learning another language, like GO, or Ruby, or PHP, etc.

Instead, efforts could be focused on mastering React through the challenges of developing both marketing based websites and a state heavy interconnected beast such as our Portal.

Which doubled as keeping our ecosystem smaller.

Styled Components

There’s conflicting opinions on CSS in JS frameworks within the community. For us, its benefit in avoiding conflicting styles, readability, and the reusability factor outweigh its cons.

Each of our projects contains a common/components folder that we place reusable components within. This also means we’re able to leverage auto imports.

Cycle UI


How do we maintain consistency?

Internally we have our own UI library that is not built on top of any other UI framework. It consists of components that are common amongst projects and are core to Cycle’s branding.

Several utility libraries are utilized to power more complex interactions:

We also utilize Storybook to mock our components and views for testing before implementation into our codebase.

Automating Docs


Personally, as a developer, I think one of the most frustrating things is interacting with someones API and the method being hard to find, unintuitive, or worse, inaccurate.

Documentation for a product that greatly simplifies something like multi-cloud container orchestration is difficult enough to write in a compelling, easy to understand language. When you combine that with the huge amount of functionality our REST API brings to the table, it’s a massive undertaking.
- Chris Aubuchon - Director of Customer Success @ Cycle

Our process around docs requires every change be marked, categorized, and implemented, manually. During development, we iterate on a component or struct several times within a day until we feel both the language within the API and interactive experience is intuitive and seamless. It’s simply not scalable.

That being said, semi-automated docs are coming! Grady, my fellow Software Engineer is spearheading the project and here’s what he had to say about it,

Cycle experimented with multiple solutions for automating our API documentation. The most promising was TypeDocs, however there was still much to be desired with formatting and layout so creating our own documentation generator was the next logical step.

The new API documentation website is being built using Gatsby for the framework and Algolia for the search engine. The project is still in development, but be sure to check back for another blog post to get an in-depth look into how it was built!

As the platform continues to mature, this is a critical step for us. Ensuring information is always readily available, but most importantly, accurate.

Deployments and Management Made Simple


As companies and individuals continue to extend our platform in creative ways, the Portal must be 100% stable at all times. And the constant changing of a user’s Hubs must always be reflected in the state.

We utilize Redux and Redux Sagas for state management, which is the backbone of the Portal. This is paired with a Notification Pipeline system implemented using the WebSocket API — more on this can be found here.

The Foundation

Our Redux store is hierarchical and can be broken down into “views”. For example:

store
  / environments
  / containers
    / cache
    / create
    / notifications
    / ...

Notifications

Let’s start with the notifications folder. A full list of notifications can be found here at our public API repo.

We subscribe each “main” folder to the Notification Pipeline and handle each event that is received. A simple example would be <resource>.updated. Say you want to change the name of one of your environments and add a description.

Example picture to update an environment

Pressing “update” would submit a request with the details provided. Once processed, a notification will be received via the WebSocket connection with a topic of environment.updated. This triggers several actions within the corresponding Saga:

  1. Check if the environment is within the store’s cache.
  2. If it is, the filter object that contains paginated environments must be invalidated and refetched.
    1. This is because anywhere that utilizes the name and description of this environment must contain an updated state. Otherwise cached data could be displayed which isn’t guaranteed to be accurate.
  3. Other logic.

Creating a Container

Our Sagas typically follow this convention:

...

function* createContainerHandler(
  action: ActionType<typeof actions.createContainer.request>
) {
  const { values, query } = action.payload;

  const resp: Request.ApiResult<Containers.Single> = yield callApi(
    Containers.create, {
      value: values,
      query
  });

  if (!resp.ok) {
    yield put(
      actions.containerCreateFailed({
        err: resp.error,
      })
    );
    return;
  }

  yield put(
    ContainerCache.actions.cacheContainers({
     containers: [resp.value.data],
    });
  );

  yield put(
    actions.containerCreated({
      containerId: resp.value.data.id,
    });
  );
}

...

We kept with Redux Saga due to its readability and how easy it is to change or add new logic. Epics were experimented with, but the logic tended to be verbose at almost twice as many lines which also hindered its readability and speed of development.

Typescript and Functional Components


With so much going on and data constantly being exchanged, it’s important to understand what is happening and what the data looks like. That’s why all of our applications utilize Typescript.

Here’s our tsconfig.json for the Portal. Keeping the TS compiler strict helps maintain a tidy codebase. This is a bit of a trade-off as it slows down development, but makes it purposeful.

{
  "compilerOptions": {
    "module": "esnext",
    "strict": true,
    "target": "es6",
    "outDir": "dist",
    "noImplicitReturns": true,
    "sourceMap": true,
    "pretty": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "removeComments": false,
    "suppressImplicitAnyIndexErrors": true,
    "baseUrl": "./src",
    "moduleResolution": "node",
    "jsx": "react",
    "noEmitHelpers": true,
    "importHelpers": true,
    "esModuleInterop": true,
    "lib": ["dom", "es6", "es7"],
    "paths": {
      "__mocks__/*": ["../__mocks__/*"]
    }
  },
  "exclude": ["node_modules", "fuse.ts"]
}

We also follow Functional Programming paradigms and if you’re unfamiliar to what that is or means, here’s a good intro.

Most of our components start with this as a base:

import React, { FC } from "react";
/* Other imports */

type ComponentProps = {
  prop1: string;
  prop2: boolean;
  prop3?: () => void;
};

export const Component: FC<ComponentProps> = ({
  prop1,
  prop2,
  prop3
}) => {
  return (
    ...
  );
};

One of the key aspects of Functional Programming is immutable variables. Meaning, it’s not possible to modify a variable once it has been initialized. However, new variables can be created. For example:

const data = [5, 3, 2, 8, 1];

const sortedData = data.sort((a, b) => a - b);
// sortedData = [1, 2, 3, 5, 8];

const dataWithIds = data.map((number, idx) => ({
  id: idx,
  data: number,
}));
// dataWithIds = [{ id: 0, data: 5 }, { id: 1, data: 3 }, ...];

Following this paradigm allows us to more easily follow the data as operations are performed — this means debugging made easier.

Nivo


One of the important features we have is our graphs. This allows you to better manage your resources all from within the Portal and for this we utilize nivo.

Example photo of Cycle instance telemetry

Cycle watches and reports data for each of these:

Total Server Resources By ClusterIndividual Server ResourcesContainer/Instance Telemetry
  • RAM Allocations
  • RAM Usage
  • CPU Shares
  • Load Averages
  • RAM Usage
  • Storage Usage
  • Volume Information
  • Other general info
  • Live Stream of CPU Usage and RAM Usage
  • Usage Reports by:
    • Last Hour
    • Last 6 Hours
    • Last 12 Hours
    • Last Day
    • Last Three Days

Using the Past to Shape the Future

As the company continues to scale, so do our processes. It’s important to understand why some of these decisions were and are being made.

Staying lean is part of Cycle’s core foundation and these decisions have allowed Cycle to maintain its rapid pace without spending exorbitant amounts of money. With passion and a vision to cause change we keep things simple, without taking shortcuts.


If you’re not already a user we’d like you to check out Cycle and join us in simplifying cloud orchestration.

We also recently did a number of exciting changes which you can read about here.

Talk with us on Slack, Intercom in the bottom right, or at https://cycle.io/support/.