-
Notifications
You must be signed in to change notification settings - Fork 12
Adding possibility to export hypercube data and copy hypercube def to clipboard #232
Changes from 6 commits
e4226f5
5faad3d
0afb1cd
0a372c9
0e0e12b
7bbdc9c
502506e
4cee72c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,20 +1,58 @@ | ||
import React, { useRef, useState } from 'react'; | ||
import React, | ||
{ | ||
forwardRef, | ||
useImperativeHandle, | ||
useRef, | ||
useState, | ||
} from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import copy from 'copy-to-clipboard'; | ||
|
||
import useColumnOptions from './use/column-options'; | ||
import CubeColumnChooser from './cube-column-chooser'; | ||
import HypercubeTable from './hypercube-table'; | ||
import useForce from './use/force'; | ||
import useModel from './use/model'; | ||
|
||
import './cube.pcss'; | ||
|
||
export default function Cube({ app, tableData: { initialColumns }, closeOnClickOutside }) { | ||
// The component needs to be wrapped in `forwardRef` to give access to the | ||
// ref object assigned using the `ref` prop. | ||
const Cube = forwardRef(({ app, tableData: { initialColumns }, closeOnClickOutside }, ref) => { | ||
const selectableColumns = useColumnOptions(app); | ||
const [columns, setColumns] = useState(initialColumns); | ||
const currentHeader = useRef(null); | ||
const columnToReplace = useRef(null); | ||
const addOpen = useRef(false); | ||
const forceUpdate = useForce(); | ||
let model = null; | ||
let hypercubeProps = null; | ||
|
||
// Any instance of the component is extended with what is returned from the | ||
// callback passed as the second argument. | ||
useImperativeHandle(ref, () => ({ | ||
copyToClipboard() { | ||
if (hypercubeProps) { | ||
copy(JSON.stringify(hypercubeProps)); | ||
} | ||
}, | ||
async exportHypercube() { | ||
if (model) { | ||
const result = await model.exportData('OOXML', '/qHyperCubeDef'); | ||
const engineUrl = new URLSearchParams(document.location.search).get('engine_url'); | ||
if (engineUrl) { | ||
const wsUrl = new URL(engineUrl); | ||
const protocol = wsUrl.protocol === 'wss' ? 'https' : 'http'; | ||
const elem = document.createElement('a'); | ||
elem.href = `${protocol}://${wsUrl.host}${result.qUrl}`; | ||
hrigner marked this conversation as resolved.
Show resolved
Hide resolved
|
||
elem.target = '_blank'; | ||
document.body.appendChild(elem); | ||
elem.click(); | ||
document.body.removeChild(elem); | ||
} | ||
} | ||
}, | ||
})); | ||
|
||
function closeAdd() { | ||
if (currentHeader.current) { | ||
|
@@ -57,10 +95,41 @@ export default function Cube({ app, tableData: { initialColumns }, closeOnClickO | |
toggleAdd(event); | ||
} | ||
|
||
function createProperties(dimensions, measures) { | ||
const hypercubeDef = { | ||
qInfo: { | ||
qType: 'catwalk-hypercube', | ||
}, | ||
qHyperCubeDef: { | ||
qInitialDataFetch: [ | ||
{ | ||
qTop: 0, | ||
qLeft: 0, | ||
qHeight: 20, | ||
qWidth: dimensions.length + measures.length, | ||
}, | ||
], | ||
}, | ||
}; | ||
if (dimensions && dimensions.length > 0) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need for |
||
hypercubeDef.qHyperCubeDef.qDimensions = dimensions.map(dimension => dimension.hyperCubeContent); | ||
} else { | ||
hypercubeDef.qHyperCubeDef.qDimensions = []; | ||
} | ||
if (measures && measures.length > 0) { | ||
hypercubeDef.qHyperCubeDef.qMeasures = measures.map(measure => measure.hyperCubeContent); | ||
} else { | ||
hypercubeDef.qHyperCubeDef.qMeasures = []; | ||
} | ||
return hypercubeDef; | ||
} | ||
|
||
const popup = addOpen.current ? <CubeColumnChooser alignTo={currentHeader.current} selectableColumns={selectableColumns} chooseColumn={addColumn} closeOnClickOutside={closeOnClickOutside} /> : null; | ||
|
||
const measures = columns.filter(column => column.type === 'measure'); | ||
const dimensions = columns.filter(column => column.type === 'dimension' || column.type === 'field'); | ||
hypercubeProps = createProperties(dimensions, measures); | ||
model = useModel(app, hypercubeProps); | ||
|
||
const isEmpty = measures.length + dimensions.length === 0; | ||
if (isEmpty && addOpen.current) { | ||
|
@@ -73,11 +142,12 @@ export default function Cube({ app, tableData: { initialColumns }, closeOnClickO | |
<div role="button" title="Add another column" tabIndex="-1" className={`column-add-button ${isEmpty ? 'empty' : ''}`} onClick={e => toggleAdd(e)}> | ||
<span className="text">+</span> | ||
</div> | ||
{!isEmpty ? <HypercubeTable app={app} onHeaderClick={data => onHeaderClick(data)} dimensions={dimensions} measures={measures} height={28 * 8} maxWidth={100 * 8} /> : null} | ||
{!isEmpty ? <HypercubeTable model={model} onHeaderClick={data => onHeaderClick(data)} dimensions={dimensions} measures={measures} height={28 * 8} maxWidth={100 * 8} /> : null} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
</div> | ||
</div> | ||
); | ||
} | ||
}); | ||
export default Cube; | ||
|
||
Cube.defaultProps = { | ||
closeOnClickOutside: () => true, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,7 +2,11 @@ import React, { useRef, useState } from 'react'; | |
import PropTypes from 'prop-types'; | ||
import SVGInline from 'react-svg-inline'; | ||
import Cube from './cube'; | ||
import { useInfoBox } from './info-box'; | ||
import close from '../assets/close-outline.svg'; | ||
import copy from '../assets/copy-outline.svg'; | ||
import exportCube from '../assets/download-outline.svg'; | ||
import loader from '../assets/loader-outline.svg'; | ||
|
||
import useColumnOptions from './use/column-options'; | ||
import CubeColumnChooser from './cube-column-chooser'; | ||
|
@@ -15,9 +19,12 @@ export function Cubes({ app, closeOnClickOutside }) { | |
const [cubeList, setCubeList] = useState([]); | ||
const forceUpdate = useForce(); | ||
const addButtonRef = useRef(null); | ||
const refs = useRef([React.createRef()]); | ||
|
||
const selectableColumns = useColumnOptions(app); | ||
const addOpen = useRef(false); // Start with add dialog open | ||
const [spinner, setSpinner] = useState(false); | ||
const infoBox = useInfoBox(); | ||
|
||
const closeButton = { className: 'close', svg: close }; | ||
|
||
|
@@ -29,34 +36,58 @@ export function Cubes({ app, closeOnClickOutside }) { | |
newCube, | ||
...cubeList, | ||
]); | ||
refs.current[newCube.id] = React.createRef(); | ||
} | ||
addOpen.current = false; | ||
forceUpdate(); | ||
} | ||
|
||
function removeCube(id) { | ||
setCubeList(cubeList.filter(item => item.id !== id)); | ||
refs.current[id] = null; | ||
} | ||
|
||
function openColumnChooser() { | ||
addOpen.current = true; | ||
forceUpdate(); | ||
} | ||
|
||
function copyToClipboard(id) { | ||
if (refs.current && refs.current[id] && refs.current[id].current) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does our babel implementation allow optional chaining? https://github.com/tc39/proposal-optional-chaining E.g. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sadly not, seems to be in the proposal phase still.. |
||
refs.current[id].current.copyToClipboard(); | ||
infoBox.show('success', 'The hypercube def was copied to clipboard.'); | ||
} | ||
} | ||
|
||
const exportHypercube = async (id) => { | ||
if (refs.current && refs.current[id] && refs.current[id].current) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as above ⬆️ |
||
setSpinner(true); | ||
try { | ||
await refs.current[id].current.exportHypercube(); | ||
infoBox.show('success', 'The data was successfully exported.'); | ||
} catch (error) { | ||
infoBox.show('warning', 'The data could not be exported. Check that you have permissions to export data.'); | ||
} | ||
setSpinner(false); | ||
} | ||
}; | ||
|
||
const popup = addOpen.current ? <CubeColumnChooser arrowStyle="arrowRight" alignTo={addButtonRef.current} selectableColumns={selectableColumns} chooseColumn={column => addCube(column)} closeOnClickOutside={closeOnClickOutside} /> : null; | ||
const cubeDivs = cubeList.map(cube => ( | ||
<div key={cube.id} className="card"> | ||
<div className="top-bar"> | ||
<div className="title">HYPERCUBE</div> | ||
<SVGInline {...closeButton} onClick={() => removeCube(cube.id)} /> | ||
{ spinner ? <SVGInline className="spinner" svg={loader} /> : <SVGInline className="export" svg={exportCube} onClick={() => exportHypercube(cube.id)} title="Export to excel" /> } | ||
<SVGInline className="copy" svg={copy} onClick={() => copyToClipboard(cube.id)} title="Copy hypercube def to clipboard" /> | ||
<SVGInline {...closeButton} onClick={() => removeCube(cube.id)} title="Close cube" /> | ||
</div> | ||
<Cube app={app} tableData={cube} closeOnClickOutside={closeOnClickOutside} /> | ||
<Cube ref={refs.current[cube.id]} app={app} tableData={cube} closeOnClickOutside={closeOnClickOutside} /> | ||
</div> | ||
)); | ||
return ( | ||
<div> | ||
<div className="cubes"> | ||
<div className="add-button" ref={addButtonRef} role="button" tabIndex="-1" onClick={() => openColumnChooser()}> | ||
<div className="add-button" ref={addButtonRef} role="button" tabIndex="-1" onClick={openColumnChooser}> | ||
<span className="text" title="Create a new hypercube">+</span> | ||
</div> | ||
{cubeDivs} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps
if (engineUrl)
instead of the double condition?