mirror of https://github.com/grafana/grafana
parent
6af67197a7
commit
9fbb0b6636
@ -1,192 +0,0 @@ |
||||
import { utils } from 'xlsx'; |
||||
|
||||
import { DataFrame } from '@grafana/data'; |
||||
|
||||
import { workSheetToFrame } from './sheet'; |
||||
|
||||
describe('sheets', () => { |
||||
it('should handle an empty sheet', () => { |
||||
const emptySheet = utils.aoa_to_sheet([]); |
||||
const frame = workSheetToFrame(emptySheet); |
||||
|
||||
expect(frame.name).toBeUndefined(); |
||||
expect(frame.fields).toHaveLength(0); |
||||
expect(frame.length).toBe(0); |
||||
}); |
||||
|
||||
it('will use first row as names', () => { |
||||
const sheet = utils.aoa_to_sheet([ |
||||
['Number', 'String', 'Bool', 'Date', 'Object'], |
||||
[1, 'A', true, Date.UTC(2020, 1, 1), { hello: 'world' }], |
||||
[2, 'B', false, Date.UTC(2020, 1, 2), { hello: 'world' }], |
||||
]); |
||||
const frame = workSheetToFrame(sheet); |
||||
|
||||
expect(toSnapshotFrame(frame)).toMatchInlineSnapshot(` |
||||
[ |
||||
{ |
||||
"name": "Number", |
||||
"type": "number", |
||||
"values": [ |
||||
1, |
||||
2, |
||||
], |
||||
}, |
||||
{ |
||||
"name": "String", |
||||
"type": "string", |
||||
"values": [ |
||||
"A", |
||||
"B", |
||||
], |
||||
}, |
||||
{ |
||||
"name": "Bool", |
||||
"type": "boolean", |
||||
"values": [ |
||||
true, |
||||
false, |
||||
], |
||||
}, |
||||
{ |
||||
"name": "Date", |
||||
"type": "number", |
||||
"values": [ |
||||
1580515200000, |
||||
1580601600000, |
||||
], |
||||
}, |
||||
{ |
||||
"name": "Object", |
||||
"type": "string", |
||||
"values": [ |
||||
undefined, |
||||
undefined, |
||||
], |
||||
}, |
||||
] |
||||
`);
|
||||
}); |
||||
|
||||
it('will use calculated data when cells are typed', () => { |
||||
const sheet = utils.aoa_to_sheet([ |
||||
[1, 'A', true, Date.UTC(2020, 1, 1), { hello: 'world' }], |
||||
[2, 'B', false, Date.UTC(2020, 1, 2), { hello: 'world' }], |
||||
[3, 'C', true, Date.UTC(2020, 1, 3), { hello: 'world' }], |
||||
]); |
||||
const frame = workSheetToFrame(sheet); |
||||
|
||||
expect(toSnapshotFrame(frame)).toMatchInlineSnapshot(` |
||||
[ |
||||
{ |
||||
"name": "A", |
||||
"type": "number", |
||||
"values": [ |
||||
1, |
||||
2, |
||||
3, |
||||
], |
||||
}, |
||||
{ |
||||
"name": "B", |
||||
"type": "string", |
||||
"values": [ |
||||
"A", |
||||
"B", |
||||
"C", |
||||
], |
||||
}, |
||||
{ |
||||
"name": "C", |
||||
"type": "boolean", |
||||
"values": [ |
||||
true, |
||||
false, |
||||
true, |
||||
], |
||||
}, |
||||
{ |
||||
"name": "D", |
||||
"type": "number", |
||||
"values": [ |
||||
1580515200000, |
||||
1580601600000, |
||||
1580688000000, |
||||
], |
||||
}, |
||||
{ |
||||
"name": "E", |
||||
"type": "string", |
||||
"values": [ |
||||
undefined, |
||||
undefined, |
||||
undefined, |
||||
], |
||||
}, |
||||
] |
||||
`);
|
||||
}); |
||||
|
||||
it('is OK with nulls and undefineds, and misalignment', () => { |
||||
const sheet = utils.aoa_to_sheet([ |
||||
[null, 'A', true], |
||||
[2, 'B', null, Date.UTC(2020, 1, 2), { hello: 'world' }], |
||||
[3, 'C', true, undefined, { hello: 'world' }], |
||||
]); |
||||
const frame = workSheetToFrame(sheet); |
||||
|
||||
expect(toSnapshotFrame(frame)).toMatchInlineSnapshot(` |
||||
[ |
||||
{ |
||||
"name": "A", |
||||
"type": "number", |
||||
"values": [ |
||||
undefined, |
||||
2, |
||||
3, |
||||
], |
||||
}, |
||||
{ |
||||
"name": "B", |
||||
"type": "string", |
||||
"values": [ |
||||
"A", |
||||
"B", |
||||
"C", |
||||
], |
||||
}, |
||||
{ |
||||
"name": "C", |
||||
"type": "boolean", |
||||
"values": [ |
||||
true, |
||||
undefined, |
||||
true, |
||||
], |
||||
}, |
||||
{ |
||||
"name": "D", |
||||
"type": "number", |
||||
"values": [ |
||||
undefined, |
||||
1580601600000, |
||||
undefined, |
||||
], |
||||
}, |
||||
{ |
||||
"name": "E", |
||||
"type": "string", |
||||
"values": [ |
||||
undefined, |
||||
undefined, |
||||
undefined, |
||||
], |
||||
}, |
||||
] |
||||
`);
|
||||
}); |
||||
}); |
||||
|
||||
function toSnapshotFrame(frame: DataFrame) { |
||||
return frame.fields.map((f) => ({ name: f.name, values: f.values, type: f.type })); |
||||
} |
@ -1,172 +0,0 @@ |
||||
import { read, utils, WorkSheet, WorkBook, Range, ColInfo, CellObject, ExcelDataType } from 'xlsx'; |
||||
|
||||
import { DataFrame, FieldType } from '@grafana/data'; |
||||
|
||||
export function readSpreadsheet(file: ArrayBuffer): DataFrame[] { |
||||
return workBookToFrames(read(file, { type: 'buffer' })); |
||||
} |
||||
|
||||
export function workBookToFrames(wb: WorkBook): DataFrame[] { |
||||
return wb.SheetNames.map((name) => workSheetToFrame(wb.Sheets[name], name)); |
||||
} |
||||
|
||||
export function workSheetToFrame(sheet: WorkSheet, name?: string): DataFrame { |
||||
const columns = sheetAsColumns(sheet); |
||||
if (!columns?.length) { |
||||
return { |
||||
fields: [], |
||||
name: name, |
||||
length: 0, |
||||
}; |
||||
} |
||||
|
||||
return { |
||||
fields: columns.map((c, idx) => { |
||||
let type = FieldType.string; |
||||
let values: unknown[] = []; |
||||
switch (c.type ?? 's') { |
||||
case 'b': |
||||
type = FieldType.boolean; |
||||
values = c.data.map((v) => (v?.v == null ? v?.v : Boolean(v.v))); |
||||
break; |
||||
|
||||
case 'n': |
||||
type = FieldType.number; |
||||
values = c.data.map((v) => (v?.v == null ? v?.v : +v.v)); |
||||
break; |
||||
|
||||
case 'd': |
||||
type = FieldType.time; |
||||
values = c.data.map((v) => (v?.v == null ? v?.v : +v.v)); // ???
|
||||
break; |
||||
|
||||
default: |
||||
type = FieldType.string; |
||||
values = c.data.map((v) => (v?.v == null ? v?.v : utils.format_cell(v))); |
||||
break; |
||||
} |
||||
|
||||
return { |
||||
name: c.name, |
||||
config: {}, // TODO? we could apply decimal formatting from worksheet
|
||||
type, |
||||
values, |
||||
}; |
||||
}), |
||||
name: name, |
||||
length: columns[0].data.length, |
||||
}; |
||||
} |
||||
|
||||
interface ColumnData { |
||||
index: number; |
||||
name: string; |
||||
info?: ColInfo; |
||||
data: CellObject[]; |
||||
type?: ExcelDataType; |
||||
} |
||||
|
||||
function sheetAsColumns(sheet: WorkSheet): ColumnData[] | null { |
||||
const r = sheet['!ref']; |
||||
if (!r) { |
||||
return null; |
||||
} |
||||
const columnInfo = sheet['!cols']; |
||||
const cols: ColumnData[] = []; |
||||
const range = safe_decode_range(r); |
||||
const types = new Set<ExcelDataType>(); |
||||
let firstRowIsHeader = true; |
||||
|
||||
for (let c = range.s.c; c <= range.e.c; ++c) { |
||||
types.clear(); |
||||
const info = columnInfo?.[c] ?? {}; |
||||
if (info.hidden) { |
||||
continue; // skip the column
|
||||
} |
||||
const field: ColumnData = { |
||||
index: c, |
||||
name: utils.encode_col(c), |
||||
data: [], |
||||
info, |
||||
}; |
||||
const pfix = utils.encode_col(c); |
||||
for (let r = range.s.r; r <= range.e.r; ++r) { |
||||
const cell = sheet[pfix + utils.encode_row(r)]; |
||||
if (cell) { |
||||
if (field.data.length) { |
||||
types.add(cell.t); |
||||
} else if (cell.t !== 's') { |
||||
firstRowIsHeader = false; |
||||
} |
||||
} |
||||
field.data.push(cell); |
||||
} |
||||
cols.push(field); |
||||
if (types.size === 1) { |
||||
field.type = Array.from(types)[0]; |
||||
} |
||||
} |
||||
|
||||
if (firstRowIsHeader) { |
||||
return cols.map((c) => { |
||||
const first = c.data[0]; |
||||
if (first?.v) { |
||||
c.name = utils.format_cell(first); |
||||
} |
||||
c.data = c.data.slice(1); |
||||
return c; |
||||
}); |
||||
} |
||||
return cols; |
||||
} |
||||
|
||||
/** |
||||
* Copied from Apache 2 licensed sheetjs: |
||||
* https://git.sheetjs.com/sheetjs/sheetjs/src/branch/master/xlsx.flow.js#L4338
|
||||
*/ |
||||
function safe_decode_range(range: string): Range { |
||||
let o = { s: { c: 0, r: 0 }, e: { c: 0, r: 0 } }; |
||||
let idx = 0, |
||||
i = 0, |
||||
cc = 0; |
||||
let len = range.length; |
||||
for (idx = 0; i < len; ++i) { |
||||
if ((cc = range.charCodeAt(i) - 64) < 1 || cc > 26) { |
||||
break; |
||||
} |
||||
idx = 26 * idx + cc; |
||||
} |
||||
o.s.c = --idx; |
||||
|
||||
for (idx = 0; i < len; ++i) { |
||||
if ((cc = range.charCodeAt(i) - 48) < 0 || cc > 9) { |
||||
break; |
||||
} |
||||
idx = 10 * idx + cc; |
||||
} |
||||
o.s.r = --idx; |
||||
|
||||
if (i === len || cc !== 10) { |
||||
o.e.c = o.s.c; |
||||
o.e.r = o.s.r; |
||||
return o; |
||||
} |
||||
++i; |
||||
|
||||
for (idx = 0; i !== len; ++i) { |
||||
if ((cc = range.charCodeAt(i) - 64) < 1 || cc > 26) { |
||||
break; |
||||
} |
||||
idx = 26 * idx + cc; |
||||
} |
||||
o.e.c = --idx; |
||||
|
||||
for (idx = 0; i !== len; ++i) { |
||||
if ((cc = range.charCodeAt(i) - 48) < 0 || cc > 9) { |
||||
break; |
||||
} |
||||
idx = 10 * idx + cc; |
||||
} |
||||
o.e.r = --idx; |
||||
return o; |
||||
} |
@ -1,39 +0,0 @@ |
||||
import { formatFileTypes } from './utils'; |
||||
|
||||
describe('Dataframe import / Utils', () => { |
||||
describe('formatFileTypes', () => { |
||||
it('should nicely format file extensions', () => { |
||||
expect( |
||||
formatFileTypes({ |
||||
'text/plain': ['.csv', '.txt'], |
||||
'application/json': ['.json'], |
||||
}) |
||||
).toBe('.csv, .txt or .json'); |
||||
}); |
||||
|
||||
it('should remove duplicates', () => { |
||||
expect( |
||||
formatFileTypes({ |
||||
'text/plain': ['.csv', '.txt'], |
||||
'application/json': ['.json', '.txt'], |
||||
}) |
||||
).toBe('.csv, .txt or .json'); |
||||
}); |
||||
|
||||
it('should nicely format a single file type extension', () => { |
||||
expect( |
||||
formatFileTypes({ |
||||
'text/plain': ['.txt'], |
||||
}) |
||||
).toBe('.txt'); |
||||
}); |
||||
|
||||
it('should nicely format two file type extension', () => { |
||||
expect( |
||||
formatFileTypes({ |
||||
'text/plain': ['.txt', '.csv'], |
||||
}) |
||||
).toBe('.txt or .csv'); |
||||
}); |
||||
}); |
||||
}); |
@ -1,55 +1,31 @@ |
||||
import { Accept } from 'react-dropzone'; |
||||
import { Observable } from 'rxjs'; |
||||
|
||||
import { toDataFrame } from '@grafana/data'; |
||||
import { readCSV, toDataFrame } from '@grafana/data'; |
||||
|
||||
import { FileImportResult } from './types'; |
||||
|
||||
function getFileExtensions(acceptedFiles: Accept) { |
||||
const fileExtentions = new Set<string>(); |
||||
Object.keys(acceptedFiles).forEach((v) => { |
||||
acceptedFiles[v].forEach((extension) => { |
||||
fileExtentions.add(extension); |
||||
}); |
||||
}); |
||||
return fileExtentions; |
||||
} |
||||
|
||||
export function formatFileTypes(acceptedFiles: Accept) { |
||||
const fileExtentions = Array.from(getFileExtensions(acceptedFiles)); |
||||
if (fileExtentions.length === 1) { |
||||
return fileExtentions[0]; |
||||
} |
||||
return `${fileExtentions.slice(0, -1).join(', ')} or ${fileExtentions.slice(-1)}`; |
||||
} |
||||
|
||||
export function filesToDataframes(files: File[]): Observable<FileImportResult> { |
||||
return new Observable<FileImportResult>((subscriber) => { |
||||
let completedFiles = 0; |
||||
import('app/core/utils/sheet') |
||||
.then((sheet) => { |
||||
files.forEach((file) => { |
||||
const reader = new FileReader(); |
||||
reader.readAsArrayBuffer(file); |
||||
reader.onload = () => { |
||||
const result = reader.result; |
||||
if (result && result instanceof ArrayBuffer) { |
||||
if (file.type === 'application/json') { |
||||
const decoder = new TextDecoder('utf-8'); |
||||
const json = JSON.parse(decoder.decode(result)); |
||||
subscriber.next({ dataFrames: [toDataFrame(json)], file: file }); |
||||
} else { |
||||
subscriber.next({ dataFrames: sheet.readSpreadsheet(result), file: file }); |
||||
} |
||||
if (++completedFiles >= files.length) { |
||||
subscriber.complete(); |
||||
} |
||||
} |
||||
}; |
||||
}); |
||||
}) |
||||
.catch(() => { |
||||
throw 'Failed to load sheets module'; |
||||
}); |
||||
files.forEach((file) => { |
||||
const reader = new FileReader(); |
||||
reader.readAsArrayBuffer(file); |
||||
reader.onload = () => { |
||||
const result = reader.result; |
||||
if (result && result instanceof ArrayBuffer) { |
||||
const decoder = new TextDecoder('utf-8'); |
||||
const fileString = decoder.decode(result); |
||||
if (file.type === 'application/json') { |
||||
const json = JSON.parse(fileString); |
||||
subscriber.next({ dataFrames: [toDataFrame(json)], file: file }); |
||||
} else { |
||||
subscriber.next({ dataFrames: readCSV(fileString), file: file }); |
||||
} |
||||
if (++completedFiles >= files.length) { |
||||
subscriber.complete(); |
||||
} |
||||
} |
||||
}; |
||||
}); |
||||
}); |
||||
} |
||||
|
Loading…
Reference in new issue