Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions sample-application/baselayer/.eslintrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
env:
browser: true
node: true
es2021: true
extends:
- eslint:recommended
- plugin:react/recommended
parserOptions:
ecmaVersion: latest
sourceType: module
plugins:
- react
rules:
react/prop-types: off
35 changes: 35 additions & 0 deletions sample-application/baselayer/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
33 changes: 33 additions & 0 deletions sample-application/baselayer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Visier Baselayer Sample Application
Baselayer is a Visier sample application that showcases:
* Client application interaction with Visier's OAuth 2.0 client without any third-party OAuth libraries.
* API calls to Visier's public APIs built on top of a Next.js application.

Baselayer is built with [React](https://react.dev/) and [Next.js](https://nextjs.org/). It consists of 4 smaller React components, each showcasing a Visier API. All React components live together in the main React app. The React components use Visier's [Data Model and Data Query API](https://docs.visier.com/developer/apis/data-model-query/data-model-query-api.htm). Next.js routes API calls through `pages/api/execute.js`, which utilizes `axios` to make API calls. Each API call routes through this workflow before returning the data. Each component sets up its own API call within the component itself.

Baselayer also has a data intake widget to showcase Visier's [Direct Data Intake API](https://docs.visier.com/developer/Tutorials/direct-data-intake/send-data-to-visier.htm).

To use Baselayer, you must authenticate with OAuth 2.0 in Visier.

# Prerequisites
* Register a client application in Visier. For more information, see [Register a Client Application](https://docs.visier.com/developer/Studio/sign-in%20settings/oauth2-setup.htm).
* Obtain the client application's `Client ID` and `Client secret`.
* Set the client application's `Redirect URI` to http://localhost:3000/oauth2/callback. In development mode, Baselayer listens for authorization codes at this URL.
* Retrieve your Visier API key. For more information, see [Generate an API Key](https://docs.visier.com/developer/Studio/solution%20settings/api-key-generate.htm).
* Create Expense Report objects and load data in Visier. For more information, see [Create a Subject to Analyze Expense Report Data](https://docs.visier.com/developer/Tutorials/analytic-model/extend-analytic-model-subject.htm).
* Install `node`.

# Environment
Before you start the application, create a file named `.env.development.local` to define the following variables:
* `VISIER_HOST`: The base URL for API calls; for example, `https://vanity.api.visier.io`.
* `VISIER_APIKEY`: The API key required for all Visier API calls.
* `VISIER_CLIENT_ID`: A Visier-generated unique identifier for the registered OAuth 2.0 client.
* `VISIER_CLIENT_SECRET`: A Visier-generated secret to protect the registered OAuth 2.0 client.

## Installation
Run `yarn install` in a command line application of your choice, such as Terminal.

## Run the Sample
Execute `yarn dev` to run Baselayer in development mode. Next, navigate to `localhost:3000` to login, authenticate, and begin sending API calls.


7 changes: 7 additions & 0 deletions sample-application/baselayer/jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}
4 changes: 4 additions & 0 deletions sample-application/baselayer/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {}

module.exports = nextConfig
26 changes: 26 additions & 0 deletions sample-application/baselayer/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "employee-details",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"axios": "^1.5.0",
"bootstrap": "^5.3.2",
"next": "13.4.19",
"react": "18.2.0",
"react-bootstrap": "^2.8.0",
"react-dom": "18.2.0",
"react-error-boundary": "^4.0.11",
"zustand": "^4.4.1"
},
"devDependencies": {
"eslint": "8.49.0",
"eslint-config-next": "13.5.1",
"eslint-plugin-react": "^7.33.2"
}
}
18 changes: 18 additions & 0 deletions sample-application/baselayer/pages/api/execute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// pages/api/execute.js

import { POST } from '../../src/app/execute/route';

export default async (req, res) => {
if (req.method === 'POST') {
// Check if request method is POST
try {
const data = await POST(req);
res.status(200).json(data.data);
} catch (error) {
res.status(500).json({ error: error.toString() }); // Make sure your error is serializable or use error.message
}
} else {
// Handle any requests that aren't POST
res.status(405).json({ error: 'Method not allowed' });
}
}
117 changes: 117 additions & 0 deletions sample-application/baselayer/src/app/data-intake/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Copyright 2023 Visier Solutions 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.

"use client"
import React, { useState } from "react";
import Card from 'react-bootstrap/Card';
import Button from 'react-bootstrap/Button';
import Form from 'react-bootstrap/Form';
import useCredsStore from "@/store/credsStore";

export default function DataIntake() {
const [fileName, setFileName] = useState(null);
const [file, setFile] = useState(null);
const [transactionId, setTransactionId] = useState(null);
const [authHeader, config] = useCredsStore(s => [s.authHeader, s.config]);

const startTransaction = () => {
const requestBody = {
auth: authHeader(),
headers: {
Cookie: `VisierASIDToken=${authHeader()}`,
},
config,
url: encodeURI("/v1/data/directloads/prod/transactions"),
method: "POST"
};

fetch("/api/execute", {
method: "POST",
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody)
}).then(response => response.json())
.then(data => setTransactionId(data.transactionId));
};

const handleChange = (e) => {
setFile(e.target.files[0]);
setFileName(e.target.files[0].name);
};

const handleUpload = () => {
if (!file || !transactionId) {
alert("You must choose a file and specify a transaction ID.");
return;
}
const formData = new FormData();
formData.append('files', file);
formData.append('transactionId', transactionId);

const requestBody = {
auth: authHeader(),
config,
url: encodeURI(`/v1/data/directloads/prod/transactions/${transactionId}/Expense_Report`),
method: "PUT",
data: formData,
};

fetch("/api/execute", {
method: "POST",
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody),
}).then(response => {
if (response.ok) {
setUploadStatus("SUCCEEDED");
alert("File upload succeeded");
} else {
setUploadStatus("FAILED");
alert("File upload failed");
}
});
};

return (
<Card style={{ width: '100%' }} className="m-3">
<Card.Body>
<Card.Title>Data Upload (Direct Data Intake API)</Card.Title>
<Card.Text>
Load data directly into Visier objects. These objects can be delivered as part of Visier Blueprint, locally modified objects, or even completely custom objects.
In this application, please upload expense report data.
</Card.Text>
<Form>
<Form.Group controlId="apiTransaction">
<Form.Label>Transaction ID</Form.Label>
<Form.Control type="text" placeholder="Transaction ID"
value={transactionId}
onChange={e => setTransactionId(e.target.value)} />
<Button variant="primary" onClick={startTransaction}>Start Transaction with API</Button>
</Form.Group>
<Form.Group>
<Form.Control type="file"
onChange={(event) => {
setFile(event.target.files[0]);
setFileName(event.target.files[0]?.name);
}} />
<Button variant="primary" onClick={handleUpload}>Upload Data</Button>
{fileName && (
<Form.Text className="text-muted">
{fileName} selected
</Form.Text>
)}
</Form.Group>
</Form>
</Card.Body>
</Card>
);
}
104 changes: 104 additions & 0 deletions sample-application/baselayer/src/app/data-model/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright 2023 Visier Solutions 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.

"use client"
import React, { useState } from "react";
import Card from 'react-bootstrap/Card';
import Button from 'react-bootstrap/Button';
import Table from 'react-bootstrap/Table';

import Form from 'react-bootstrap/Form';
import useCredsStore from "@/store/credsStore";

export default function DataModel() {
const [authHeader, config] = useCredsStore(s => [s.authHeader, s.config]);
const [metricsData, setMetricsData] = useState(null);

const handleCall = async () => {
const requestBody = {
auth: authHeader(),
config,
url: encodeURI("/v1/data/model/analytic-objects/Expense_Report"),
method: "GET"
};

fetch("/api/execute", {
method: "POST",
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody)
}).then(response => response.json())
.then(data => {
setMetricsData(data);
}).catch(e => {
console.error('Fetch failed', e);
});
};


return (
<Card style={{ width: '100%' }} className="m-3">
<Card.Body>
<Card.Title>Expense Report Dimensions (Data Model API)</Card.Title>
<Card.Text>
The response returns a list of all expense report dimensions.
</Card.Text>
<Form>
<Form.Group controlId="apiTransaction">
<Button variant="primary" onClick={handleCall}>Call Data Model API</Button>
</Form.Group>
<Table striped bordered hover>
<thead>
<tr>
<th colspan="2">Expense Report Data Model</th>
</tr>
</thead>
<tbody>
{
metricsData && (
<tr>
<td>{metricsData.id}</td>
<td>
<div>
<h6>{metricsData.displayName}</h6>
<p>{metricsData.description}</p>
<p>Data Start Date: {new Date(parseInt(metricsData.dataStartDate)).toLocaleString()}</p>
<p>Data End Date: {new Date(parseInt(metricsData.dataEndDate)).toLocaleString()}</p>
</div>
</td>
</tr>
)
}
</tbody>
</Table>
<Table striped bordered hover >
<thead>
<tr>
<th>Property IDs</th>
</tr>
</thead>
<tbody>
{
metricsData?.propertyIds && metricsData?.propertyIds.map((property, index) => (
<tr key={'property-' + index}>
<td>{property}</td>
</tr>
))
}
</tbody>
</Table>
</Form>
</Card.Body>
</Card>
);
}
Loading