diff --git a/ui/Dockerfile b/ui/Dockerfile
index 2d917caa1f..7a3b80042b 100644
--- a/ui/Dockerfile
+++ b/ui/Dockerfile
@@ -16,13 +16,19 @@
FROM node:14.18.1-alpine3.11 as canopy-app-build-stage
RUN apk add --no-cache python g++ git make
+COPY ui/canopyjs /canopyjs
+ENV REACT_APP_SPLINTER_URL "/splinterd"
+ENV REACT_APP_SAPLING_URL "/sapling-dev-server"
+ENV REACT_APP_GRID_URL "/gridd"
+WORKDIR /canopyjs
+RUN npm config set unsafe-perm true \
+ && npm install \
+ && npm run build
WORKDIR /ui
COPY ui/grid-ui/package*.json ./
RUN npm config set unsafe-perm true && npm install
COPY ui/grid-ui .
-ENV REACT_APP_SPLINTER_URL "/splinterd"
-ENV REACT_APP_SAPLING_URL "/sapling-dev-server"
-ENV REACT_APP_GRID_URL "/gridd"
+
RUN npm run build
WORKDIR /ui/build
ARG REPO_VERSION
@@ -42,6 +48,11 @@ RUN apk add --no-cache python g++ git make \
COPY ui/saplings /saplings
COPY ui/sapling-dev-server /sapling-dev-server
COPY ui/protos /protos
+COPY ui/saplingjs /saplingjs
+
+WORKDIR /saplingjs
+RUN npm install \
+ && npm run build
ARG PUBLIC_URL_PARTIAL
ENV PUBLIC_URL $PUBLIC_URL_PARTIAL
diff --git a/ui/canopyjs/.babelrc b/ui/canopyjs/.babelrc
new file mode 100644
index 0000000000..67d7adc2ae
--- /dev/null
+++ b/ui/canopyjs/.babelrc
@@ -0,0 +1,6 @@
+{
+ "presets": ["@babel/preset-react", "@babel/preset-env"],
+ "plugins": [
+ ["@babel/transform-runtime"]
+ ]
+}
diff --git a/ui/canopyjs/.dockerignore b/ui/canopyjs/.dockerignore
new file mode 100644
index 0000000000..08017e5a0a
--- /dev/null
+++ b/ui/canopyjs/.dockerignore
@@ -0,0 +1,22 @@
+# Copyright 2018-2020 Cargill Incorporated
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+**/node_modules
+**/.pnp
+**/.pnp.js
+**/yarn.lock
+**/package-lock.json
+**/coverage
+**/build
+**/dist
diff --git a/ui/canopyjs/.env b/ui/canopyjs/.env
new file mode 100644
index 0000000000..cbe9914025
--- /dev/null
+++ b/ui/canopyjs/.env
@@ -0,0 +1,17 @@
+# Copyright 2018-2020 Cargill Incorporated
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+ISOLATION_ID=latest
+DISTRO=bionic
+REPO_VERSION=0.3.12-dev
diff --git a/ui/canopyjs/.eslintignore b/ui/canopyjs/.eslintignore
new file mode 100644
index 0000000000..bf6b87e7aa
--- /dev/null
+++ b/ui/canopyjs/.eslintignore
@@ -0,0 +1,16 @@
+# Copyright 2018-2020 Cargill Incorporated
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+saplings/*
+build/*
diff --git a/ui/canopyjs/.eslintrc b/ui/canopyjs/.eslintrc
new file mode 100644
index 0000000000..9f320e0238
--- /dev/null
+++ b/ui/canopyjs/.eslintrc
@@ -0,0 +1,29 @@
+{
+ "env": {
+ "es6": true,
+ "jest": true,
+ "browser": true
+ },
+ "parserOptions": {
+ "ecmaVersion": 2020
+ },
+ "extends": [
+ "airbnb",
+ "plugin:prettier/recommended"
+ ],
+ "rules": {
+ "react/jsx-filename-extension": 0,
+ "react/prefer-stateless-function": 0,
+ "import/prefer-default-export": 0,
+ "react/forbid-prop-types": 0
+ },
+ "settings": {
+ "import/resolver": {
+ "node": {
+ "paths": [
+ "src"
+ ]
+ }
+ }
+ }
+}
diff --git a/ui/canopyjs/.prettierrc b/ui/canopyjs/.prettierrc
new file mode 100644
index 0000000000..57a1013229
--- /dev/null
+++ b/ui/canopyjs/.prettierrc
@@ -0,0 +1,5 @@
+{
+ "printWidth": 80,
+ "tabWidth": 2,
+ "singleQuote": true
+}
diff --git a/ui/canopyjs/Dockerfile b/ui/canopyjs/Dockerfile
new file mode 100644
index 0000000000..cd78b107c8
--- /dev/null
+++ b/ui/canopyjs/Dockerfile
@@ -0,0 +1,21 @@
+# Copyright 2018-2020 Cargill Incorporated
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+FROM node:lts-alpine
+
+WORKDIR /splinter-canopyjs
+
+COPY . .
+
+RUN npm install
diff --git a/ui/canopyjs/LICENSE b/ui/canopyjs/LICENSE
new file mode 100644
index 0000000000..141e5aeba0
--- /dev/null
+++ b/ui/canopyjs/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2018-2020 Cargill, Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
\ No newline at end of file
diff --git a/ui/canopyjs/README.md b/ui/canopyjs/README.md
new file mode 100644
index 0000000000..1d221f1ad1
--- /dev/null
+++ b/ui/canopyjs/README.md
@@ -0,0 +1,128 @@
+
+
+# CanopyJS
+
+CanopyJS is a library for building Canopy applications. A Canopy application is
+a React app that is capable of dynamically loading in saplings, which are UI
+components designed to work with Splinter.
+
+The central component provided by CanopyJS is a React context provider called
+`CanopyProvider`. This context provider should wrap the top level component of
+a Canopy application.
+
+See [splinter.dev](https://www.splinter.dev/) for Splinter documentation,
+release notes, and community information.
+
+## Features
+
+- Provides functionality for loading saplings into the Canopy application
+- Implements some of the functions that are defined in SaplingJS
+- Exposes shared configuration to saplings and Canopy application components
+
+## Configuration
+
+CanopyJS makes use of two endpoints, `splinterURL` and `saplingURL`.
+
+### splinterURL
+
+`splinterURL` is the URL where the Splinter daemon is running. This URL will be
+used by Canopy and saplings to interact with Splinter via the Splinter daemon's
+REST API. Examples of these interactions would include:
+- Submitting transactions to a Scabbard service
+- Managing users using the Biome module of Splinter
+
+### saplingURL
+
+`saplingURL` is the URL where saplings are being served from. On startup,
+canopyJS will attempt to fetch sapling configuration from the following
+endpoints:
+
+- `${saplingURL}/configSaplings`: Config saplings
+- `${saplingURL}/userSaplings`: User saplings
+
+See the example in `splinter/canopy/app/saplings` for an example of these
+configuration responses.
+
+## Example
+
+### App.js
+
+```javascript
+import React from 'react';
+import { CanopyProvider } from 'canopyjs';
+
+import SideNav from './components/SideNav';
+
+function CanopyApp() {
+ return (
+
+
+
+ );
+}
+export default CanopyApp;
+```
+
+In this example, `saplingURL` and `splinterURL` are set as React app environment
+variables prior to starting up the application. The `SideNav` component gets
+wrapped by the `CanopyProvider`, which gives it access to the React context
+provided by CanopyJS.
+
+### SideNav.js
+
+```javascript
+import React from 'react';
+import { useUserSaplings } from 'canopyjs';
+
+import NavItem from './NavItem';
+
+function SideNav() {
+ const userSaplings = useUserSaplings();
+ const userSaplingRoutes = userSaplings.map(
+ ({ displayName, namespace, icon }) => {
+ return {
+ path: `/${namespace}`,
+ displayName,
+ logo: icon
+ };
+ }
+ );
+ const userSaplingTabs = userSaplingRoutes.map(
+ ({ path, displayName, logo }) => {
+ return ;
+ }
+ );
+
+ return (
+ <>
+
+
+ );
+}
+
+TabBox.defaultProps = {
+ keyExtractor: (_, contentIndex) => contentIndex
+};
+
+TabBox.propTypes = {
+ contents: PropTypes.arrayOf(
+ PropTypes.shape({
+ title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
+ content: PropTypes.oneOfType([PropTypes.string, PropTypes.func])
+ })
+ ).isRequired,
+ keyExtractor: PropTypes.func
+};
diff --git a/ui/canopyjs/src/components/tabBox/TabBox.scss b/ui/canopyjs/src/components/tabBox/TabBox.scss
new file mode 100644
index 0000000000..1706152412
--- /dev/null
+++ b/ui/canopyjs/src/components/tabBox/TabBox.scss
@@ -0,0 +1,65 @@
+/**
+ * Copyright 2018-2020 Cargill Incorporated
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.tab-box {
+ width: fit-content;
+
+ .tab-box-options {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ align-items: stretch;
+ align-content: stretch;
+
+ .tab-box-option {
+ flex-basis: $padding-s * 6;
+ flex-grow: 1;
+ min-width: $padding-s * 6;
+ padding: $padding-s;
+ color: $text-primary;
+ background-color: $background-light;
+ cursor: pointer;
+ font-family: var(--fontFamily-primary) !important;
+ border: none;
+
+ &:first-of-type {
+ border-radius: 5px 0 0;
+ border-right: 0;
+ }
+
+ &:not(:first-of-type) {
+ margin-left: -1px;
+ border-radius: 0 5px 0 0;
+ border-left: 0;
+ }
+
+ &.active {
+ background: var(--color-primary-light);
+ }
+ }
+ }
+
+ .tab-box-content {
+ background-color: $background-white;
+ border-radius: 0 0 5px 5px;
+ border-top: 0;
+ padding: $padding-s;
+ overflow-y: auto;
+ outline: none;
+ }
+
+ clear: both;
+}
diff --git a/ui/canopyjs/src/index.js b/ui/canopyjs/src/index.js
new file mode 100644
index 0000000000..860a0fc59f
--- /dev/null
+++ b/ui/canopyjs/src/index.js
@@ -0,0 +1,19 @@
+/* eslint-disable react/prop-types */
+/**
+ * Copyright 2018-2020 Cargill Incorporated
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export * from './components';
+export * from './CanopyContext';
diff --git a/ui/canopyjs/src/loadSaplings.js b/ui/canopyjs/src/loadSaplings.js
new file mode 100644
index 0000000000..cecda367b3
--- /dev/null
+++ b/ui/canopyjs/src/loadSaplings.js
@@ -0,0 +1,101 @@
+/**
+ * Copyright 2018-2020 Cargill Incorporated
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import promiseLoader from './promiseLoader';
+import { styleLoader, unloadStylesByClassName } from './styleLoader';
+
+function toUrl(filename) {
+ if (!filename.startsWith('http')) {
+ return `http://${filename}`;
+ }
+ return filename;
+}
+
+export async function mountCurrentSapling(userSaplingsResponse) {
+ // Saplings will be guaranteed to to have a collision-free namespace that
+ // corresponds with the place entry in the URL's path
+
+ // for instance /example and all of its child paths
+ // (/example/first, /example/first/a, example/second)
+ // would all let load the Sapling with `example` in its namespace attribute
+
+ // saplings themselves can take over routing at that point to manage their
+ // own routing (ie preventing full page refreshes, push on history)
+
+ const topLevelPathRgx = /\/([^/]+)/i;
+ const pathMatches = topLevelPathRgx.exec(window.location.pathname);
+ const saplingNamespaceToLoad =
+ pathMatches && pathMatches[1] ? pathMatches[1] : null;
+ const currentSaplingManifest = userSaplingsResponse.find(
+ ({ namespace }) => saplingNamespaceToLoad === namespace
+ );
+
+ if (currentSaplingManifest) {
+ unloadStylesByClassName('user-sapling-stylesheet');
+ await Promise.all(
+ currentSaplingManifest.styleFiles.map(styleFile =>
+ styleLoader(toUrl(styleFile), 'user-sapling-stylesheet')
+ )
+ );
+ await Promise.all(
+ currentSaplingManifest.runtimeFiles.map(saplingFile =>
+ promiseLoader(toUrl(saplingFile))
+ )
+ );
+
+ return true;
+ }
+
+ return false;
+}
+
+export async function mountConfigSaplingStyles(saplingResponse) {
+ const saplingStyleFiles = saplingResponse
+ .map(sapling => sapling.styleFiles)
+ .flatMap(style => style)
+ .filter(style => style !== undefined);
+
+ if (saplingStyleFiles.length === 0) {
+ return false;
+ }
+
+ await Promise.all(
+ saplingStyleFiles.map(styleFile =>
+ styleLoader(toUrl(styleFile), 'config-sapling-stylesheet')
+ )
+ );
+ return true;
+}
+
+export async function mountConfigSaplings(configSaplingResponse) {
+ // Config Saplings need to be loaded with every page load.
+ // An example of a Config Saplings would be a module to handle
+ // user login/registration.
+ const configSaplingRuntimeFiles = configSaplingResponse
+ .map(s => s.runtimeFiles)
+ .flatMap(r => r);
+
+ if (configSaplingRuntimeFiles.length === 0) {
+ return false;
+ }
+
+ await Promise.all(
+ configSaplingRuntimeFiles.map(saplingFile =>
+ promiseLoader(toUrl(saplingFile))
+ )
+ );
+ return true;
+}
diff --git a/ui/canopyjs/src/promiseLoader.js b/ui/canopyjs/src/promiseLoader.js
new file mode 100644
index 0000000000..a881789b05
--- /dev/null
+++ b/ui/canopyjs/src/promiseLoader.js
@@ -0,0 +1,32 @@
+/**
+ * Copyright 2018-2020 Cargill Incorporated
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+function promiseLoader(scriptUrl) {
+ const canopyUrl = new URL(window.location);
+ const saplingUrl = new URL(scriptUrl);
+ return new Promise((resolve, reject) => {
+ const body = document.querySelector('body');
+ const script = document.createElement('script');
+ script.async = true;
+ script.crossOrigin = canopyUrl.origin !== saplingUrl.origin;
+ script.src = scriptUrl;
+ script.onload = resolve;
+ script.onerror = reject;
+ body.insertAdjacentElement('beforeend', script);
+ });
+}
+
+export default promiseLoader;
diff --git a/ui/canopyjs/src/request.js b/ui/canopyjs/src/request.js
new file mode 100644
index 0000000000..faf1e636ae
--- /dev/null
+++ b/ui/canopyjs/src/request.js
@@ -0,0 +1,58 @@
+/**
+ * Copyright 2018-2020 Cargill Incorporated
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+function errorResponse(request, message) {
+ return {
+ ok: false,
+ status: request.status,
+ statusText: request.statusText,
+ headers: request.getAllResponseHeaders(),
+ data: message || request.responseText,
+ json: JSON.parse(message || request.responseText)
+ };
+}
+
+export function get(url, headerFn = null) {
+ return new Promise(resolve => {
+ const request = new XMLHttpRequest();
+ request.open('GET', url, true);
+ if (headerFn) {
+ headerFn(request);
+ }
+ request.timeout = 5000;
+
+ request.onload = () => {
+ return resolve({
+ ok: request.status >= 200 && request.status < 300,
+ status: request.status,
+ statusText: request.statusText,
+ headers: request.getAllResponseHeaders(),
+ data: request.responseText,
+ json: JSON.parse(request.responseText)
+ });
+ };
+
+ request.onError = () => {
+ resolve(errorResponse());
+ };
+
+ request.ontimeout = () => {
+ resolve(errorResponse(request, 'Request took longer than expected.'));
+ };
+
+ request.send();
+ });
+}
diff --git a/ui/canopyjs/src/styleLoader.js b/ui/canopyjs/src/styleLoader.js
new file mode 100644
index 0000000000..8a9fe12d45
--- /dev/null
+++ b/ui/canopyjs/src/styleLoader.js
@@ -0,0 +1,38 @@
+/**
+ * Copyright 2018-2020 Cargill Incorporated
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+function unloadStylesByClassName(elementClassName) {
+ const stylesheetLinks = document.getElementsByClassName(elementClassName);
+ Array.from(stylesheetLinks).map(element => element.remove());
+}
+
+function styleLoader(styleUrl, isUserSaplingStyle) {
+ const url = new URL(styleUrl);
+ return new Promise((resolve, reject) => {
+ const head = document.querySelector('head');
+ const link = document.createElement('link');
+ if (isUserSaplingStyle) {
+ link.className = 'user-sapling-stylesheet';
+ }
+ link.href = url;
+ link.rel = 'stylesheet';
+ link.onload = resolve;
+ link.onerror = reject;
+ head.insertAdjacentElement('beforeend', link);
+ });
+}
+
+export { unloadStylesByClassName, styleLoader };
diff --git a/ui/canopyjs/src/styles/colors.scss b/ui/canopyjs/src/styles/colors.scss
new file mode 100644
index 0000000000..3bdd133f76
--- /dev/null
+++ b/ui/canopyjs/src/styles/colors.scss
@@ -0,0 +1,52 @@
+/**
+ * Copyright 2018-2020 Cargill Incorporated
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#brand-block,
+#background-block,
+#status-block {
+ width: 100%;
+ display: flex;
+ flex-wrap: wrap;
+
+ .color-block {
+ width: 25rem;
+ height: 15rem;
+ padding: 0 $padding-s $padding-s 0;
+ display: flex;
+ flex-direction: column;
+
+ &:last-child {
+ padding-right: 0;
+ }
+
+ .color {
+ width: 100%;
+ flex: 1 1 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ &:first-child {
+ border-top-left-radius: 1rem;
+ border-top-right-radius: 1rem;
+ }
+
+ &:last-child {
+ border-bottom-left-radius: 1rem;
+ border-bottom-right-radius: 1rem;
+ }
+ }
+ }
+}
diff --git a/ui/canopyjs/src/styles/components.scss b/ui/canopyjs/src/styles/components.scss
new file mode 100644
index 0000000000..ae15f217a1
--- /dev/null
+++ b/ui/canopyjs/src/styles/components.scss
@@ -0,0 +1,22 @@
+/**
+ * Copyright 2018-2020 Cargill Incorporated
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import './../components/progress/Progress.scss';
+@import './../components/tabBox/TabBox.scss';
+@import './../components/navigation/SideNav.scss';
+@import './../components/input/Input.scss';
+@import './../components/forms/AuthForm.scss';
+@import './../components/button/Button.scss';
diff --git a/ui/canopyjs/src/styles/core.scss b/ui/canopyjs/src/styles/core.scss
new file mode 100644
index 0000000000..eb3adfaf25
--- /dev/null
+++ b/ui/canopyjs/src/styles/core.scss
@@ -0,0 +1,85 @@
+/**
+ * Copyright 2018-2020 Cargill Incorporated
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import './mixins.scss';
+@import './layout.scss';
+@import './navigation.scss';
+@import './colors.scss';
+@import './components.scss';
+
+html {
+ font-size: 16px;
+}
+
+$display-types: (
+ 'none': none,
+ 'block': block,
+ 'flex': flex,
+ 'grid': grid,
+ 'inlineGrid': inline-grid,
+ 'inlineBlock': inline-block,
+ 'inlineFlex': inline-flex
+);
+
+@each $name, $value in $display-types {
+ .display-#{$name} {
+ display: $value;
+ }
+}
+
+$flex-directions: (
+ 'row': row,
+ 'column': column
+);
+
+@each $name, $value in $flex-directions {
+ .flexDirection-#{$name} {
+ flex-direction: $value;
+ }
+}
+
+$flex-alignments: (
+ 'flexStart': flex-start,
+ 'flexEnd': flex-end,
+ 'center': center,
+ 'spaceAround': space-around,
+ 'spaceBetween': space-between
+);
+
+@each $name, $value in $flex-alignments {
+ .justifyItems-#{$name} {
+ justify-items: $value;
+ }
+
+ .justifyContent-#{$name} {
+ justify-content: $value;
+ }
+
+ .justifySelf-#{$name} {
+ justify-self: $value;
+ }
+
+ .alignItems-#{$name} {
+ align-items: $value;
+ }
+
+ .alignContent-#{$name} {
+ align-content: $value;
+ }
+
+ .alignSelf-#{$name} {
+ align-self: $value;
+ }
+}
diff --git a/ui/canopyjs/src/styles/layout.scss b/ui/canopyjs/src/styles/layout.scss
new file mode 100644
index 0000000000..11d82d3446
--- /dev/null
+++ b/ui/canopyjs/src/styles/layout.scss
@@ -0,0 +1,112 @@
+/**
+ * Copyright 2018-2020 Cargill Incorporated
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+$space-sizes: (
+ 's': 16px,
+ 'm': 24px,
+ 'l': 36px
+);
+
+@each $name, $size in $space-sizes {
+ .margin-#{$name} {
+ margin: $size;
+ }
+
+ .marginTop-#{$name} {
+ margin-top: $size;
+ }
+
+ .marginRight-#{$name} {
+ margin-right: $size;
+ }
+
+ .marginBottom-#{$name} {
+ margin-bottom: $size;
+ }
+
+ .marginLeft-#{$name} {
+ margin-left: $size;
+ }
+
+ .padding-#{$name} {
+ padding: $size;
+ }
+
+ .paddingTop-#{$name} {
+ padding-top: $size;
+ }
+
+ .paddingRight-#{$name} {
+ padding-right: $size;
+ }
+
+ .paddingBottom-#{$name} {
+ padding-bottom: $size;
+ }
+
+ .paddingLeft-#{$name} {
+ padding-left: $size;
+ }
+}
+
+@for $i from 1 through 6 {
+ .grid-#{$i} {
+ grid-template-columns: repeat($i, 1fr);
+ grid-column-gap: $padding-m;
+ }
+}
+
+@for $i from 0 through 5 {
+ .borderRight-#{$i} {
+ border-right: ($i * 1px) solid;
+ }
+
+ .borderBottom-#{$i} {
+ border-bottom: ($i * 1px) solid;
+ }
+
+ .borderLeft-#{$i} {
+ border-left: ($i * 1px) solid;
+ }
+
+ .borderWidth-#{$i} {
+ border-width: $i * 1px;
+ }
+
+ .borderTopWidth-#{$i} {
+ border-top-width: $i * 1px;
+ }
+
+ .borderRightWidth-#{$i} {
+ border-right-width: $i * 1px;
+ }
+
+ .borderBottomWidth-#{$i} {
+ border-bottom-width: $i * 1px;
+ }
+
+ .borderLeftWidth-#{$i} {
+ border-left-width: $i * 1px;
+ }
+}
+
+@mixin border($style, $width) {
+ border-style: $style;
+ border-width: $width * 1px;
+}
+
+.borderStyle-solid {
+ border-style: solid;
+}
diff --git a/ui/canopyjs/src/styles/mixins.scss b/ui/canopyjs/src/styles/mixins.scss
new file mode 100644
index 0000000000..9e67951ba7
--- /dev/null
+++ b/ui/canopyjs/src/styles/mixins.scss
@@ -0,0 +1,23 @@
+/**
+ * Copyright 2018-2020 Cargill Incorporated
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@mixin arrow($size, $color) {
+ width: 0;
+ height: 0;
+ border-left: $size solid transparent;
+ border-right: $size solid transparent;
+ border-bottom: $size solid $color;
+ transition: transform 0.2s linear;
+}
diff --git a/ui/canopyjs/src/styles/navigation.scss b/ui/canopyjs/src/styles/navigation.scss
new file mode 100644
index 0000000000..228d56f432
--- /dev/null
+++ b/ui/canopyjs/src/styles/navigation.scss
@@ -0,0 +1,75 @@
+/**
+ * Copyright 2018-2020 Cargill Incorporated
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+.nav-item {
+ width: 100%;
+
+ .label {
+ width: 100%;
+ box-sizing: border-box;
+ font-size: 1.4rem;
+ cursor: pointer;
+ color: $text-light;
+ transition: all 0.2s linear;
+
+ &:hover {
+ color: $text-primary;
+ }
+ }
+
+ .nav-logo {
+ width: 16rem;
+ height: 16rem;
+ margin-bottom: $margin-m;
+ }
+ .nav-items {
+ width: 100%;
+ margin-bottom: auto;
+
+ .nav-item {
+ display: grid;
+ grid-template-columns: [icon-start] 4rem [icon-end label-start] 12rem [label-end];
+ grid-column-gap: $padding-l;
+ align-items: center;
+ padding: $padding-s;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+ }
+
+ .nested {
+ overflow: hidden;
+ background: #eeeeee;
+ width: 100%;
+ }
+}
+
+@media only screen and (max-width: $mobile) {
+ .nav {
+ transition: width $transitionDuration-s linear, visibility 0s 0s;
+
+ .nav-logo {
+ width: 4rem;
+ height: 4rem;
+ }
+
+ &:not(.is-open) {
+ width: 0;
+ visibility: hidden;
+ transition: width $transitionDuration-s linear,
+ visibility 0s $transitionDuration-s;
+ }
+ }
+}
diff --git a/ui/canopyjs/src/themes/default/colors.scss b/ui/canopyjs/src/themes/default/colors.scss
new file mode 100644
index 0000000000..a8309bb6f9
--- /dev/null
+++ b/ui/canopyjs/src/themes/default/colors.scss
@@ -0,0 +1,96 @@
+/**
+ * Copyright 2018-2020 Cargill Incorporated
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+ // Brand
+ $color-primary: rgb(69, 190, 147) !default;
+ $color-primary-light: lighten($color-primary, 15%) !default;
+ $color-primary-dark: darken($color-primary, 15%) !default;
+ $color-secondary: rgb(115, 29, 216) !default;
+ $color-secondary-light: lighten($color-secondary, 15%);
+ $color-secondary-dark: darken($color-secondary, 15%);
+
+ // Backgrounds
+ $background-white: rgb(255, 255, 255) !default;
+ $background-light: rgb(239, 239, 239) !default;
+ $background-colors: (
+ 'white': $background-white,
+ 'light': $background-light
+ );
+
+ @each $name, $color in $background-colors {
+ .background-#{$name} {
+ background: $color;
+ }
+ }
+
+:root {
+ --background-color-white: #{$background-white};
+ --background-color-light: #{$background-light};
+ --color-primary: #{$color-primary};
+ --color-primary-light: #{$color-primary-light};
+ --color-primary-dark: #{$color-primary-dark};
+ --color-secondary: #{$color-secondary};
+ --color-secondary-light: #{$color-secondary-light};
+ --color-secondary-dark: #{$color-secondary-dark};
+ --color-dark-grey: #333333;
+ --color-grey: #555555;
+ --color-text--on-dark: rbga(255, 255, 255, 0.9);
+ --color-text-light--on-dark: rgba(255, 255, 255, 0.7);
+ --color-text-lighter--on-dark: rgba(255, 255, 255, 0.4);
+}
+
+$brand-colors: (
+ 'primary-light': $color-primary-light,
+ 'primary': $color-primary,
+ 'primary-dark': $color-primary-dark,
+ 'secondary-light': $color-secondary-light,
+ 'secondary': $color-secondary,
+ 'secondary-dark': $color-secondary-dark
+);
+@each $name, $color in $brand-colors {
+ .background-#{$name} {
+ background: $color;
+ }
+}
+
+// Components
+$link-color: #4d9de0;
+
+
+// Text
+$text-primary: var(--color-dark-grey);
+$text-light: var(--color-grey);
+$text-white: rgb(255, 255, 255);
+
+// Status
+$color-success: rgb(0, 211, 74) !default;
+$color-danger: rgb(219, 3, 0) !default;
+$color-warning: rgb(255, 225, 0) !default;
+$status-colors: (
+ 'success': $color-success,
+ 'danger': $color-danger,
+ 'warning': $color-warning
+);
+
+@each $name, $color in $status-colors {
+ .background-#{$name} {
+ background: $color;
+ }
+
+ .color-#{$name} {
+ color: $color;
+ }
+}
diff --git a/ui/canopyjs/src/themes/default/index.js b/ui/canopyjs/src/themes/default/index.js
new file mode 100644
index 0000000000..686a982dec
--- /dev/null
+++ b/ui/canopyjs/src/themes/default/index.js
@@ -0,0 +1,16 @@
+/**
+ * Copyright 2018-2020 Cargill Incorporated
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './index.scss';
diff --git a/ui/canopyjs/src/themes/default/index.scss b/ui/canopyjs/src/themes/default/index.scss
new file mode 100644
index 0000000000..8798b66620
--- /dev/null
+++ b/ui/canopyjs/src/themes/default/index.scss
@@ -0,0 +1,17 @@
+/**
+ * Copyright 2018-2020 Cargill Incorporated
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import './variables.scss';
+@import '../../styles/core.scss';
diff --git a/ui/canopyjs/src/themes/default/sizes.scss b/ui/canopyjs/src/themes/default/sizes.scss
new file mode 100644
index 0000000000..8ebba039de
--- /dev/null
+++ b/ui/canopyjs/src/themes/default/sizes.scss
@@ -0,0 +1,23 @@
+/**
+ * Copyright 2018-2020 Cargill Incorporated
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+$padding-s: 1rem;
+$padding-m: 1.5rem;
+$padding-l: 2rem;
+$margin-s: 1rem;
+$margin-m: 1.5rem;
+$margin-l: 2rem;
+
+$mobile: 768px;
diff --git a/ui/canopyjs/src/themes/default/timings.scss b/ui/canopyjs/src/themes/default/timings.scss
new file mode 100644
index 0000000000..db2009948a
--- /dev/null
+++ b/ui/canopyjs/src/themes/default/timings.scss
@@ -0,0 +1,18 @@
+/**
+ * Copyright 2018-2020 Cargill Incorporated
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+$transitionDuration-s: 0.2s;
+$transitionDuration-m: 0.3s;
+$transitionDuration-l: 0.5s;
diff --git a/ui/canopyjs/src/themes/default/typography.scss b/ui/canopyjs/src/themes/default/typography.scss
new file mode 100644
index 0000000000..fc4c506a67
--- /dev/null
+++ b/ui/canopyjs/src/themes/default/typography.scss
@@ -0,0 +1,102 @@
+/**
+ * Copyright 2018-2020 Cargill Incorporated
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import url('https://fonts.googleapis.com/css?family=Noto+Sans:400,400i,700|Roboto+Mono:400,700&display=swap');
+
+:root {
+ --fontFamily-primary: 'Roboto', 'Helvetica', sans-serif;
+ --fontFamily-code: 'Roboto Mono', monospace;
+}
+
+$fontFamily-primary: var(--fontFamily-primary);
+$fontFamily-code: var(--fontFamily-code);
+
+.fontFamily-primary {
+ font-family: $fontFamily-primary;
+}
+
+.fontFamily-code {
+ font-family: $fontFamily-code;
+}
+
+.fontWeight-bold {
+ font-weight: bold;
+}
+
+.fontWeight-regular {
+ font-weight: 400;
+}
+
+h1 {
+ @extend .fontFamily-primary;
+ @extend .fontWeight-bold;
+ font-size: 4rem;
+}
+
+h2 {
+ @extend .fontFamily-primary;
+ @extend .fontWeight-regular;
+ font-size: 3.5rem;
+}
+
+h3 {
+ @extend .fontFamily-primary;
+ @extend .fontWeight-regular;
+ font-size: 3rem;
+}
+
+h4 {
+ @extend .fontFamily-primary;
+ @extend .fontWeight-bold;
+ font-size: 2.5rem;
+}
+
+h5 {
+ @extend .fontFamily-primary;
+ @extend .fontWeight-regular;
+ font-size: 2rem;
+}
+
+h6 {
+ @extend .fontFamily-primary;
+ @extend .fontWeight-regular;
+ font-size: 1.5rem;
+}
+
+p,
+span {
+ @extend .fontFamily-primary;
+ @extend .fontWeight-regular;
+ font-size: 1rem;
+}
+
+b {
+ @extend .fontFamily-primary;
+ @extend .fontWeight-bold;
+ font-size: 1rem;
+}
+
+button {
+ @extend .fontFamily-primary;
+ @extend .fontWeight-regular;
+ font-size: 1rem;
+ letter-spacing: 1px;
+}
+
+a {
+ @extend span;
+ color: $link-color;
+ font-size: 1rem;
+}
diff --git a/ui/canopyjs/src/themes/default/variables.scss b/ui/canopyjs/src/themes/default/variables.scss
new file mode 100644
index 0000000000..85039ce21a
--- /dev/null
+++ b/ui/canopyjs/src/themes/default/variables.scss
@@ -0,0 +1,19 @@
+/**
+ * Copyright 2018-2020 Cargill Incorporated
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import './colors.scss';
+@import './sizes.scss';
+@import './timings.scss';
+@import './typography.scss';
diff --git a/ui/grid-ui/package.json b/ui/grid-ui/package.json
index 8b3cbdff88..edc0ebc2bb 100644
--- a/ui/grid-ui/package.json
+++ b/ui/grid-ui/package.json
@@ -12,7 +12,7 @@
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-scripts": "^3.4.0",
- "splinter-canopyjs": "github:Cargill/splinter-canopyjs#main"
+ "splinter-canopyjs": "file:../canopyjs"
},
"scripts": {
"start:saplings": "http-server .. -p 3030 --cors",
diff --git a/ui/saplingjs/.dockerignore b/ui/saplingjs/.dockerignore
new file mode 100644
index 0000000000..6edf06beed
--- /dev/null
+++ b/ui/saplingjs/.dockerignore
@@ -0,0 +1,22 @@
+# Copyright 2018-2020 Cargill Incorporated
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+**/node_modules
+**/.pnp
+**/.pnp.js
+**/yarn.lock
+**/coverage
+**/build
+**/dist
+**/package-lock.json
diff --git a/ui/saplingjs/.env b/ui/saplingjs/.env
new file mode 100644
index 0000000000..cbe9914025
--- /dev/null
+++ b/ui/saplingjs/.env
@@ -0,0 +1,17 @@
+# Copyright 2018-2020 Cargill Incorporated
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+ISOLATION_ID=latest
+DISTRO=bionic
+REPO_VERSION=0.3.12-dev
diff --git a/ui/saplingjs/.eslintrc b/ui/saplingjs/.eslintrc
new file mode 100644
index 0000000000..44ac895377
--- /dev/null
+++ b/ui/saplingjs/.eslintrc
@@ -0,0 +1,39 @@
+{
+ "parser": "@typescript-eslint/parser",
+ "plugins": ["@typescript-eslint"],
+ "settings": {
+ "import/resolver": {
+ "node": {
+ "extensions": [".js", ".ts"]
+ }
+ }
+ },
+ "env": {
+ "es6": true,
+ "jest": true,
+ "browser": true
+ },
+ "parserOptions": {
+ "ecmaVersion": 2020
+ },
+ "rules": {
+ "import/no-default-export": 2,
+ "import/prefer-default-export": 0,
+ "import/no-var-requires": 0,
+ "import/extensions": [
+ "error",
+ "ignorePackages",
+ {
+ "js": "never",
+ "jsx": "never",
+ "ts": "never",
+ "tsx": "never"
+ }
+ ]
+ },
+ "extends": [
+ "plugin:@typescript-eslint/recommended",
+ "airbnb",
+ "plugin:prettier/recommended"
+ ]
+}
diff --git a/ui/saplingjs/.prettierrc b/ui/saplingjs/.prettierrc
new file mode 100644
index 0000000000..57a1013229
--- /dev/null
+++ b/ui/saplingjs/.prettierrc
@@ -0,0 +1,5 @@
+{
+ "printWidth": 80,
+ "tabWidth": 2,
+ "singleQuote": true
+}
diff --git a/ui/saplingjs/Dockerfile b/ui/saplingjs/Dockerfile
new file mode 100644
index 0000000000..a9a99f9b86
--- /dev/null
+++ b/ui/saplingjs/Dockerfile
@@ -0,0 +1,25 @@
+# Copyright 2018-2020 Cargill Incorporated
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+FROM node:lts-alpine
+
+<<<<<<< HEAD:ui/saplingjs/Dockerfile
+WORKDIR /saplingjs
+=======
+WORKDIR /splinter-canopyjs
+>>>>>>> canopyjs-main:ui/canopyjs/Dockerfile
+
+COPY . .
+
+RUN npm install
diff --git a/ui/saplingjs/LICENSE b/ui/saplingjs/LICENSE
new file mode 100644
index 0000000000..141e5aeba0
--- /dev/null
+++ b/ui/saplingjs/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2018-2020 Cargill, Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
\ No newline at end of file
diff --git a/ui/saplingjs/README.md b/ui/saplingjs/README.md
new file mode 100644
index 0000000000..bd76e96b65
--- /dev/null
+++ b/ui/saplingjs/README.md
@@ -0,0 +1,21 @@
+
+
+# SaplingJS
+
+SaplingJS is a library for building saplings, which are UI plugins for Canopy
+applications. Saplings are designed to work with Splinter.
+
+See [splinter.dev](https://www.splinter.dev/) for Splinter documentation,
+release notes, and community information.
diff --git a/ui/saplingjs/VERSION b/ui/saplingjs/VERSION
new file mode 100644
index 0000000000..0b9c019963
--- /dev/null
+++ b/ui/saplingjs/VERSION
@@ -0,0 +1 @@
+0.3.12
diff --git a/ui/saplingjs/bin/get_version b/ui/saplingjs/bin/get_version
new file mode 100755
index 0000000000..8a89756e11
--- /dev/null
+++ b/ui/saplingjs/bin/get_version
@@ -0,0 +1,79 @@
+#!/usr/bin/env python3
+
+# Copyright 2016, 2017 Intel Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ------------------------------------------------------------------------------
+
+import os
+import subprocess
+import sys
+
+top_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
+
+version_file = top_dir + "/VERSION"
+
+with open(version_file, 'r') as f:
+ version_data = f.read().strip()
+
+
+def bump_version(version):
+ (major, minor, patch) = version.split('.')
+ if 'rc' in patch:
+ parts = patch.split('rc')
+ parts[1] = str(int(parts[1]) + 1)
+ patch = "rc".join(parts)
+ else:
+ patch = str(int(patch) + 1)
+ return ".".join([major, minor, patch])
+
+
+def auto_version(default, strict):
+ output = subprocess.check_output(['git', 'describe', '--dirty'])
+ parts = output.decode('utf-8').strip().split('-', 3)
+ parts[0] = parts[0][1:] # strip the leading 'v'
+ if len(parts) > 1:
+ parts[0] = bump_version(parts[0])
+ if default != parts[0]:
+ msg = "VERSION file and (bumped?) git describe versions differ: " \
+ "{} != {}".format(default, parts[0])
+ if strict:
+ print("ERROR: " + msg, file=sys.stderr)
+ sys.exit(1)
+ else:
+ print("WARNING: " + msg, file=sys.stderr)
+ parts[0] = default
+
+ if len(parts) > 1:
+ parts[0] = "-dev".join([parts[0], parts[1].replace("-", ".")])
+ if len(parts) == 4:
+ parts[0] = parts[0] + "-" + parts[3]
+ return parts[0]
+ else:
+ return parts[0]
+
+
+def version(default):
+ if 'VERSION' in os.environ:
+ if os.environ['VERSION'] == 'AUTO_STRICT':
+ version = auto_version(default, strict=True)
+ elif os.environ['VERSION'] == 'AUTO':
+ version = auto_version(default, strict=False)
+ else:
+ version = os.environ['VERSION']
+ else:
+ version = default + "-dev1"
+ return version
+
+
+print(version(version_data))
diff --git a/ui/saplingjs/docker/compose/.env b/ui/saplingjs/docker/compose/.env
new file mode 100644
index 0000000000..cbe9914025
--- /dev/null
+++ b/ui/saplingjs/docker/compose/.env
@@ -0,0 +1,17 @@
+# Copyright 2018-2020 Cargill Incorporated
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+ISOLATION_ID=latest
+DISTRO=bionic
+REPO_VERSION=0.3.12-dev
diff --git a/ui/saplingjs/docker/compose/run-lint.yaml b/ui/saplingjs/docker/compose/run-lint.yaml
new file mode 100644
index 0000000000..ebad2eb637
--- /dev/null
+++ b/ui/saplingjs/docker/compose/run-lint.yaml
@@ -0,0 +1,29 @@
+# Copyright 2018-2020 Cargill Incorporated
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+version: "3.7"
+
+services:
+<<<<<<< HEAD:ui/saplingjs/docker/compose/run-lint.yaml
+ lint-saplingjs:
+ build:
+ context: ../..
+ image: saplingjs:${ISOLATION_ID}
+=======
+ lint-canopyjs:
+ build:
+ context: ../..
+ image: canopyjs:${ISOLATION_ID}
+>>>>>>> canopyjs-main:ui/canopyjs/docker/compose/run-lint.yaml
+ command: yarn lint
diff --git a/ui/saplingjs/jest.config.js b/ui/saplingjs/jest.config.js
new file mode 100644
index 0000000000..edfb2c94d7
--- /dev/null
+++ b/ui/saplingjs/jest.config.js
@@ -0,0 +1,20 @@
+/**
+ * Copyright 2018-2020 Cargill Incorporated
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+module.exports = {
+ preset: 'ts-jest',
+ testEnvironment: 'jsdom'
+};
diff --git a/ui/saplingjs/package.json b/ui/saplingjs/package.json
new file mode 100644
index 0000000000..7899dfba82
--- /dev/null
+++ b/ui/saplingjs/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "splinter-saplingjs",
+ "private": false,
+ "version": "0.0.1",
+ "author": "Cargill Incorporated",
+ "license": "Apache-2.0",
+ "main": "dist/index.js",
+ "dependencies": {
+ "@babel/parser": "^7.8.3",
+ "@types/sjcl": "^1.0.29",
+ "babel-plugin-macros": "^2.8.0",
+ "js-yaml": "^3.13.1",
+ "sjcl": "^1.0.8"
+ },
+ "scripts": {
+ "test": "jest",
+ "build": "tsc -p ./tsconfig.json --strict --outDir dist",
+ "prepare": "npm run build",
+ "format": "prettier --write \"**/*.+(ts|*rc|json|js)\"",
+ "lint": "eslint src/*"
+ },
+ "devDependencies": {
+ "@types/jest": "^24.0.19",
+ "@types/uuid": "^3.4.5",
+ "@typescript-eslint/eslint-plugin": "^2.5.0",
+ "@typescript-eslint/parser": "^2.5.0",
+ "eslint": "^6.6.0",
+ "eslint-config-airbnb": "^18.0.1",
+ "eslint-config-prettier": "^6.4.0",
+ "eslint-plugin-import": "^2.18.2",
+ "eslint-plugin-jsx-a11y": "^6.2.3",
+ "eslint-plugin-prettier": "^3.1.1",
+ "eslint-plugin-react": "^7.16.0",
+ "jest": "^24.9.0",
+ "prettier": "^1.18.2",
+ "ts-jest": "^24.1.0",
+ "typescript": "^3.6.4"
+ }
+}
diff --git a/ui/saplingjs/src/crypto.ts b/ui/saplingjs/src/crypto.ts
new file mode 100644
index 0000000000..3990866937
--- /dev/null
+++ b/ui/saplingjs/src/crypto.ts
@@ -0,0 +1,37 @@
+/**
+ * Copyright 2018-2020 Cargill Incorporated
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import sjcl from 'sjcl';
+
+/**
+ * Encrypts a private key.
+ * @param password - Encryption key.
+ * @param privateKey - Unencrypted private key.
+ */
+export function encryptKey(privateKey: string, password: string): string {
+ return JSON.stringify(sjcl.encrypt(password, privateKey));
+}
+
+/**
+ * Decrypts a private key.
+ * @param password - Encryption key.
+ * @param encryptedPrivateKey - Encrypted private key.
+ */
+export function decryptKey(
+ encryptedPrivateKey: string,
+ password: string
+): string {
+ return sjcl.decrypt(password, encryptedPrivateKey);
+}
diff --git a/ui/saplingjs/src/index.spec.ts b/ui/saplingjs/src/index.spec.ts
new file mode 100644
index 0000000000..10a3c7200b
--- /dev/null
+++ b/ui/saplingjs/src/index.spec.ts
@@ -0,0 +1,108 @@
+/**
+ * Copyright 2018-2020 Cargill Incorporated
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const bootstrap = (): void => {
+ /* no op */
+};
+const completeUser = {
+ userId: 'COMPLETE',
+ displayName: 'canopy',
+ token: 'canopy.token'
+};
+const minimalUser = { userId: 'MINIMAL' };
+
+// In order to prevent the need to overwrite the window interface,
+// a intentional `any` is cast here.
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+(window as any).$CANOPY = {
+ registerConfigSapling: jest.fn(),
+ registerApp: jest.fn(),
+ getUser: jest.fn(() => completeUser),
+ setUser: jest.fn(),
+ getSharedConfig: jest.fn(() => ({
+ mock: true
+ }))
+};
+
+interface MockCanopy {
+ registerConfigSapling: jest.Mock;
+ registerApp: jest.Mock;
+ getUser: jest.Mock;
+ setUser: jest.Mock;
+ getSharedConfig: jest.Mock;
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const $CANOPY = (window as any).$CANOPY as MockCanopy;
+
+describe('CanopyJS', () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+ describe('registerApp(bootstrapFn)', () => {
+ it('should call the window.$CANOPY.registerApp function with the same signature as the register function', async () => {
+ expect.assertions(1);
+ // dynamic import is used here to ensure that the window.$CANOPY object has been set up
+ const { registerApp } = await import('./index');
+ registerApp(bootstrap);
+ expect($CANOPY.registerApp.mock.calls[0][0]).toEqual(bootstrap);
+ });
+ });
+
+ describe('registerConfigSapling(configNamespace, bootstrapFn)', () => {
+ it('should register to the window Canopy object', async () => {
+ expect.assertions(2);
+ // dynamic import is used here to ensure that the window.$CANOPY object has been set up
+ const { registerConfigSapling } = await import('./index');
+ registerConfigSapling('login', bootstrap);
+ expect($CANOPY.registerConfigSapling.mock.calls[0][0]).toEqual('login');
+ expect($CANOPY.registerConfigSapling.mock.calls[0][1]).toEqual(bootstrap);
+ });
+ });
+
+ describe('getUser()', () => {
+ it('should call getUser from window object', async () => {
+ expect.assertions(1);
+ // dynamic import is used here to ensure that the window.$CANOPY object has been set up
+ const { getUser } = await import('./index');
+ expect(getUser()).toEqual(completeUser);
+ });
+ });
+
+ describe('setUser(user)', () => {
+ it('should call setUser from window object with a complete user object', async () => {
+ expect.assertions(1);
+ // dynamic import is used here to ensure that the window.$CANOPY object has been set up
+ const { setUser } = await import('./index');
+ setUser(completeUser);
+ expect($CANOPY.setUser.mock.calls[0][0]).toEqual(completeUser);
+ });
+ it('it should call setUser from window object with a minimal user object', async () => {
+ expect.assertions(1);
+ // dynamic import is used here to ensure that the window.$CANOPY object has been set up
+ const { setUser } = await import('./index');
+ setUser(minimalUser);
+ expect($CANOPY.setUser.mock.calls[0][0]).toEqual(minimalUser);
+ });
+ });
+
+ describe('getSharedConfig', () => {
+ it('should call getSharedConfig from window object', async () => {
+ const { getSharedConfig } = await import('./index');
+ expect(getSharedConfig()).toEqual({ mock: true });
+ });
+ });
+});
diff --git a/ui/saplingjs/src/index.ts b/ui/saplingjs/src/index.ts
new file mode 100644
index 0000000000..09012073b7
--- /dev/null
+++ b/ui/saplingjs/src/index.ts
@@ -0,0 +1,107 @@
+/**
+ * Copyright 2018-2020 Cargill Incorporated
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { submitBatchList } from './submitter';
+export { decryptKey, encryptKey } from './crypto';
+
+interface User {
+ userId: string;
+ displayName?: string;
+ token?: string;
+}
+
+interface KeyPair {
+ publicKey: string;
+ privateKey: string;
+}
+
+interface SetUser {
+ (user: User): void;
+}
+
+interface SetKeys {
+ (keys: KeyPair): void;
+}
+
+interface SharedConfig {
+ canopyConfig: {
+ splinterURL: string;
+ };
+}
+
+interface GetSharedConfig {
+ (): SharedConfig;
+}
+
+interface GetUser {
+ (): User;
+}
+
+interface GetKeys {
+ (): KeyPair;
+}
+
+interface RegisterApp {
+ (bootstrapFunction: (domNode: Node) => void): void;
+}
+
+interface RegisterConfigSapling {
+ (
+ configNamespace: 'login' | 'notifications',
+ bootstrapFunction: () => void
+ ): void;
+}
+
+interface HideCanopy {
+ (): void;
+}
+
+interface Canopy {
+ registerApp: RegisterApp;
+ registerConfigSapling: RegisterConfigSapling;
+ getUser: GetUser;
+ setUser: SetUser;
+ setKeys: SetKeys;
+ getKeys: GetKeys;
+ getSharedConfig: GetSharedConfig;
+ hideCanopy: HideCanopy;
+}
+
+function assertAndGetWindowCanopy(): Canopy {
+ // In order to prevent the need to overwrite the window interface,
+ // a intentional `any` is cast here.
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ if (!window || !(window as any).$CANOPY) {
+ throw new Error(
+ `Must be in a Canopy with 'window.$CANOPY' in scope to call this CanopyJS functions`
+ );
+ }
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ return (window as any).$CANOPY;
+}
+
+const canopy = assertAndGetWindowCanopy();
+
+export const {
+ registerApp,
+ registerConfigSapling,
+ getUser,
+ setUser,
+ setKeys,
+ getKeys,
+ getSharedConfig,
+ hideCanopy
+}: Canopy = canopy;
diff --git a/ui/saplingjs/src/submitter.ts b/ui/saplingjs/src/submitter.ts
new file mode 100644
index 0000000000..5e83d51a68
--- /dev/null
+++ b/ui/saplingjs/src/submitter.ts
@@ -0,0 +1,101 @@
+/**
+ * Copyright 2018-2020 Cargill Incorporated
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+interface BatchStatus {
+ statusType: string;
+ message: BatchMessage[];
+}
+
+interface BatchMessage {
+ transactionId: string;
+ errorMessage: string;
+ errorData: number[];
+}
+
+interface BatchInfo {
+ id: string;
+ status: BatchStatus;
+}
+
+const HTTPMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
+
+/**
+ * Wrapper function to set up XHR.
+ * @param {string} method HTTP method for the request
+ * @param {string} url endpoint to make the request to
+ * @param {Uint8Array} data Byte array representation of the request body
+ * @param {function} headerFn Function to set the correct request headers
+ */
+async function http(
+ method: string,
+ url: string,
+ data: Uint8Array | null,
+ headerFn: (request: XMLHttpRequest) => void
+): Promise {
+ return new Promise((resolve, reject) => {
+ if (!HTTPMethods.includes(method.toUpperCase())) {
+ reject(Error('Invalid HTTP Method'));
+ }
+
+ const request = new XMLHttpRequest();
+ request.open(method, url);
+ if (headerFn) {
+ headerFn(request);
+ }
+ request.onload = (): void => {
+ if (request.status >= 200 && request.status < 300) {
+ resolve(request.response);
+ } else if (request.status >= 400 && request.status < 500) {
+ reject(
+ Error('Failed to send request. Contact the administrator for help.')
+ );
+ } else {
+ reject(
+ Error(
+ 'The server has encountered an error. Please contact the administrator.'
+ )
+ );
+ }
+ };
+ request.onerror = (): void => {
+ reject(
+ Error(
+ 'The server has encountered an error. Please contact the administrator.'
+ )
+ );
+ };
+ request.send(data);
+ });
+}
+
+/**
+ * Submits a batch list of transaction batches
+ * @param {string} url The endpoint to submit the batch list to
+ * @param {Uint8Array} batchList The serialized batch list
+ */
+export async function submitBatchList(
+ url: string,
+ batchList: Uint8Array
+): Promise {
+ return http('POST', url, batchList, (request: XMLHttpRequest) => {
+ request.setRequestHeader('Content-Type', 'application/octet-stream');
+ })
+ .catch(err => {
+ throw new Error(err);
+ })
+ .then(body => {
+ return JSON.parse(body).data as BatchInfo[];
+ });
+}
diff --git a/ui/saplingjs/tests/.env b/ui/saplingjs/tests/.env
new file mode 100644
index 0000000000..cbe9914025
--- /dev/null
+++ b/ui/saplingjs/tests/.env
@@ -0,0 +1,17 @@
+# Copyright 2018-2020 Cargill Incorporated
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+ISOLATION_ID=latest
+DISTRO=bionic
+REPO_VERSION=0.3.12-dev
diff --git a/ui/saplingjs/tests/test-splinter.yaml b/ui/saplingjs/tests/test-splinter.yaml
new file mode 100644
index 0000000000..9b23d9ddbb
--- /dev/null
+++ b/ui/saplingjs/tests/test-splinter.yaml
@@ -0,0 +1,25 @@
+# Copyright 2018-2020 Cargill Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ------------------------------------------------------------------------------
+
+version: "3.7"
+
+services:
+ unit-test-saplingjs:
+ build:
+ context: ..
+ environment:
+ - CI=true
+ image: saplingjs:${ISOLATION_ID}
+ command: yarn test
diff --git a/ui/saplingjs/tsconfig.json b/ui/saplingjs/tsconfig.json
new file mode 100644
index 0000000000..afe2494c0c
--- /dev/null
+++ b/ui/saplingjs/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "compilerOptions": {
+ "lib": ["es2015", "dom", "es2017"],
+ "esModuleInterop": true,
+ "declaration": true
+ },
+ "exclude": ["node_modules", "**/*.spec.ts"],
+ "include": ["src/**/*"]
+}
diff --git a/ui/saplings/circuits/package.json b/ui/saplings/circuits/package.json
index 87e1a8e183..d70f66c768 100644
--- a/ui/saplings/circuits/package.json
+++ b/ui/saplings/circuits/package.json
@@ -44,7 +44,7 @@
"react-dropdown": "^1.7.0",
"react-router-dom": "^5.1.2",
"react-toast-notifications": "^2.4.0",
- "splinter-saplingjs": "github:cargill/splinter-saplingjs#main",
+ "splinter-saplingjs": "file:../../saplingjs",
"transact-sdk": "^0.1.0"
},
"devDependencies": {
diff --git a/ui/saplings/circuits/test/Dockerfile b/ui/saplings/circuits/test/Dockerfile
index b718ad4914..0a59f5d92b 100644
--- a/ui/saplings/circuits/test/Dockerfile
+++ b/ui/saplings/circuits/test/Dockerfile
@@ -19,10 +19,9 @@ WORKDIR /saplings/circuits
COPY package*.json ./
-RUN apk add --no-cache git
-
-# Gives npm permission to run the prepare script in splinter-canopyjs as root
-RUN npm config set unsafe-perm true && npm install
+RUN apk add --no-cache git \
+ # Gives npm permission to run the prepare script in splinter-canopyjs as root
+ && npm install
COPY . .
diff --git a/ui/saplings/product/package.json b/ui/saplings/product/package.json
index 1571a3042f..e02fac8d13 100644
--- a/ui/saplings/product/package.json
+++ b/ui/saplings/product/package.json
@@ -57,7 +57,7 @@
"react-router-dom": "^5.1.2",
"react-table": "^7.6.3",
"react-toast-notifications": "^2.4.0",
- "splinter-saplingjs": "github:cargill/splinter-saplingjs#main",
+ "splinter-saplingjs": "file:../../saplingjs",
"transact-sdk": "^0.1.0"
},
"devDependencies": {
diff --git a/ui/saplings/profile/package.json b/ui/saplings/profile/package.json
index e775d71017..52484b874b 100644
--- a/ui/saplings/profile/package.json
+++ b/ui/saplings/profile/package.json
@@ -20,7 +20,7 @@
"react": "^16.12.0",
"react-dom": "^16.12.0",
"sjcl": "^1.0.8",
- "splinter-saplingjs": "github:cargill/splinter-saplingjs#main",
+ "splinter-saplingjs": "file:../../saplingjs",
"transact-sdk": "^0.1.0"
},
"scripts": {
diff --git a/ui/saplings/profile/test/Dockerfile b/ui/saplings/profile/test/Dockerfile
index 95cd88a7b7..0470123b58 100644
--- a/ui/saplings/profile/test/Dockerfile
+++ b/ui/saplings/profile/test/Dockerfile
@@ -19,9 +19,8 @@ WORKDIR /saplings/profile
COPY package*.json ./
-RUN apk add --no-cache git
-
-# Gives npm permission to run the prepare script in splinter-canopyjs as root
-RUN npm config set unsafe-perm true && npm install
+RUN apk add --no-cache git \
+ # Gives npm permission to run the prepare script in splinter-canopyjs as root
+ && npm install
COPY . .
diff --git a/ui/saplings/register-login/package.json b/ui/saplings/register-login/package.json
index 2fad35bdb3..60e13ac726 100644
--- a/ui/saplings/register-login/package.json
+++ b/ui/saplings/register-login/package.json
@@ -9,7 +9,7 @@
"axios": "^0.21.1",
"history": "^4.10.1",
"js-sha256": "^0.9.0",
- "splinter-saplingjs": "github:cargill/splinter-saplingjs#main"
+ "splinter-saplingjs": "file:../../saplingjs"
},
"scripts": {
"test": "jest",
diff --git a/ui/saplings/register-login/test/Dockerfile b/ui/saplings/register-login/test/Dockerfile
index 6f471b2c20..f4e9c87662 100644
--- a/ui/saplings/register-login/test/Dockerfile
+++ b/ui/saplings/register-login/test/Dockerfile
@@ -19,9 +19,8 @@ WORKDIR /saplings/register-login
COPY package*.json ./
-RUN apk add --no-cache git
-
-# Gives npm permission to run the prepare script in splinter-canopyjs as root
-RUN npm config set unsafe-perm true && npm install
+RUN apk add --no-cache git \
+ # Gives npm permission to run the prepare script in splinter-canopyjs as root
+ && npm install
COPY . .