main
Ayush Mukherjee 3 years ago
commit acb3b0b80f

@ -0,0 +1,3 @@
VITE_ASANA_API_URI=
VITE_ASANA_PROJECT_ID=
VITE_ACCESS_TOKEN=

5
.gitignore vendored

@ -0,0 +1,5 @@
node_modules
.DS_Store
dist
dist-ssr
*.local

@ -0,0 +1,34 @@
# One.com Asana Incidents Viz
This is a single page application written using React and Tailwind css, and uses
(Vite JS)[https://vitejs.dev] as the bundler/dev server.
This project heavily relies on the Asana v1.0 API and thus any changes to the API
will result in a non-functioning viz tool.
## Development setup
1. Clone this repo
2. Install node dependencies: `npm install`
3. Copy the `.env.example` file as `.env.local` and fill in the required values.
4. Run the local dev server: `npm run dev`
```
Note: Asana API URI can be set as https://app.asana.com/api/1.0
```
## Deployment
1. Clone this repo
2. Install node dependencies: `npm install`
3. Either follow step 3 from the development setup OR provide the same variables to the build context
4. Run the build command: `npm run build`
5. Deploy the `dist` directory to any web server of your choice
```
Note: Make sure you don't copy the node_modules folder if you are making a zip.
```
## License
This project is licensed under MIT.

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Incidents and Maintenance Viz | one.com</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

3378
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,27 @@
{
"name": "one-com-viz",
"version": "1.0.0",
"author": "Ayush Mukherjee <me@ayushm.com>",
"license": "MIT",
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview"
},
"dependencies": {
"axios": "^0.21.1",
"chart.js": "^3.2.1",
"lodash": "^4.17.21",
"react": "^17.0.0",
"react-chartjs-2": "^3.0.3",
"react-dom": "^17.0.0"
},
"devDependencies": {
"@tailwindcss/forms": "^0.3.2",
"@vitejs/plugin-react-refresh": "^1.3.1",
"autoprefixer": "^10.2.5",
"postcss": "^8.2.15",
"tailwindcss": "^2.1.4",
"vite": "^2.3.3"
}
}

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

@ -0,0 +1,70 @@
import React, { useState, useEffect } from 'react'
import _ from 'lodash'
import { getFields, getTasks } from './services/AsanaService'
import IncidentsChart from './components/IncidentsChart'
import ProductsChart from './components/ProductsChart'
import ServicesChart from './components/ServicesChart'
import ServiceInfo from './components/ServiceInfo'
const App = () => {
const [fields, setFields] = useState([])
const [tasks, setTasks] = useState([])
const [loading, setLoading] = useState(true)
const [statusMsg, setStatusMsg] = useState('Loading...')
useEffect(async () => {
// get information from api
setStatusMsg('Getting fields information...')
await getFields(setFields)
setStatusMsg('Fetching records...')
await getTasks(setTasks)
// set loading to false
setLoading(false)
}, [])
return <div className="container mx-auto pt-4 px-4 min-h-screen overflow-x-hidden">
{
loading === true ? statusMsg : <>
<div className="flex flex-col">
<div className="my-2">
<h1 className="font-bold text-2xl text-center">Incidents Graph</h1>
</div>
<div>
<IncidentsChart atasks={tasks} />
</div>
</div>
<div className="mt-4 flex flex-col">
<div className="my-2">
<h1 className="font-bold text-2xl text-center">Impacted Products Graph</h1>
</div>
<div>
<ProductsChart fields={fields} tasks={tasks} />
</div>
</div>
<div className="mt-4 flex flex-col">
<div className="my-2">
<h1 className="font-bold text-2xl text-center">Service Impact Graphs</h1>
</div>
<div className="flex flex-wrap md:flex-nowrap">
<ServicesChart fields={fields[0]} idx={0} tasks={tasks} />
<ServicesChart fields={fields[1]} idx={1} tasks={tasks} />
</div>
</div>
<div className="mt-4 flex flex-col my-4">
<div className="mb-2">
<p className="text-xl">Service Impact Information</p>
</div>
<div className="flex flex-col">
<ServiceInfo fields={fields} tasks={tasks} />
</div>
</div>
</>
}
</div>
}
export default App

@ -0,0 +1,58 @@
import React, { memo, useEffect } from 'react'
import array from 'lodash/array'
import { Line } from 'react-chartjs-2'
import { getRandomColor } from '../utils'
const IncidentsChart = ({ atasks }) => {
let tasks = [...atasks]
array.reverse(tasks)
const monthNames = ["January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
];
const dates = []
const counts = []
const bgColors = []
useEffect(() => {
tasks.forEach((t) => {
const d = new Date(t.created_at)
const dstr = monthNames[d.getMonth()] + ' ' + d.getFullYear()
if (dates.indexOf(dstr) === -1) {
dates.push(dstr)
counts.push(1)
bgColors.push(getRandomColor())
} else {
counts[dates.indexOf(dstr)] += 1
}
})
}, [])
return <div className="my-2 h-64 w-full">
<Line
data={{
labels: dates,
datasets: [{
data: counts,
borderColor: 'rgba(29, 78, 216, 1)'
}],
}}
options={{
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
},
},
scales: {
yAxis: {
min: 0
},
},
}}
/>
</div>
}
export default memo(IncidentsChart)

@ -0,0 +1,46 @@
import React, { memo } from 'react'
import { Pie } from 'react-chartjs-2'
import { getRandomColor } from '../utils'
const ProductsChart = ({ fields, tasks }) => {
const products = fields[2].options
const counts = []
const bgColors = []
products.forEach(() => {
counts.push(0)
bgColors.push(getRandomColor())
})
tasks.forEach((t) => {
if (t.custom_fields[2].enum_value) {
counts[products.indexOf(t.custom_fields[2].enum_value.name)] += 1
} else if (t.custom_fields[3].enum_value) {
counts[products.indexOf(t.custom_fields[3].enum_value.name)] += 1
}
})
return <div className="my-2 h-64 w-full">
<Pie
data={{
labels: products,
datasets: [{
data: counts,
backgroundColor: bgColors,
}],
}}
options={{
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
},
},
}}
/>
</div>
}
export default memo(ProductsChart)

@ -0,0 +1,79 @@
import React, { memo, useState, useEffect } from 'react'
import array from 'lodash/array'
import ServiceProductChart from './ServiceProductChart'
const ServiceInfo = ({ fields, tasks }) => {
const [services, setServices] = useState([])
const [selection, setSelection] = useState(null)
const [fTasks, setTasks] = useState([])
useEffect(() => {
setServices(array.union(fields[0].options, fields[1].options))
}, [])
const filterTasksByService = (service) => {
setTasks(tasks.filter((t) =>
t.custom_fields[0].enum_value !== null && t.custom_fields[0].enum_value.name === service
|| t.custom_fields[1].enum_value !== null && t.custom_fields[1].enum_value.name === service
))
}
const setSelect = (e) => {
setSelection(e.currentTarget.value)
filterTasksByService(e.currentTarget.value)
}
// Asssuming Asana structure doesn't change
const getProductsAndDetails = (t) => {
const prod1 = t.custom_fields[2].enum_value?.name
const prod2 = t.custom_fields[3].enum_value?.name
if (prod1 && prod2) {
return prod1 + ', ' + prod2
} else if (prod1) {
return prod1
} else if (prod2) {
return prod2
}
return 'None'
}
return <>
<div className="flex flex-col w-96">
<label htmlFor="serviceSelect">Select Service:</label>
<select onChange={setSelect} defaultValue="0">
<option value="0" disabled>Select an option...</option>
{ services.map((s, idx) => (
<option key={idx} value={s}> {s} </option>
)) }
</select>
</div>
{ selection !== null ? <>
<div className="mt-2 px-2 py-4 flex flex-col md:flex-row justify-center">
{ fTasks.length !== 0 ?
<ol className="list-decimal list-inside">
{ fTasks.map((t, idx) => (
<li className="px-2 my-4" key={idx}>
{t.name}
<ul className="list-disc list-inside px-2 py-2">
<li><strong className="font-bold">Reason: </strong> {t.custom_fields[4].text_value || 'None'} </li>
<li><strong className="font-bold">Impacted Products: </strong> {
getProductsAndDetails(t)
} </li>
</ul>
</li>
)) }
</ol>
: 'None' }
<div className="w-full h-64 md:w-1/2">
<ServiceProductChart atasks={fTasks} />
</div>
</div>
</> : '' }
</>
}
export default memo(ServiceInfo)

@ -0,0 +1,69 @@
import React, { memo } from 'react'
import array from 'lodash/array'
import { Line } from 'react-chartjs-2'
import { getRandomColor } from '../utils'
const ServiceProductChart = ({atasks}) => {
let tasks = [...atasks]
const monthNames = ["January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
];
const labels = []
const prods = []
const data = []
const datasets = []
array.reverse(tasks)
tasks.forEach((t) => {
const d = new Date(t.created_at)
const dstr = d.getDate() + ' ' + monthNames[d.getMonth()] + ' ' + d.getFullYear()
labels.push(dstr)
if (t.custom_fields[2].display_value !== null && prods.indexOf(t.custom_fields[2].display_value) === -1) {
prods.push(t.custom_fields[2].display_value)
} else if (t.custom_fields[3].display_value !== null && prods.indexOf(t.custom_fields[3].display_value) === -1) {
prods.push(t.custom_fields[3].display_value)
}
})
prods.forEach(() => data.push([]))
if (prods.length > 0) {
tasks.forEach((t) => {
prods.forEach((p, idx) => {
if (t.custom_fields[2].display_value === p || t.custom_fields[3].display_value === p) {
data[idx].push(1)
} else {
data[idx].push(0)
}
})
})
}
prods.forEach((p, idx) => {
datasets.push({
label: p,
data: data[idx],
borderColor: getRandomColor(),
})
})
return <Line
data={{
labels,
datasets,
}}
options={{
maintainAspectRatio: false,
scales: {
yAxis: {
min: 0
},
},
}}
/>
}
export default ServiceProductChart

@ -0,0 +1,50 @@
import React, { memo } from 'react'
import { Bar } from 'react-chartjs-2'
import { getRandomColor } from '../utils'
const Chart = ({ fields, idx, tasks }) => {
const labels = fields.options
const data = []
const bgColors = []
labels.forEach((l) => {
let count = 0
tasks.forEach((t) => {
if (t.custom_fields[idx].enum_value != null && t.custom_fields[idx].enum_value.name === l) {
count += 1
}
})
data.push(count)
bgColors.push(getRandomColor())
})
return <div className="my-2 h-96 w-full md:w-1/2">
<Bar
data={{
labels,
datasets: [{
data,
backgroundColor: bgColors,
}],
}}
options={{
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
},
title: {
display: true,
text: fields.name,
font: {
size: '20'
},
},
}
}}
/>
</div>
}
export default memo(Chart)

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

@ -0,0 +1,11 @@
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
)

@ -0,0 +1,43 @@
import axios from 'axios'
export const getFields = async (setFields) => {
const fArr = [];
const res = await axios.get(`${import.meta.env.VITE_ASANA_API_URI}/projects/${import.meta.env.VITE_ASANA_PROJECT_ID}`, {
headers: {
Authorization: `Bearer ${import.meta.env.VITE_ACCESS_TOKEN}`
}
});
res.data.data.custom_field_settings.forEach((a) => {
const f = {
id: a.gid,
name: a.custom_field.name,
type: a.custom_field.type,
};
const opts = [];
if (a.custom_field.type == 'enum') {
a.custom_field.enum_options.forEach((o) => {
opts.push(o.name);
});
f.options = opts;
}
fArr.push(f);
});
setFields(fArr);
}
export const getTasks = async (setTasks) => {
const res2 = await axios.get(`${import.meta.env.VITE_ASANA_API_URI}/tasks`, {
params: {
section: '1199915951089746',
opt_fields: 'name,created_at,custom_fields',
},
headers: {
Authorization: `Bearer ${import.meta.env.VITE_ACCESS_TOKEN}`
}
});
const data = res2.data.data;
data.sort((a, b) => {
return new Date(b.created_at) - new Date(a.created_at);
});
setTasks(data);
}

@ -0,0 +1,8 @@
export const getRandomColor = () => {
let letters = '0123456789ABCDEF'.split('');
let color = '#';
for (let i = 0; i < 6; i++ ) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}

@ -0,0 +1,14 @@
module.exports = {
purge: ['./index.html', './src/**/*.{js,jsx}'],
mode: 'jit',
darkMode: false,
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [
require('@tailwindcss/forms'),
],
}

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import reactRefresh from '@vitejs/plugin-react-refresh'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [reactRefresh()]
})
Loading…
Cancel
Save