Overview
Why did we build Helium?
Building and delivering highly differentiated and impactful learning experiences shouldn’t be difficult. Turning your vision into reality shouldn’t be constrained by the technical limitations of your software platform. Our mission for Helium is to empower developers and designers to bring their visions to life through a simple and powerful framework that enables full customization and extensibility of the Thought Industries Customer Learning Cloud.
Ultimately we want to make it easy to:
- Customize the web site & learner experience to achieve design goals
- Modify the structure, appearance and functionality of courses, assessments, tests, and more
- Incorporate external data into learner-facing components like dashboards and catalogs
- Utilize internally-developed functionality and seamlessly integrate into the learner experience
- Deliver in-the-moment training natively across multiple platforms (beyond Web/HTML)
What is Helium?
Helium is a frontend web development framework for building highly contextual, dynamic and personalized learning experiences. Helium provides you with a complete library of UI Components, Hooks, and Utilities that make building custom learning experiences fast, easy, and fun.
Helium utilizes a modern technology stack that includes React, GraphQL, and Tailwind CSS to provide an exceptional developer experience. The out-of-the-box UI components make getting started easy and with GraphQL getting to the data you need is intuitive and fast.
The code for Helium is fully open source! All of the source code in available in the Helium GitHub Repository.
Benefits of Helium:
- Get Started Quickly – Helium provides a starter application that is simple to use. The starter application interfaces with the Customer Learning Cloud seamlessly and delivers a custom learning experience right out of the box. The starter application is based on the Helium framework and styled with the Tailwind CSS utility package.
- Design Flexibility – Helium separates the section of your website that is visible to learners, or the “head,” from the component that manages data and functionality. This “headless” architecture allows you complete control over the visual appearance and functionality giving you complete design freedom.
- Easier Personalization – Helium’s React-based framework facilitates the ability to display dynamic content and tailor the learners’ experience by more easily accessing and incorporating relevant data – wherever the data exists.
- Build Highly Differentiated Experiences Faster – The Helium framework uses a modern tech stack that speeds up the development process. It’s seamlessly integrated with the Customer Learning Cloud hosting environment and includes all of the necessary tools and utilities for local development along with utilities for staging and production code management. Helium comes with out-of-the-box, code accessible React components, hooks, tools, utilities and a starter application.
- Optimized Performance – Helium is developed on cutting-edge technology that makes heavy use of server side rendering. As a result, the bandwidth required to load a website is significantly reduced. This also results in increases to your website’s Core Web Vitals, as well as an increase in organic search traffic.
Limitations and Considerations
- Helium requires technical resources and familiarity with JavaScript and the React framework.
- Helium is designed to run on the Thought Industries Enterprise Learning Cloud platform. Therefore, an Enterprise Learning Cloud account is necessary.
- We are continually adding new components and functionality so please be sure to check documentation frequently as well as the Helium community forums for updates.
Was this helpful?
Getting Started
Requirements
- Ti Enterprise Learning Cloud account and API key
- Node 14 or higher
- NPM 8.6.0 or higher
Installation and Set Up
- Install with
$ npm init helium-app
- Enter the directory inside the folder you want your helium app to live in
- Select the theme you want to install with. The hello world theme is a minimal theme that is best if you’d like to write most of the code for your Helium project from scratch. The starter-app theme is a more robust theme and is better if you’d like a starting off point for your Helium project.
- Run
$ cd [directory name]
and then run$ npm install
- Finally, run
$ npm run authenticate
Authentication
- Run
$ npm run authenticate
and complete the following steps- Enter the url of a Ti learning instance
- Enter the API key for this instance (Available from Admin homepage under Settings > Security > API Key)
- Enter a nickname for this instance
- Enter an email of a test user for this instance. When you preview your learning instance you’ll see the view accessible to this test user. For example, you’ll only see courses that this user has access to.
- Determine if you like to add another instance (y/n)
- Note: If you have both a sandbox and production instance we recommend adding both with this command so you can deploy Helium apps to your sandbox before you deploy to production.
- a
ti-config.json
file will get generated which can be edited later. For example, if you want to change the test user, you can just edit theti-config.json
file. - Note: Your instance’s brand appearance (color, logo, etc.) and widget translations are readily available to use at this point.
Deployment and Hosting
In production, Helium runs on globally distributed Cloudflare workers that are optimized for speed and performance regardless of where the client is located. Thought Industries manages all hosting needs.
To deploy, run $ npm run deploy [instance name]
where [instance name]
is the nickname you entered when running $ npm run authenticate
.
We’d also like to call out some key points:
- If the Helium feature flag is not turned on for your learning instance, you won’t be able to deploy your Helium site
- It may take some time for your first Helium deployment to register to your learning instance URL
- If you want to delete a deployed Helium page, simply remove the folder or file that represents the page from your Helium app and run
$ npm run deploy [instance name]
- Helium works on a page by page basis, meaning it’s possible (and likely) that some of your pages will be Helium pages and some of your pages will be powered by the existing Thought Industries platform. For example, your homepage might be powered by Helium while your catalog page is not
- It’s not possible to utilize Helium and the existing Thought Industries platform on the same page. You can’t use Thought Industries widgets on Helium pages.
Was this helpful?
Framework
Server Side Rendering
What is Server Side Rendering (SSR)?
SSR is technique used to improve the load time of web apps. Most web apps built today are Single Page Applications (SPA) where all the code for the site is built and served on a single page. This makes for a great and intuitive user experience but bundling all of this code and loading it on to a single page is slow. This leads to poor performing SEO and a degraded user experience.
SSR solves this problem in a creative way. Instead of sending all the code at once to the client browser, SSR sends a barebones structure of the web app to the client which then renders very quickly. At the same time, the entire web app code is sent to a server which renders the code as well (hence the name Server Side Rendering). Once the web app is generated on the server, the server then hydrates the barebones structure making the web app fully functional on the client.
Helium and SSR
Helium utilizes SSR. It’s not required but we do recommend it. Below is an example of how you can utilize SSR in your Helium app with the FeaturedContent component
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
FeaturedContent,
ContentTileStandardLayout,
FeaturedContentContentItem
} from '@thoughtindustries/featured-content';
import {
hydrateContent,
useCatalogQuery,
useAddResourceToQueueMutation
} from '@thoughtindustries/content';
export { Page };
function Page() {
const { i18n } = useTranslation();
const [addResourceToQueue] = useAddResourceToQueueMutation();
const handleAddedToQueue = (item: FeaturedContentContentItem): Promise<boolean | void> =>
item.displayCourse
? addResourceToQueue({ variables: { resourceId: item.displayCourse } }).then()
: Promise.resolve(undefined);
const { data, loading, error } = useCatalogQuery();
let content;
if (data) {
content = data.CatalogQuery.contentItems.map((item, index) => {
const hydratedItem = hydrateContent(i18n, item);
const { authors, description, href, ...restItemProps } = hydratedItem;
const transformedItem = {
...restItemProps,
authors,
shortDescription: description && `${description.substring(0, 75)} ...`,
linkUrl: href
};
return <ContentTileStandardLayout.Item key={`item-${index}`} {...transformedItem} />;
});
}
return (
<div className="px-4">
<FeaturedContent>
<ContentTileStandardLayout
desktopColumnCount={4}
onAddedToQueue={handleAddedToQueue}
>
{content}
</ContentTileStandardLayout>
</FeaturedContent>
</div>
);
}
Routing
Helium utilizes a file based routing system which means you define all routes for your Helium app in the /pages directory. We’ll walk through a couple of use cases. Examples of all these different use cases can found in this github repository.
Replacing an existing page
It’s very easy to take control of existing pages in Helium. Simply create a new folder underneath the pages directory with the name of the route you want to replace, then create file in that new directory named index.page.tsx. For example, if you wanted to replace the dashboard page, you would do that like so:
Creating a new page
In a similar fashion if you want to create a new route, simply create a new folder named the new route in the pages folder and create an index.page.tsx. You can also create other files under this folder with naming convention [otherName].page.tsx. For example, if you wanted to create a new route /test and have another page under /test/three, you would that like so:
Replacing Multiple Pages at Once
To avoid having to define a course-details page for every course-slug, a single file can be created which will control the layout of the course details page, which can then load in content dynamically. This concept is called Route Strings.
- In the pages/courses directory, create a file named @courseSlug.page.tsx
- In renderer/_default.page.server.tsx add 'routeParams' to end of
passToClient
- The contents of this file will now control all course-detail pages of the helium app, that do not already have a specific course-detail page created. For example, the test-course slug above will not be replaced by this template. An example file using
// an example @courseSlug.page.tsx file
import React from 'react';
import { usePageContext } from '../../renderer/usePageContext';
function Page() {
const { routeParams } = usePageContext();
return (
<>
<h1>Welcome to {routeParams.courseSlug}</h1>
</>
);
}
export { Page };
Creating a Catch All Route
Catch all routes are great for letting a user know they are probably somewhere they don’t want to be. A catch all route string can be used to replace the classic 404 error page, by providing the user with a little bit more information.
- To create a catch all route, create a new file in pages/courses named *.page/tsx
- The contents of this page will now be shown when the user visits courses/course-slug/a-page-that-doesn’t-exist.
Secret Management
Summary
This document introduces the support for environment variables in the Helium project. The support for environment variables relies on the technology used to build and bundle the Helium application, and is adapted to the technology used to publish the application. Once the Helium application is authenticated, developers can store the environment variables in an ‘.env’ file and reference the variables in ‘Page’ components and their imports. The Helium application lifecycle scripts will take care of parsing and transforming these variables accordingly.
Use Cases
If you are defining variables for public use, consider using the public variables. For example: a title for the Helium application. The public variables can be referenced from interface ‘import.meta.env’. When building for production, these variables are statically replaced and their values will be included in the bundles.
There are two use cases to use environment variables.
If you are defining variables for private use, consider using the private variables. For example: server side data-fetching from a private server (with secrets). The private variables can be referenced from interface ‘process.env’. These variables will remain as-is during build for production. When publishing for production, these variables are transformed to runtime secrets and their values will not be included in the bundles.
Examples
The examples below show how to use both the public and private environment variables in the Helium application.
Step 1
Once the Helium application is authenticated, add the environment variable to the ‘.env’ file at the root of the project. For public variables, define the variable name with the prefix ‘HELIUM_PUBLIC_’. For private variables, define the variable name with the prefix ‘HELIUM_PRIVATE_’.
See example ‘.env’ file below with one public variable and one private variable. An example file ‘.env.example’ is also included in the package for helium template.
# Example public variables
HELIUM_PUBLIC_APP_VERSION='v1'
# Example private variables
HELIUM_PRIVATE_SECRET='secret value'
Step 2
See example below for the configuration of a ‘Page’ component.
In the ‘Page’ components and their imports, reference the public variable with ‘import.meta.env.HELIUM_PUBLIC_APP_VERSION’.
// in pages/hello.page.ts
const appVersion = import.meta.env.HELIUM_PUBLIC_APP_VERSION;
export { Page }
function Page() {
return <>{`Hello! App version: ${appVersion}`}</>
}
Step 3
In the server ‘Page’ components and their imports, reference the private variable with ‘process.env.HELIUM_PRIVATE_SECRET’.
See example below for the configuration of a server ‘Page’ component. The server ‘Page’ component will fetch data with the private variable and will make the response data available in page context.
// in page/data.page.server.ts
export { onBeforeRender }
async function onBeforeRender(pageContext){
const secretValue = process.env.HELIUM_PRIVATE_SECRET;
// data fetching with secret value
const response = await fetch("<remote_server_url>", {
headers: {
secret: secretValue
}
});
const { data } = await response.json();
return {
pageContext: {
data
}
};
}
See example below for the related ‘Page’ component to render the response data from the page context.
// in pages/data.page.ts
export { Page }
function Page(pageContext: { data: string }) {
const { data } = pageContext;
return <>{`Data: ${data}`}</>
}
Step 4
To use TypeScript IntelliSense for the public variables, use an ‘env.d.ts’ file to augment ‘ImportMetaEnv’.
See example ‘.env.d.ts’ file below with the public variable defined in the sample ‘.env’ file. An existing file ‘.env.d.ts’ is also included in the package for helium template.
/// <reference types="vite/client" />
interface ImportMetaEnv {
// public env variables...
readonly HELIUM_PUBLIC_APP_VERSION: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
Flow under the Hood
When developers run scripts to deploy the Helium application, the script builds the bundle first then deploys the bundle to Cloudflare Worker.
During build: references to public variables are statically replaced and their values will be included in the bundle; references to private variables remain as-is in the bundle.
During deploy: only references to private variables are transformed to global variables and relevant secrets are created for Cloudflare Worker.
Data Reporting with Helium
We are currently building out native data reporting support on the Thought Industries platform for Helium. A list of our currently supported data reporting can be found on our GraphQL documentation. Because Helium is headless, we also encourage you to investigate other data reporting tools that fit your needs if the data reporting you’re looking for isn’t currently supported in Helium.
SSO
Accommodating Single Sign On within your Helium project is simple as adding a button, hook, or action that will send the user to your Provider’s login page. After successfully logging in via your Provider, the user will be sent to the `returnTo` url as specified by your Provider or by the query parameters included in your request.
SAML 2.0
${host}/access/saml/login/:client?
- Path –> description
- client –> (optional) the slug of the Panorama the user should belong to, if the Panorama has its own SSO Configuration
OpenID Connect
${host}/access/openId/login/:client?
- Path –> description
- client –> (optional) the slug of the Panorama the user should belong to, if the Panorama has its own SSO Configuration
Query Parmeters
- Parameter –> description
- returnTo –> (optional) the URL the user should be sent to after successfully authenticating with your Provider. If no `returnTo` is provided, the user will be sent to `/learn/`
JSON Web Token (JWT)
${host}/access/jwt
- Paremeter –> description
- JWT –> (required) a signed, valid token containing the identity and attributes of the user, e.g., `email`, `externalCustomerId`, `role`, `returnTo` etc. The token can be signed either by your Secret Key or API Key
Helpful Commands
npm run authenticate [-k]
Authenticate a new project by validating against your Thought Industries instance and writing you ti-config.json file.
Options:
-k, -insecure (optional): used when the Thought Industries instance is behind an untrusted SSL certificate (e.g. local development).
npm run deploy <i> [-k]
Builds and deploys the project.
Options:
-i, -instance (required): which instance from your ti-config.json file to be used, as noted by the instance nickname provided during installation
-k, -insecure (optional): used when the Thought Industries instance is behind an untrusted SSL certificate (e.g. local development).
npm run dev [i] [p] [-k]
Starts up the development server for your project
Options:
-i, -instance (optional): which instance from your ti-config.json file to be used, as noted by the instance nickname provided during installation
-p, -port (optional): Which port the server should listen on
-k, -insecure (optional): used when the Thought Industries instance is behind an untrusted SSL certificate (e.g. local development).
npm run update-translations [i] [-k]
Fetches translation from your Thought Industries instance and updates the locales/translations-source.json and locales/translations.json files.
Options:
-i, -instance (required): which instance from your ti-config.json file to be used, as noted by the instance nickname provided during installation
-k, -insecure (optional): used when the Thought Industries instance is behind an untrusted SSL certificate (e.g. local development).
What is Tailwind CSS?
Tailwind CSS is a utility first CSS framework that is fast and built to scale. Tailwinds readable classes make it easier and faster for developers to understand each other’s code. There are many benefits to Tailwind but the largest one is that you no longer have to write your own CSS code. More can be learned about Tailwind and its benefits here.
Tailwind comes out of the box with Helium and is readily available for you to use with your own Helium project.
GraphQL
Data and GraphQL
All data in Helium flows through GraphQL, an extremely modern and open-source data query and manipulation language for APIs. GraphQL APIs differs from REST API’s in various ways, but at the heart of it, GraphQL has greater flexibility and efficiency in terms of data access and management. More can be read about the comparison between the two here.
There are two ways to interact with GraphQL in Helium apps: the more general useQuery hook and other more specific hooks for specific data groups. An example of these more specific hooks is the useCatalogQuery hook which fetches specific data about catalog contents. Any hook provided by the Helium packages can replicated with useQuery–the provided hooks exist to make it easier to start and maintain Helium apps.
authTokens
A user’s authToken is valid for as long as they have it configured for that particular role in the Role Settings in the Thought Industries Admin UI. More documentation on that can be found here. A users authToken is generated by calling the Login mutation.
Inside a Helium app, authTokens are handled automatically with pageContext. When calling GraphQL from an outside client such as Postman, pass the authToken in the header of the request with the key as authToken and the value as what’s returned from the Login mutation.
If the Login mutation is called again (during the time period the current authToken is valid), a new token will be created (with the same duration til expiration as configured starting from that point).
Postman Collection
You can explore our GraphQL API though our Postman collection. You’ll have to define your URL and API Key variables.
Fetching Content with GraphQL
In our admin experience we offer several different content types: Courses, Videos, Blogs, Scorms, Articles, and more. Although these objects are presented differently, on the backend many of them overlap. It’s important to understand how content data is structured so that you can fetch the appropriate data with our GraphQL API.
Courses
Courses are one of our most complex content types. At its root each course belongs to a courseGroup. Courses then have sections, sections have lessons, and lessons have topics. Examples of topics are quizzes, tests, or text pages. To get the id of the courses in your learning instance you can call CatalogContent. You would typically use this query to build your CatalogPage.
CatalogContent(page: 1) {
contentItems {
title
displayCourse
}
}
You can then use the ids returned from CatalogContent to call CourseById to fetch the various sections, lessons, and topics that a course has. This will give the outline of the course and you can also fetch more data about a specific course than you can with the CatalogContent query.
CourseById(id: “sample-id”) {
Sections {
Lessons {
Topics {
… on ArticlePage {
title
id
type
}
… on QuizPage {
title
id
type
}
}
}
}
}
Now that you have the ids of the various topics in the course you can call the Pages query to get more data about the specific topics themselves. For example, if you have a quiz topic with the Pages query you can get the quiz questions, the answers, and how long it usually takes to complete the quiz.
Pages(identifiers: [String!]!) {
... on QuizPage {
title
questions {
body
choices {
value
correct
}
}
}
}
Videos, Blogs, Articles:
Under the hood, Videos, Blogs, and Articles are also all stored as course objects but they have just one topic associated with them. Fetching the data for a Blog is very similar to fetching data for a normal course. You’d follow a similar pattern of calling CatalogContent, then calling CourseById, and then calling Pages. Blogs, Videos, and Articles are all available under the ArticlePage type.
query Pages($identifiers: [String!]!) {
Pages(identifiers: $identifiers) {
...on ArticlePage {
videoAsset
languages {
language
label
title
subtitle
body
copyright
}
}
}
}
ILT’s
ILTs or Instructor Led Training is another content type and is also stored as courseGroup objects. Most of the information surrounding ILT’s is at the CourseById level and doesn’t require you to call the Pages query. With ILTs, you can have one ILT with multiple sessions. This structure corresponds to one parent courseGroup and then multiple child courses for each session. Here is an example of how you could fetch an ILT with multiple sessions:
query CourseById($id: ID!) {
CourseById(id: $id) {
title
slug
courseGroup {
description
asset
courses {
title
courseStartDate
courseEndDate
}
}
}
}
Learning Path
To fetch the data about Learning Paths, you can use the LearningPathBySlug query like so:
query LearningPathBySlug($slug: Slug!) {
LearningPathBySlug(slug: $slug) {
milestones {
name
id
contentItems {
id
title
kind
}
}
}
}
If you’re only interested in data about a specific milestone, you can use the Milestone query:
query Milestone($id: ID!) {
Milestone(id: $id) {
name
id
contentItems {
id
title
kind
}
}
}
Working with the Quiz Object
There’s more moving pieces with a quiz then compared to fetching a text page for example. With a quiz, you have to both fetch data and then allow the user to send data back in the form of a quiz submission. We’ll walk you through the various API calls you’ll need to use do that here.
Fetching the Quiz Questions
The first order of business is actually getting the quiz questions to show the users. We’ll do that with the Pages query.
query Pages($identifiers: [String!]!) {
Pages(identifiers: $identifiers) {
... on QuizPage {
title
questions {
body
choices {
value
correct
}
}
}
}
}
Creating an Assessment Attempt
Next, we need to let the user create a quiz attempt. We’ll do that with the LoadAssessmentAttemptWithQuestions query. Given the headless nature of Helium, the choice is completely up to you when you want to call this query. Note: most users that land on a quiz page will usually submit a quiz, so it might make sense to create an assessment attempt as soon as the user lands on a quiz page. You could also do it when the user answers the first question.
query {
LoadAssessmentAttemptWithQuestions(
id: $id
courseId: $courseId
topicType: $topicType
) {
id
status
grade
}
}
Updating the Assessment Attempt
Now that we have the assessment attempt, we need to update it each time a user answers a question. We’ll use the UpdateAssessment mutation for this.
mutation UpdateAssessmentAttempt(
$activeQuestion: QuestionInput,
$assessmentAttempt: AssessmentAttemptInput
) {
UpdateAssessmentAttempt(
activeQuestion: $activeQuestion,
assessmentAttempt: $assessmentAttempt
) {
id
grade
}
}
// SAMPLE VARIABLES
{
"assessmentAttempt": {
"id": "{{ id from LoadAssessmentAttemptWithQuestions }}",
"status": "started"
},
"activeQuestion": {
"body": "<p>Who is the current quarterback for the Eagles?</p>",
"mustSelectAllCorrectChoices": true,
"selectedChoice": {
"value": "Jalen Hurts",
"correct": true
}
}
}
Submitting the Assessment Attempt
Awesome, the user has finished the quiz and is ready to click the submit button. To submit the quiz submission, we’ll call the UpdateAssessment mutation one last time
mutation UpdateAssessmentAttempt(
$assessmentAttempt: AssessmentAttemptInput
) {
UpdateAssessmentAttempt(
assessmentAttempt: $assessmentAttempt
) {
id
grade
}
}
// SAMPLE VARIABLES
{
"assessmentAttempt": {
"id": "{{ id from LoadAssessmentAttemptWithQuestions }}",
"status": "finished"
}
}
Comments, Discussion Threads in GraphQL
Fetching Comments on a Video
In order to fetch the comments on a video content type, you’ll first call the Forums query and then use the ID returned from that to call the Comments query.
Call the Forums query like so:
query Forums(
$courseId: ID!
) {
Forums(
courseId: $courseId
) {
id // <-- pass this into the Comments query
label
}
}
// SAMPLE VARIABLES
{
"courseId": "{{ ID from CatalogContent > displayCourse }}"
}
Then call the Comments query, using the data returned from the Forums query.
query Comments(
$commentableId: ID!,
$commentableType: CommentableType!,
) {
Comments(
commentableId: $commentableId,
commentableType: $commentableType,
) {
comments {
asset
assetFileName
body
}
}
}
// SAMPLE VARIABLES
{
"commentableId": "{{ ID from Forums query }}",
"commentableType": "discussionBoard"
}
Fetching Discussion Threads on a Course
To fetch a discussion thread on a course, you’ll need to call three queries consecutively: Forums, Threads, and Comments.
First call the Forums query like so:
query Forums(
$courseId: ID!
) {
Forums(
courseId: $courseId
) {
id // <-- pass this into the Comments query
label
}
}
// SAMPLE VARIABLES
{
"courseId": "{{ ID from CatalogContent > displayCourse }}"
}
Call the Threads query like so:
query Threads(
$forumId: ID!,
$courseId: ID!
) {
Threads(
forumId: $forumId,
courseId: $courseId
) {
threads {
body
id
}
}
}
// SAMPLE VARIABLES
{
"forumId": "{{ ID from Forums query }}",
"courseId": "{{ ID from CatalogContent > DisplayCourse }}"
}
Finally call the Comments query like so:
query Comments(
$commentableId: ID!,
$commentableType: CommentableType!,
) {
Comments(
commentableId: $commentableId,
commentableType: $commentableType,
) {
comments {
asset
assetFileName
body
}
}
}
// SAMPLE VARIABLES
{
"commentableId": "{{ ID from Threads query }}",
"commentableType": "thread"
}
GraphQL Rate Limits
In order for a request to be processed, there are 3 limitations that must be met:
– A single query must not exceed a calculated complexity of 1000 points.
– Each instance is allotted 100,000 points per minute. This is not on a rolling basis; the points allowance resets 60 seconds from the first request.
– The instance can not exceed 1000 requests per minute, regardless of complexity or points allowance.
Helium is meant to scale with your learning instance. If you have any concerns on limitations, please reach out to us.
Calculating Query Complexity
Each requested property costs 1 point by default.
// Query example
query {
CurrentUser {
firstName
lastName
}
}
// 1 (firstName) + 1 (lastName) = a 2 point query
Certain queries types, such as `CourseViews` or `Courses` may have an additional cost as they require more resource-intensive queries.
// Query Example
query {
CatalogContent {
id
title
slug
}
}
// 1(id) + 1(title) + 1(slug) + 3(performance cost) = a 6 point query
Additional Rate Limiting Information
Information related to points can be found in the following headers
X-RateLimit-Remaining: Number of allotted points remaining in the current duration
X-RateLimit-Reset: Timestamp at which point the allotted points will reset
Retry-After: Received only after exceeding a limit, denotes the number of seconds to wait before attempting the request again
Project Breakdown
/components
This is where you can create your own custom react components! We’ve include a few of our standard ones to get you started: feel free to use them, customize or create your own.
/pages
Helium uses a file based routing system as explained below.
The pages directory is where you build out different pages of your Helium site and handle routing. Every folder within the /pages directory corresponds to a different subdirectory of your url. If your learning instance’s url is learn.com, creating a folder called test in /pages and a file named index.page.tsx in /pages/test will create a route on learn.com/test where the contents of index.page.tsx will be served. Additionally, if you want to create another route on top of test, you can create another file in /pages/test following a similar pattern. For example, creating a file named custom.page.tsx in /pages/test would create a route at learn.com/test/custom.
It’s also useful to know how to remove Helium pages. Say you deployed a page to Helium and for some reason you want to take it down. Simply remove the corresponding folder or file and re-run $ npm run deploy
.
Note: Your Thought Industries Learning Instance comes with several pre-built routes such as /catalog /register and /support. If you create a folder in /pages called /catalog with a file named index.page.tsx, the content of that file will be served at learn.com/catalog.
/locales
The only file in this directory is translations.json which handles translations in your helium app. Any translation you set up in the TI Admin Dashboard get’s converted here so your translations will remain consistent. If you ever change any translations on the Admin side, simply run $ npm run update-translations
and the file will update.
/server
This folders contains a simple file for started a local web server.
/vendor
This folder contains code that allows you to publish MDX files as pages on your Helium site. MDX files are Markdown files that also allow you to use JSX with markdown. You can read more about MDX here.
For example, if you create a directory called /markdown-example
with a file called index.page.mdx
, that MDX file will be served at example.com/markdown-example
.
Components
This is a high level overview of some of the main Helium components currently available. For a complete, up-to-date list of all Helium Components and more in-depth, technical documentation please reference our Storybook component documentation.
@thoughtindustries/catalog
The Catalog allows learners to search and browse for content. It is a central component to any online learning platform.
@thoughtindustries/content
A collection of content specific resources for @thoughtindustries/helium UI components. It comes with typings and React Hooks for making GraphQL queries and mutations, as well as utilities for content hydration.
@thoughtindustries/featured-content
Featured Content components add real power to your platform page creation. These components let you display courses, microlearning, and products in neat blocks. Configure them to show static content or set them up to dynamically exhibit content based on search queries or tags.
@thoughtindustries/dashboard-stats
The Dashboard Stats component displays a high level view of how many courses the user has access to, as well as how many threads/comments (“collaborations”) they have participated in, alongside how many certificates they have earned.
Hooks
useQuery
useQuery
is a hook provided by the Apollo client and allows you to write custom GraphQL queries and mutations. Below is a code example of a component which utilizes useQuery
to fetch the name of a company to welcome the user:
import React from 'react';
import { gql, useQuery } from '@apollo/client';
export { Welcome };
function Welcome() {
const query = gql`
query CompanyDetailsQuery {
CompanyDetails {
__typename
name
}
}`;
const { data } = useQuery(query);
const companyName = data && data.CompanyDetails ? data.CompanyDetails.name : 'No name';
return (
<div>
<h1>Welcome to {companyName}! </h1>
</div>
);
}
useCatalogQuery
The useCatalogQuery
hook the CatalogQuery
GraphQL document, essentially an array of content items with data such as contentType
, description
, authors
, etc. There are three variable fields you pass in to the useCatalogQuery
hook.
- The
query
field allows you to filter which content in the catalog you want to see. Input forquery
follows a similar pattern to writing search queries for the Ti Platform, more of which can be read here. - The
querySort
field controls the order of results - The
querySignature
field is a security field…
// Fetch only Learning Path content types, sorted by when it was last updated
const { data, loading, error } = useCatalogQuery({
variables: {
query: "contentType:\"Learning Path\"",
querySort: "updatedAt:asc"
}
});
useCatalogContentQuery
The useCatalogContentQuery hook is very similar to the useCatalogQuery hook.
useContentsQuery
The useContentsQuery hook is another way to fetch content based off of content ids. The useContentsQuery hook takes in an array of ids and returns a QueryContents object.
import React from "react";
import { useContentsQuery } from '@thoughtindustries/content';
export { Page }
function Page() {
const { data, loading, error } = useContentsQuery({
variables: {
ids: ['c2653420-295a-4be0-a415-f1256cf81520',
'536434a8-2e69-4117-b21e-dfa72900f4bf']
}
});
console.log(data)
return(
<div>
<h1>Hey</h1>
</div>
)
}
useLanguagesQueryQuery
The useLanguagesQueryQuery hook is a hook to see all the languages supported in your Learning Instance. You can add different languages in your admin under translations. The useLanguagesQueryQuery takes in no arguments and returns a Languages object.
useAddResourceToQueueMutation
The useAddResourceToQueueMutation hook is relates to enrolling a user in a piece of content. It’s most common use case is in conjunction with the featured-content
component.
function Page() {
const [addResourceToQueue] = useAddResourceToQueueMutation();
const handleAddedToQueue = (item: FeaturedContentContentItem): Promise<boolean | void> =>
item.displayCourse
? addResourceToQueue({ variables: { resourceId: item.displayCourse } }).then()
: Promise.resolve(undefined);
return (
<div className="px-4">
<FeaturedContent>
<ContentTileStandardLayout
desktopColumnCount={4}
onAddedToQueue={handleAddedToQueue}
>
{/* Content Tile Items */}
</ContentTileStandardLayout>
</FeaturedContent>
</div>
);
}
useUserStatsQuery
The useUserStatsQuery hook returns a CurrentUser object and takes in no arguments. The CurrentUser object is populated with the fields of availableCoursesCount, certificatesCount, collaborationsCount, completedCoursesCount, and startedCoursesCount.
useRecentContentQuery
The useRecentContentQuery hook relates to fetching content that a user recently viewed. It returns a UserRecentContent
object.
Developer and Designer Tools
Storybook Component Documentation
All of our components are visually documented with Storybook. Storybook is a development environment for UI components. It allows you to browse a component library, view the different states of each component, and interactively develop and test components.
Useful Extensions
We have written a lot of code as we’ve built out Helium. Here are a few extensions we found useful if you’d like to try them out too (no affiliation):
GraphQL Explorer
- Run npm run dev and then go to http://localhost:3000/graphiql to find the GraphQL Explorer
- Documentation is available when you click docs button
- The GraphQL Explorer is an extremely useful tool for writing, testing, and validating GraphQL queries before you use them in your actual code
- Alternatively, you can view the static version of our GraphQL documentation here.
Figma Helium Design Library
Best Practices
Packages and Dependencies
Because Cloudflare Workers do not utilize containerized processes running an instance of a language runtime, certain Nodejs-specific dependencies – such as fs or crypto – may not function correctly, or could prevent the project from publishing to the Cloudflare Worker correctly. But, any package that utilizes webpack or another polyfill bundler should run within the environment.
Deployment Tips
Add both your sandbox instance and your production instance when running $ npm run authenticate
. When you want to deploy your project to production, first deploy to your sandbox instance to preview the changes and then deploy to your production instance when you confirm the look and feel. If you didn’t add both instances when you ran $ npm run authenticate
you can simply go to you ti-config.json file and enter the instance details there.
You should deploy early and often to your sandbox. Building a headless LMS and integrating with 3rd parties can introduce complexities if not done carefully.
3rd Party Libraries
Because Helium is fully open source and headless, you can easily use 3rd party developed libraries and components to quickly build a world class learning experience. However, there are some things to be careful about. Helium utilizes Cloudflare workers for hosting and not all components or libraries are compatible with the Helium stack. The quickest way to check is to install a 3rd party library and see if can still deploy successfully to your sandbox.
Writing GraphQL Queries
Only query the data you need, when you need it. One of the greatest features of GraphQL is that you only receive the data you need, nothing more and nothing less. Each component should only fetch data they need. While it is possible to run a single large query in the root component and then pass data to its child components, this can lead to poorer performance.