Skip to content
This repository has been archived by the owner on Nov 30, 2022. It is now read-only.

Adding possibility to export hypercube data and copy hypercube def to clipboard #232

Merged
merged 8 commits into from
Apr 16, 2019
Merged
Show file tree
Hide file tree
Changes from 6 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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"lint:fix": "eslint --ext .js,.jsx --fix . && stylelint --fix src/."
},
"dependencies": {
"copy-to-clipboard": "3.1.0",
"enigma.js": "2.4.0",
"prop-types": "15.7.2",
"react": "16.8.6",
Expand Down
1 change: 1 addition & 0 deletions src/assets/alert-triangle-outline.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/assets/checkmark-circle-2-outline.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/assets/copy-outline.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/assets/download-outline.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/assets/loader-outline.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 14 additions & 3 deletions src/components/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useMemo, useEffect, useRef } from 'react';
import usePromise from 'react-use-promise';
import enigma from 'enigma.js';

import { InfoBoxProvider } from './info-box';
import config from '../enigma/config';
import useLayout from './use/layout';
import TopBar from './topbar';
Expand Down Expand Up @@ -50,6 +51,14 @@ export default function App() {
);
}

const engineUrl = new URLSearchParams(document.location.search).get('engine_url');
if (!engineUrl && engineUrl !== '') {
Copy link
Contributor

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?

if (window.location) {
hrigner marked this conversation as resolved.
Show resolved Hide resolved
const URLobject = new URL(window.location.href);
window.location.assign(`${URLobject.protocol}//${window.location.host}?engine_url=${config.url}`);
}
}

if (!global || (!app && appState !== 'pending')) {
return (
<Splash
Expand All @@ -69,9 +78,11 @@ export default function App() {
<AppContext.Provider value={app}>
<div className="app">
{guide}
<TopBar app={app} appLayout={appLayout} startGuide={() => guideRef.current.startGuideFunc()} />
<Model app={app} appLayout={appLayout} />
{cubes}
<InfoBoxProvider>
<TopBar app={app} appLayout={appLayout} startGuide={() => guideRef.current.startGuideFunc()} />
<Model app={app} appLayout={appLayout} />
{cubes}
</InfoBoxProvider>
</div>
{reloadSplasher}
</AppContext.Provider>
Expand Down
78 changes: 74 additions & 4 deletions src/components/cube.jsx
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) {
Expand Down Expand Up @@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for > 0

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) {
Expand All @@ -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}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

</div>
</div>
);
}
});
export default Cube;

Cube.defaultProps = {
closeOnClickOutside: () => true,
Expand Down
37 changes: 34 additions & 3 deletions src/components/cubes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 };

Expand All @@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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. refs.current?[id]?.current

Copy link
Author

Choose a reason for hiding this comment

The 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) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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}
Expand Down
17 changes: 15 additions & 2 deletions src/components/cubes.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -62,17 +62,25 @@
align-items: center;
display: flex;
flex: 1 1 auto;
justify-content: space-between;
justify-content: flex-end;
margin-bottom: 0.5em;
width: 100%;

.title {
color: #3f9dd0;
font-size: 1.5em;
left: 1rem;
margin-right: auto;
}

.close {
.spinner {
animation: spin 2000ms infinite linear;
}

.close,
.copy,
.export,
.spinner {
cursor: pointer;
fill: #4d4d4d;
height: 2em;
Expand All @@ -94,3 +102,8 @@
}
}
}

@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
40 changes: 4 additions & 36 deletions src/components/hypercube-table.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { Column } from 'react-virtualized';
import VirtualTable from './virtual-table';

import './hypercube-table.pcss';
import useModel from './use/model';
import useLayout from './use/layout';

function cellGetterForIndex(index) {
Expand Down Expand Up @@ -130,42 +129,11 @@ function getTotalTableWidth(layout, dimensions, measures) {
return totalWidth;
}

function createProperties(dimensions, measures) {
const hypercubeProps = {
qInfo: {
qType: 'catwalk-hypercube',
},
qHyperCubeDef: {
qInitialDataFetch: [
{
qTop: 0,
qLeft: 0,
qHeight: 20,
qWidth: dimensions.length + measures.length,
},
],
},
};
if (dimensions && dimensions.length > 0) {
hypercubeProps.qHyperCubeDef.qDimensions = dimensions.map(dimension => dimension.hyperCubeContent);
} else {
hypercubeProps.qHyperCubeDef.qDimensions = [];
}
if (measures && measures.length > 0) {
hypercubeProps.qHyperCubeDef.qMeasures = measures.map(measure => measure.hyperCubeContent);
} else {
hypercubeProps.qHyperCubeDef.qMeasures = [];
}
return hypercubeProps;
}

export default function HypercubeTable({
app, measures, dimensions, height, maxWidth, onHeaderClick,
model, measures, dimensions, height, maxWidth, onHeaderClick,
}) {
const hypercubeProps = createProperties(dimensions, measures);
const model = useModel(app, hypercubeProps);
const layout = useLayout(model);
if (!model) {
if (!model || !layout) {
return null;
}
const calculatedWidth = getTotalTableWidth(layout, dimensions, measures);
Expand Down Expand Up @@ -223,13 +191,13 @@ HypercubeTable.propTypes = {
onHeaderClick: PropTypes.func,
measures: PropTypes.arrayOf(PropTypes.object),
dimensions: PropTypes.arrayOf(PropTypes.object),
app: PropTypes.object,
model: PropTypes.object,
maxWidth: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
};
HypercubeTable.defaultProps = {
onHeaderClick: () => {},
measures: [],
dimensions: [],
app: null,
model: null,
};
Loading