Skip to content

Instantly share code, notes, and snippets.

@dylangolow
Last active February 9, 2021 17:45
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dylangolow/6bd010df8234a6cc6ff3a8e33491b967 to your computer and use it in GitHub Desktop.
Save dylangolow/6bd010df8234a6cc6ff3a8e33491b967 to your computer and use it in GitHub Desktop.
// dashboard_charts/index.js
import React, {useEffect, useState} from 'react';
import SingleChart from '../single_chart'
import {Box, Button, useGlobalConfig} from '@airtable/blocks/ui';
import {GlobalConfigKeys} from "../../index";
function getCharts({table, records}) {
const globalConfig = useGlobalConfig();
useEffect(() => {
(async () => {
let chartSaved = globalConfig.get([GlobalConfigKeys.X_CHARTS]);
if (chartSaved) {
let keys = Object.keys(chartSaved);
if (keys.length > 0) {
setCharts([...keys.map(each => {
return {id: each}
})])
}
}
})();
}, []);
const [charts, setCharts] = useState([]);
const handleAddChart = () => {
setCharts([...charts, {id: 'chart-' + new Date().getTime()}]);
}
const handleDeleteTable = (id) => {
charts.splice(charts.findIndex(each => each.id === id), 1);
setCharts([...charts]);
globalConfig.setPathsAsync([{path: [GlobalConfigKeys.X_CHARTS, id], value: undefined}]);
}
return (
<><Box margin={0} display="flex" flexWrap="wrap">
{charts && charts.length > 0 ?
charts.map((chart, index) => <SingleChart key={chart.id} id={chart.id} table={table} records={records} deleteTable={handleDeleteTable} index={index+1} />)
: null}
</Box>
<Button style={{margin: 20}} onClick={() => handleAddChart()}>Add Chart</Button>
</>
);
}
export default getCharts;
// frontend/index.js
loadCSSFromURLAsync('https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css');
// ...
// typeahead implementation
import {Highlighter, Menu, MenuItem, Typeahead} from 'react-bootstrap-typeahead';
import {getOptionLabel, getOptionProperty} from "react-bootstrap-typeahead/lib/utils";
function Settings({table, xFieldValues, setFieldValue}) {
const ref = React.createRef();
const changeBackground = (e) => {
e.target.style.background = '#F0F0F0';
}
const revertBackground = (e) => {
e.target.style.background = 'white'
}
const TypeaheadMenu = (props) => {
const {
labelKey,
newSelectionPrefix,
options,
paginationText,
renderMenuItemChildren,
text,
...menuProps
} = props;
const renderMenuItem = (option, position) => {
if (option === undefined) return;
const label = getOptionLabel(option, labelKey);
const menuItemProps = {
disabled: getOptionProperty(option, 'disabled'),
label,
option,
position,
};
if (option && option.paginationOption) {
return (
<Fragment key="pagination-item">
<div style={{width: "100%", marginTop: 8, display: "flex", flexDirection: "column"}}
onMouseEnter={changeBackground} onMouseLeave={revertBackground}>
<MenuItem
{...menuItemProps}
className="rbt-menu-pagination-option"
style={{
color: '#2D2D2D',
textTransform: 'none',
width: "100%",
textAlign: 'center',
borderTopColor: 'lightGrey',
borderTopWidth: 1,
borderTopStyle: 'solid',
paddingTop: 4,
paddingBottom: 4
}}
label={paginationText}>
{paginationText}
</MenuItem>
</div>
</Fragment>
);
}
return (
<MenuItem {...menuItemProps} key={position} onMouseEnter={changeBackground}
onMouseLeave={revertBackground}>
{renderMenuItemChildren(option, props, position)}
</MenuItem>
);
};
return (
<Menu {...menuProps} text={text}>
{options.map(renderMenuItem)}
</Menu>
);
};
loadCSSFromString('a, a:hover, a:focus {text-decoration: none; color: #2D2D2D;}');
loadCSSFromString('mark { padding: 0; font-weight: bold; background: transparent; }')
return (
<Box display="flex" padding={3} borderBottom="thick" maxWidth={viewport.width}>
{table && xFieldValues && xFieldValues.length > 0 && (<>
<FormField label="Valor do filtro" paddingRight={1} marginBottom={0}>
<Typeahead
ref={ref}
options={xFieldValues}
onChange={(newValue) => {
if (newValue && newValue.length > 0) {
globalConfig.setAsync(GlobalConfigKeys.X_PATIENT_EMAIL, newValue[0].email);
globalConfig.setAsync(GlobalConfigKeys.X_TYPEAHEAD_VALUE, newValue[0].label);
} else {
globalConfig.setAsync(GlobalConfigKeys.X_PATIENT_EMAIL, undefined);
globalConfig.setAsync(GlobalConfigKeys.X_TYPEAHEAD_VALUE, undefined);
}
globalConfig.setAsync(GlobalConfigKeys.X_SELECTED_VALUE, JSON.stringify(newValue));
setFieldValue(newValue)
}}
id="react-bootstrap-typeahead"
open={undefined}
selected={xFieldValues.find(x => x.email === globalConfig.get(GlobalConfigKeys.X_PATIENT_EMAIL)) ?
[xFieldValues.find(x => x.email === globalConfig.get(GlobalConfigKeys.X_PATIENT_EMAIL))] : null}
renderInput={({inputRef, referenceElementRef, ...inputProps}) => (
<InputSynced
globalConfigKey={GlobalConfigKeys.X_TYPEAHEAD_VALUE}
{...inputProps}
ref={(input) => {
inputRef(input);
referenceElementRef(input);
}}
/>
)}
maxResults={20}
placeholder={"Encontre um paciente..."}
paginationText={"Exibir resultados adicionais..."}
renderMenu={(results, menuProps) => {
if (!results.length) {
return null;
}
return <div style={{padding: 0}}>
<TypeaheadMenu
options={results}
labelKey="label"
paginate
filterBy={['email', 'name']}
flip
text={globalConfig.get(GlobalConfigKeys.X_TYPEAHEAD_VALUE)}
{...menuProps}
renderMenuItemChildren={(option, props) => (
<Fragment>
<div style={{padding: 12, backgroundColor: "white"}}>
<Highlighter search={props.text} style={{fontWeight: 'bold'}}>
{option[props.labelKey]}
</Highlighter>
</div>
</Fragment>
)}>
</TypeaheadMenu>
</div>
}}
/>
</FormField>
<Button alignSelf="flex-end"
justifySelf="flex-end"
marginBottom={0}
onClick={() => {
globalConfig.setAsync(GlobalConfigKeys.X_TYPEAHEAD_VALUE, undefined);
globalConfig.setAsync(GlobalConfigKeys.X_PATIENT_EMAIL, undefined);
globalConfig.setAsync(GlobalConfigKeys.X_SELECTED_VALUE, undefined);
setFieldValue(null);
ref.current.clear();
}}
>Limpar</Button>
</>
)}
</Box>
);
}
{
"xCharts": {
"chart-1605425876029": "{\"fields\":[{\"field\":\"fldxLfpjdmYeDOhXT\",\"chartOption\":\"line\",\"color\":\"blueBright\",\"hex\":\"#2d7ff9\"},{\"field\":\"fldqwG8iFazZD5CLH\",\"chartOption\":\"line\",\"color\":\"blueLight1\",\"hex\":\"#9cc7ff\"}],\"chartTitle\":\"Gráfico criado em 11/15/2020, 2:37:56 AM\"}",
"chart-1605425876288": "{\"fields\":[{\"field\":\"fldGJZIdRlq3V3cKu\",\"chartOption\":\"line\",\"color\":\"blue\",\"hex\":\"#1283da\"}],\"chartTitle\":\"Gráfico criado em 11/15/2020, 2:37:56 AM\"}",
"chart-1605425876615": "{\"fields\":[{\"field\":\"fld1AnNcfvXm8DiNs\",\"chartOption\":\"line\",\"color\":\"blueLight1\",\"hex\":\"#9cc7ff\"},{\"field\":\"fldryX5N6vUYWbdzy\",\"chartOption\":\"line\",\"color\":\"blueDark1\",\"hex\":\"#2750ae\"}],\"chartTitle\":\"Gráfico criado em 11/15/2020, 2:37:56 AM\"}",
"chart-1605425994036": "{\"fields\":[{\"field\":\"fld9ak8Ja6DPweMdJ\",\"chartOption\":\"line\",\"color\":\"blueLight2\",\"hex\":\"#cfdfff\"},{\"field\":\"fldxVgXdZSECMVEj6\",\"chartOption\":\"line\",\"color\":\"blue\",\"hex\":\"#1283da\"}],\"chartTitle\":\"Gráfico criado em 11/15/2020, 2:39:54 AM\"}",
"chart-1605430015978": "{\"fields\":[{\"field\":\"fldwdMJkmEGFFSqMy\",\"chartOption\":\"line\",\"color\":\"blue\",\"hex\":\"#1283da\"},{\"field\":\"fldqwG8iFazZD5CLH\",\"chartOption\":\"line\",\"color\":\"blueLight1\",\"hex\":\"#9cc7ff\"}],\"chartTitle\":\"New Chart\"}",
"chart-1605430916029": "{\"fields\":[{\"field\":\"fldCuf3I2V027YAWL\",\"chartOption\":\"line\",\"color\":\"blueLight1\",\"hex\":\"#9cc7ff\"},{\"field\":\"fldBJjtRkWUTuUf60\",\"chartOption\":\"line\",\"color\":\"blueDark1\",\"hex\":\"#2750ae\"}],\"chartTitle\":\"Gráfico criado em 11/15/2020, 4:01:56 AM\"}",
"chart-1605431704374": "{\"fields\":[{\"field\":\"fld7oBtl3iiHNHqoJ\",\"chartOption\":\"line\",\"color\":\"blue\",\"hex\":\"#1283da\"}],\"chartTitle\":\"Gráfico criado em 11/15/2020, 4:15:04 AM\"}"
},
"xPatientEmail": "elle@gmail.com",
"xTypeaheadValue": "Elle Gold (elle@gmail.com)",
"xSelectedValue": "[{\"label\":\"Elle Gold (elle@gmail.com)\",\"id\":\"elle@gmail.com\",\"name\":\"Elle Gold\",\"email\":\"elle@gmail.com\"}]"
}
// single_chart/index.js
import {Box, Button, ColorPalette, colors, colorUtils, FormField, Heading, Icon, Input, Select, useGlobalConfig} from '@airtable/blocks/ui';
import React, {useEffect, useState} from 'react';
import {Bar} from 'react-chartjs-2';
import {GlobalConfigKeys} from "../../index";
import {setLabel} from "../../utils";
function SingleChart({table, deleteTable, id, records}) {
const globalConfig = useGlobalConfig();
const initialFieldObj = {field: null, chartOption: null, color: null};
const [fieldSelectOptions, setFieldSelectOptions] = useState([]);
const [fields, setFields] = useState([{...initialFieldObj}]);
const [dateField, setDateField] = useState(null);
const [chartTitle, setChartTitle] = useState(`Gráfico criado em ${new Date().toLocaleString()}`);
const [editTitle, setEditTitle] = useState(false);
const {data, options} = records && dateField && fields ? new_getChartData({records, fields, dateField, table}) : {};
// set chart on load
useEffect(() => {
(async () => {
let chartSaved = globalConfig.get([GlobalConfigKeys.X_CHARTS, id]);
if (chartSaved) {
let chartData = JSON.parse(chartSaved);
if (chartData) {
setFields(chartData.fields);
setChartTitle(chartData.chartTitle);
}
}
})();
}, []);
// save to globalConfig for chart at id on changes
useEffect(() => {
(async () => {
let chartJson = JSON.stringify({fields, chartTitle});
let path = [GlobalConfigKeys.X_CHARTS, id];
await globalConfig.setPathsAsync([{path, value: chartJson}])
})();
}, [chartTitle, fields]);
useEffect(() => {
(async () => {
const dateFieldTemp = table &&
table.fields.filter(field => field.description?.includes('#DATE#')) ?
table.fields.filter(field => field.description?.includes('#DATE#'))[0]
: null;
setDateField(dateFieldTemp);
if (table) {
const tempFieldOptions = table.fields.filter(field => field.description?.includes('#CHART#')).map(field => {
return {
...setLabel(field),
value: field.id
}
});
setFieldSelectOptions([...tempFieldOptions]);
}
})();
}, [table, records, fields]);
const handleSetFields = (field, index) => {
fields[index] = field;
setFields([...fields]);
}
const deleteField = (index) => {
if (fields.length === 1) {
setFields([{...initialFieldObj}]);
return;
}
fields.splice(index, 1);
setFields([...fields]);
}
const addField = () => {
setFields([...fields, {...initialFieldObj}]);
}
const atLeastOneMetric = () => {
let flag = false;
for (const f of fields) {
if (f.field && f.color && f.chartOption) flag = true;
}
return flag;
}
return (
<Box
position="relative"
top={0}
left={0}
right={0}
bottom={0}
display="flex"
flexDirection="column"
minWidth={600}
maxWidth={1000}
border="default"
borderRadius="large"
margin={2}
padding={1}
>
<Settings table={table} fieldOptions={fieldSelectOptions} filters={fields} setFields={handleSetFields}
deleteTable={deleteTable}
deleteField={deleteField} id={id} addField={addField}
chartTitle={chartTitle} setChartTitle={setChartTitle}
editTitle={editTitle} setEditTitle={setEditTitle}
/>
{data && options && atLeastOneMetric() && (
<Box position="relative" flex="auto" padding={3}>
<Bar data={data} options={options}/>
</Box>
)}
</Box>
);
}
function new_getChartData({records, fields, dateField, table}) {
const datasets = [];
for (const [index, f] of fields.entries()) {
if (!f.field || !f.color || !f.chartOption) {
continue;
}
const fieldObj = table.fields.find(field => field.id === f.field);
const fieldWithLabel = setLabel(fieldObj);
datasets[index] = {};
datasets[index].labels = [];
datasets[index].data = [];
datasets[index].label = fieldWithLabel.label || table.getFieldByIdIfExists(f.field).name;
datasets[index].backgroundColor = f.hex;
datasets[index].borderColor = f.hex;
datasets[index].strokeColor = f.hex;
datasets[index].fillColor = f.hex;
datasets[index].type = f.chartOption;
datasets[index].yAxisID = f.field;
datasets[index].showLine = true;
datasets[index].spanGaps = true;
datasets[index].fill = true;
datasets[index].rating = fieldObj.type === 'rating';
for (const record of records) {
const yValue = record.getCellValue(f.field);
const date = dateField ? record.getCellValue(dateField.id) : null;
if (yValue) {
datasets[index].data.push({y: yValue, x: date});
}
}
}
const options = {
maintainAspectRatio: true,
scales: {
yAxes: [...datasets.map((d, index) => {
return {
type: 'linear',
display: true,
position: index === 0 ? 'left' : 'right',
label: d.label,
id: d.yAxisID,
ticks: {
beginAtZero: d.rating
}
}
})
],
xAxes: [
{
type: 'time',
scaleLabel: {
display: true,
labelString: 'Data'
},
time: {
displayFormats: {
day: 'YYYY-MM-DD',
hour: 'MMM-DD HH:mm A',
minute: 'MMM-DD HH:mm A',
second: 'MMM-DD HH:mm A'
},
minUnit: 'second'
}
}
],
},
legend: {
display: true,
},
plugins: {
filler: {
propagate: true
}
}
};
const data = {
datasets: [...datasets],
};
return {data, options};
}
function Settings({table, filters, setFields, deleteField, deleteTable, id, addField, fieldOptions, chartTitle, setChartTitle, editTitle, setEditTitle}) {
const chartOptions = [
{label: 'Linha', value: 'line'},
{label: 'Barra', value: 'bar'}
]
const allowedColors = [
colors.BLUE,
colors.BLUE_BRIGHT,
colors.BLUE_DARK_1,
colors.BLUE_LIGHT_1,
colors.BLUE_LIGHT_2
];
return (
<>
<Box display="flex" padding={3} borderBottom="thick" width="100%" justifyContent="space-between"
flexDirection={"column"}>
<Box display="flex" width="100%" marginBottom={3} justifyContent="space-between">
{editTitle ? <div style={{
display: "flex",
justifyContent: "flex-start",
maxWidth: 450,
width: "-webkit-fill-available"
}}>
<Input
value={chartTitle}
onChange={e => setChartTitle(e.target.value)}
/>
<Icon justifySelf={"flex-start"} alignSelf={"center"} marginLeft={1} paddingBottom={1}
onClick={() => setEditTitle(false)} name={"check"} size={20} fillColor={"green"}/></div>
: <div style={{display: "flex", justifyContent: "flex-start", maxWidth: 450}}><Heading
alignSelf={"flex-start"}
justifySelf={"flex-start"}
>
{chartTitle}
</Heading> <Icon justifySelf={"flex-start"} alignSelf={"center"} marginLeft={2} marginBottom={2}
onClick={() => setEditTitle(true)} name={"edit"} size={16} fillColor={"grey"}/>
</div>
}
<Button
onClick={() => deleteTable(id)}
alignSelf="flex-start"
justifySelf="flex-end">
Remover
</Button>
</Box>
{table && (
<div style={{display: "flex", flexDirection: "column"}}> {filters && filters.map((filter, index) =>
<div style={{display: "flex", flexDirection: "row"}} key={index}>
{index === 0 && filters.length === 1 &&
<Icon onClick={() => addField()} alignSelf="flex-end" justifySelf="flex-end" name={"plus"}
size={20} marginRight={1} style={{marginBottom: 6}} padding={0} fillColor={"grey"}/>
}
<div>
<FormField label={index === 0 ? "Eixo Y" : ""} minWidth={150} maxWidth={200}
paddingLeft={0} marginBottom={0}>
<Select value={filter.field} options={fieldOptions}
onChange={field => setFields({...filter, field}, index)}/>
</FormField></div>
<div><FormField label={index === 0 ? "Tipo de Gráfico" : ""} minWidth={150} maxWidth={200}
paddingLeft={1} marginBottom={0}>
<Select
options={chartOptions}
value={filter?.chartOption || null}
onChange={chartOption => setFields({...filter, chartOption}, index)}
/></FormField></div>
<div><FormField label={index === 0 ? "Cor" : ""} minWidth={150} maxWidth={200}
paddingLeft={4} paddingRight={4} marginBottom={0}>
<ColorPalette
style={{paddingTop: 6}}
alignSelf={"center"}
allowedColors={allowedColors}
color={filter?.color || null}
onChange={color => setFields({
...filter,
color,
hex: colorUtils.getHexForColor(color)
}, index)}
squareMargin={8}
width="150px"
/></FormField></div>
<Icon onClick={() => deleteField(index)} alignSelf="flex-end" justifySelf="flex-end"
name={filters.length === 1 ? "redo" : "x"} size={20} style={{marginBottom: 8}}
fillColor={"grey"}/>
</div>)}
</div>
)}
</Box>
</>
);
}
export default SingleChart;
// utils.js
export const setLabel = (field, labelTag = "#LABEL#") => {
const labelTags = (field.description?.match(new RegExp(labelTag, "g")) || []).length;
let label;
if (labelTags === 2) label = field.description?.split(`${labelTag}`)[1];
if (!label || label?.trim() === '') label = field.name;
return {...field, label, name: field.name, description: field.description};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment