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

Commit

Permalink
Adding feature to export data to excel
Browse files Browse the repository at this point in the history
  • Loading branch information
Helene Rigner committed Apr 15, 2019
1 parent e4226f5 commit 5faad3d
Show file tree
Hide file tree
Showing 11 changed files with 187 additions and 22 deletions.
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/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.
9 changes: 6 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 './useInfoBox';
import config from '../enigma/config';
import useLayout from './use/layout';
import TopBar from './topbar';
Expand Down Expand Up @@ -69,9 +70,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
21 changes: 20 additions & 1 deletion src/components/cube.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ 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';

Expand All @@ -24,6 +25,7 @@ const Cube = forwardRef(({ app, tableData: { initialColumns }, closeOnClickOutsi
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
Expand All @@ -34,6 +36,22 @@ const Cube = forwardRef(({ app, tableData: { initialColumns }, closeOnClickOutsi
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}`;
elem.target = '_blank';
document.body.appendChild(elem);
elem.click();
document.body.removeChild(elem);
}
}
},
}));

function closeAdd() {
Expand Down Expand Up @@ -111,6 +129,7 @@ const Cube = forwardRef(({ app, tableData: { initialColumns }, closeOnClickOutsi
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 @@ -123,7 +142,7 @@ const Cube = forwardRef(({ app, tableData: { initialColumns }, closeOnClickOutsi
<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} hypercubeProps={hypercubeProps} 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}
</div>
</div>
);
Expand Down
36 changes: 28 additions & 8 deletions src/components/cubes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +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 './useInfoBox';
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 @@ -16,10 +19,12 @@ export function Cubes({ app, closeOnClickOutside }) {
const [cubeList, setCubeList] = useState([]);
const forceUpdate = useForce();
const addButtonRef = useRef(null);
const cubeRef = 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 @@ -31,41 +36,56 @@ 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() {
if (cubeRef && cubeRef.current) {
cubeRef.current.copyToClipboard();
function copyToClipboard(id) {
if (refs.current && refs.current[id] && refs.current[id].current) {
refs.current[id].current.copyToClipboard();
}
}

const exportHypercube = async (id) => {
if (refs.current && refs.current[id] && refs.current[id].current) {
setSpinner(true);
try {
await refs.current[id].current.exportHypercube();
} catch (error) {
infoBox.show('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 className="copy" svg={copy} onClick={() => copyToClipboard(cube)} title="Copy hypercube def to clipboard" />
<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 ref={cubeRef} 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
14 changes: 13 additions & 1 deletion src/components/cubes.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,14 @@
margin-right: auto;
}

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

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

@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
12 changes: 4 additions & 8 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 @@ -131,11 +130,10 @@ function getTotalTableWidth(layout, dimensions, measures) {
}

export default function HypercubeTable({
app, measures, dimensions, height, maxWidth, onHeaderClick, hypercubeProps,
model, measures, dimensions, height, maxWidth, onHeaderClick,
}) {
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 @@ -193,15 +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,
hypercubeProps: PropTypes.object,
};
HypercubeTable.defaultProps = {
onHeaderClick: () => {},
measures: [],
dimensions: [],
app: null,
hypercubeProps: null,
model: null,
};
48 changes: 48 additions & 0 deletions src/components/info-box.pcss
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
.info-box {
align-items: center;
background-color: white;
border: 1px solid #ccc;
border-radius: 0.5rem;
box-shadow: 2px 2px 10px #ccc;
color: #404040;
display: flex;
font-size: 2em;
height: 7em;
justify-content: center;
left: 0;
margin-bottom: 2em;
margin-left: auto;
margin-right: auto;
position: absolute;
right: 0;
width: 30em;
z-index: 50;
}

.visible {
animation: box-in 0.7s;
bottom: 0;
transition: bottom 0.6s ease-in-out;
}

.hidden {
bottom: -15%;
transition: bottom 0.8s ease-in-out;
}

.alert {
fill: red;
padding-left: 1.5em;
padding-right: 1em;
width: 1.5em;
}

@keyframes box-in {
from {
transform: translateY(100%);
}

to {
transform: translateY(0);
}
}
1 change: 0 additions & 1 deletion src/components/scroll-area.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ export default function ScrollArea({
const pan = useRef({});
const [isScrolling, setIsScrolling] = useState(false);
const [scrollStyle, setScrollStyle] = useState({ style: { width, height, overflow: 'auto' } });

const onMouseUp = () => {
if (isScrolling) {
const cursorStyle = { ...scrollStyle.style, cursor: 'unset' };
Expand Down
65 changes: 65 additions & 0 deletions src/components/useInfoBox.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React, { useContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import SVGInline from 'react-svg-inline';

import alert from '../assets/alert-triangle-outline.svg';

import './info-box.pcss';

const context = React.createContext();

const InfoBoxContainer = props => (
<div {...props} />
);

const InfoBox = ({ children, visible }) => (
<div className={`info-box ${visible ? 'visible' : 'hidden'}`}>
<SVGInline className="alert" svg={alert} />
{children}
</div>
);

InfoBox.propTypes = {
children: PropTypes.arrayOf(PropTypes.node).isRequired,
visible: PropTypes.bool,
};

InfoBox.defaultProps = {
visible: true,
};

export function InfoBoxProvider({ children }) {
const [infoBox, setInfoBox] = useState(null);
const [showTimer, setShowTimer] = useState(0);
const [animateOutTimer, setAnimateOutTimer] = useState(0);

useEffect(() => { clearTimeout(showTimer); clearTimeout(animateOutTimer); }, []);

const show = (text) => {
const id = Math.random().toString(36).substr(2, 9);
setAnimateOutTimer(setTimeout(() => { setInfoBox({ content: text, id, visible: false }); }, 5000));
setShowTimer(setTimeout(() => { setInfoBox(null); }, 6000));
setInfoBox({ content: text, id, visible: true });
};

return (
<context.Provider value={{ show }}>
{children}
<InfoBoxContainer className="info-box-container">
{infoBox
? (
<InfoBox key={infoBox.id} visible={infoBox.visible}>
{infoBox.content}
</InfoBox>
) : null
}
</InfoBoxContainer>
</context.Provider>
);
}

InfoBoxProvider.propTypes = {
children: PropTypes.arrayOf(PropTypes.node).isRequired,
};

export const useInfoBox = () => useContext(context);

0 comments on commit 5faad3d

Please sign in to comment.