-
-
Save dylangolow/6bd010df8234a6cc6ff3a8e33491b967 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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> | |
); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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\"}]" | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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