Refactoring K-k-karma Chameleon

Scope

Just in case anyone didn’t get my reference, Karma Chameleon is a pop song from Culture Club. There’s really no connection here except for the fact that the product I am working on is code named Chameleon. This was three month project to redesign and refactor the front-end code of the branding tool we called Chameleon. I presented my redesign work to our stakeholders and pair-programmed with a senior engineer.

Stack

The application front-end utilized our React Component library to render the user experience. In order to manage the changing state of the application, we leveraged React Redux. Phoenix framework (built on Elixir, Plug library, and Cowboy Erlang and compiled with Mix) was used for scaling and maintaining the website. MySQL database with Pheonix’s built in Ecto support was used to store brand metadata and Redis for API counts.

Background

Chameleon is an internal tool used by account managers and the e-commerce team to setup branded versions of the VitalSource Bookshelf e-reader and e-commerce platform. Publishers, universities or institutions leveraging our technology can brand their own versions and change a countless number of parameters including but not limited to colors, logos, images, and verbiage. Chameleon is a portal that allows the account managers for these institutions to change the parameters without coding or having any technical background.

Chameleon is still running on its early day designs and had limited development, so a lot of the features were broken or the design was not up to par. A total refresh using our current Stewie React library will bring it to the future and some TLC is needed to fix the broken functionality. Finally, we were hoping to introduce some new backend functionality like admin privileges and better search experiences.

Feedback

I talked to internal users to get a better understanding of the pain points when using Chameleon. I asked them what the top pain points are for them when trying to change a product’s brand parameters. The feedback is not in any particular order.

  • The APIs are too easy to delete.
  • APIs are truncated and there is no tooltip or method to view the entire address.
  • The order of the parameters and brands don’t make a lot of sense.
  • Lazy rendering is jumpy and makes it difficult to view all parameters.
  • Input fields should default to “select an option” when empty.

On the other hand, stakeholders also had some business goals that they wanted to include. These are bigger asks but require more of a refactor which is not doable within the given scope. However, we want to refactor the architecture with these goals in mind.

  • Admin privileges so that not all users see the same parameters.
  • Localization so that Chameleon can be used in different languages.
  • A/B testing as a way to try it out before including it in other products.

Heuristic Analysis

Form Validation
Chameleon has form for adding/editing a brand and smaller forms for each parameter. Currently there is no feedback on validation for any of the forms. We want to validate user input and give feedback where necessary.

Simplifying the Interface
The main dashboard and brand detail pages have extra information that can be removed from the UI. The information that was originally added was intended to be useful for users; however, the information that is provided doesn’t help users when they need to change parameters. Thus, we considered removing the JSON, Preview, and Changelog tabs from the brand details page. For the main dashboard, we are removing the Lookup APIs to simplify the cards.

Sorting / Filtering
The brands, parameters, and lookups are not sorted in any particular order. Initially, we should sort the lists alphabetically and then allow users to sort them based on preference.

Mocks

Figure 1.0 Before and after redesign.

Lazy Render Abstraction

The initial code used two separate methods for tracking the visibility of components for the list of Brands and Parameters. We want to render these individual components only when in view, because loading them all at once is detrimental to the user experience. Furthermore, it is better to abstract the visibility tracker and use it throughout the application. You can tie the throttling and mounting and un-mounting of various listeners to the component as well.

Previous Parameters Code
Here the old code is specific to the array and from a bird’s eye view there’s no idea as to why the index is started from -1.

topLevelParamters(topLevels) {
    let paramIndex = -1; 
    const windowHeight = window.outerHeight;
 ...
}

This is more custom code with fixed values like 167 and 217. Page offset values are set within the placeholder prop that triggers a Boolean, which is very dirty method to track offset position on the screen.

visibleLevels.map((item) => {
  paramIndex++;
  const offsetHeight = paramIndex * 167 - this.state.windowTop + 217;
  return (
    <Parameter
      key={`value-cell-${item.key}`}
      param={item}
      addParameter={this.addParameter}
      deleteParameter={this.deleteParameter}
      getReference={this.getReference}
      brand={this.props.brand}
      placeholder={offsetHeight < -200 || offsetHeight > windowHeight + 200}
    />
 ...
}

Previous Brand Stats Code
You can see this was going in the right direction, but we need to abstract this in order to pull out the event listener and use throttler.

  componentDidMount() {
    window.addEventListener("scroll", this.onScroll);
  }

  componentWillUnmount() {
    window.removeEventListener("scroll", this.onScroll);
  }

  componentDidUpdate() {
    this.getIdsInView();
  }

  onScroll = () => {
    clearTimeout(this.getIds);
    this.getIds = setTimeout(this.getIdsInView, 250);
  };

  getIdsInView = () => {
    ...
    const brandsInView = keys.reduce((acc, key) => {
      const element = this.brandRefs[key];
      if (element) {
        const pageTop = window.scrollY;
        const pageBottom = pageTop + window.innerHeight;
        const elementTop = element.offsetTop;
        const elementBottom = elementTop + element.clientHeight;

        if (elementTop <= pageBottom && elementBottom >= pageTop) {
          return [...acc, key];
        }
      }
      return acc;
    }, []);
    ...
  };

TrackVisibility from Fadi Khadra
I was originally going to write an entire new component to use for this purpose, but upon Googling visibility tracker I found a bunch of repos with MIT licenses. This particular component uses lodash.throttle and shallowequal because we are just looking at a shallow comparison between objects. A little diving into the visibility tracker from Fadi, we see that I can repurpose this component by simply adding a listener for input. We want the components to re-render when the user searches, filters, or sorts the lists and not just call the method only on scroll and resize. All that was left to do was add a skeleton placeholder when the component is loading and visibility is False.

attachListener() {
  window.addEventListener("scroll", this.throttleCb);
  window.addEventListener("resize", this.throttleCb);
  window.addEventListener("input", this.throttleCb);
}

removeListener() {
  window.removeEventListener("scroll", this.throttleCb);
  window.removeEventListener("resize", this.throttleCb);
  window.addEventListener("input", this.throttleCb);
}

componentDidUpdate(prevProps) {
  if (
    !shallowequal(
      this.getChildProps(this.props),
      this.getChildProps(prevProps)
    )
  ) {
    this.isComponentVisible();
  }
}

Final Thoughts

Self-driven Project
This was truly a self-directed project. While I had some stakeholder goals, these were more meant to give some sense of direction. All the designs and development was owned by me with mentorship from a senior engineer. This made the project very open and allowed me to flex my management, design, and engineering skills. The majority of the project was spent updating the UI to borrow from the new design library specs.

Post Release
The outcome was well received by everyone. I don’t want to diminish the amount of work that was put into this by me and the senior engineer, but the product was not in a well maintained state when it was given to me. The enhancements we made dramatically improved the user experience and workflow. The UI kept to the original color changes through the lens of color accessibility.