Перейти к основному содержимому

biinternalServices

Предоставляет следующий набор сервисов:

фабрика, позволяет создать сервис для работы с моделью датасета, строковый идентификатор (schema_name) которого передан при созданнии
DatasetService.createInstance(‘ds_demo136’)
DatasetService,
Сервис, позволяющий получить список датасетов, которые доступны пользователю
DatasetsListService,
интерфейс модели такого сервиса:

interface IDatasetsListModel {
loading: boolean;
error: string;
datasets: IDatasetsListItem[];
roots: IDatasetsListItem[];
}
*/
IDatasetsListModel,
/*
Элемент такого списка датасетов
interface IDatasetsListItem extends IEntity {
id: string;
guid: string;
schema_name: string;
title: string;
description: string;
layout: string;
image: string;
lastPeriodTitle: string;
href: string;
color: string;
tiles: IDatasetsListTile[];
bookmarks: any[]; // tables.IBookmark[]
searchVisible: boolean; // TODO: remove
children: IDatasetsListItem[];
resourcesRoot: string;
parents: IDatasetsListItem[];
uiCfg: any;
deleteBookmark(bookmark: tables.IBookmark);
}
*/
IDatasetsListItem,
DatasetsListIcon,
DatasetsListView1,
IVizelConfig, // Конфиг визеля
IVizelProps,
  • Vizel - React.Component для встраивания внутрь дешлета. Может быть заменен на класс кастомного компонента
  • DsStateService - Сервис, наблюдающий за тем, на каком датасете и его дешборде мы сейчас
  • KoobDataService - Сервис для запросов данных в куб таблицы
  • KoobService - загружает всю информацию о кубе, кроме данных. Все дименшны и межи, конфиги, и прочие вспомогательные ключи
  • KoobFiltersService - Сервис, хранящий объект указанных фильтров на кубы. Отвечает за поведение управляющего дешлета.
  • PluginsManager - позволяет подключать компоненты как отдельные разделы Bi, примеры описаны в папке ds_res/plugins проекта luxmsbi-web-resources
  • ISummaryModel,
  • SummaryService,
  • shell,
  • getShell,
  • useService,
  • ISearchVM,
  • SearchVC,
  • service,
  • IShellVM,
  • IDsShellVM,
  • DsShellVC,
  • DsShell,
  • createSubspaceGenerator

Рассмотрим самые значимые сервисы для работы с таблицами OLAP кубов.

KoobService

Загружает информацию о таблице из куба и сохраняет информацию о ней в кеше.

interface IKoobModel {
id: string;
loading?: boolean;
error?: string;
dimensions: IKoobDimension[];
measures: IKoobMeasure[];
}
class KoobService extends BaseService<IKoobModel> {
public readonly id: string;
private _detailedEntities: { [entityId: string]: string[] } = {};
private _isMainLoading: boolean = true;

private constructor(koobId: string) {
super({
id: koobId,
loading: true,
error: null,
dimensions: [],
measures: [],
});
this.id = koobId;
this._init();
}

public async loadEntityDetails(entityId: string) {
let fullId: string = entityId;
if (!fullId.startsWith(this.id + '.')) {
fullId = this.id + '.' + fullId;
}
entityId = fullId.split('.').slice(-1)[0];

if (fullId in this._detailedEntities) return; // marked as loading or exists
this._detailedEntities[fullId] = null; // bad loading mark
this._detailedEntities[entityId] = null; // bad loading mark
const url = AppConfig.fixRequestUrl(`/api/v3/koob/${fullId}`);

this._updateWithLoading();

const request = await fetch(url, {credentials: 'include'});
const result = await request.json();

if (result.values && Array.isArray(result.values)) {
result.members = result.values.map(value => ({id: value, title: value}));
}


this._detailedEntities[fullId] = result; // save detailed entity to cache
this._detailedEntities[entityId] = result; // save detailed entity to cache

let {dimensions, measures} = this._model;

let idx = $eidx(dimensions, entityId); // update if necessary

if (idx !== -1) {
dimensions = dimensions.slice(0);
dimensions[idx] = result;
}
idx = $eidx(measures, entityId);
if (idx !== -1) {
measures = measures.slice(0);
measures[idx] = result;
}

const loading = this._isDetailsLoading() || this._isMainLoading;
this._updateModel({loading, dimensions, measures});
}

private _isDetailsLoading(): boolean {
for (let entityId in this._detailedEntities) {
if (this._detailedEntities[entityId] === null) { // null is loading mark
return true;
}
}
return false;
}

private async _init() {
try {
const url = AppConfig.fixRequestUrl(`/api/v3/koob/${this.id}`);
const result: any = (await axios.get(url)).data;
if (!this._model) { // disposed!
return;
}
result.dimensions.forEach((dimension, idx) => {
if (dimension.id.match(/^\w+\.\w+\.(\w+)$/)) { // иногда приходят в формате x.y.ID
dimension.id = RegExp.$1;
}
dimension.axisId = dimension.id;
if (this._detailedEntities[dimension.id]) {
result.dimensions[idx] = this._detailedEntities[dimension.id];
}
});
result.measures.forEach((measure, idx) => {
if (this._detailedEntities[measure.id]) {
result.measures[idx] = this._detailedEntities[measure.id];
}
});
this._isMainLoading = false;
const loading = !!this._isDetailsLoading() || this._isMainLoading;

this._updateModel({
error: null,
loading,
dimensions: result.dimensions,
measures: result.measures,
});
} catch (err) {
console.error(err);
this._updateModel({
error: extractErrorMessage(err),
loading: false,
dimensions: [],
measures: [],
});
}
}

protected _dispose() {
KoobService._cache.remove(this.id);
super._dispose();
}

private static _cache = createObjectsCache(id => new KoobService(String(id)), '__koobServices');

public static createInstance: (id: string | number) => KoobService = KoobService._cache.get;
}

KoobDataService

Сервис позволяет загружать разные срезы данных по указанным фильтрам и наборам дименшнов и меж в рамках указанного куба.

interface IKoobDataRequest3 {
options?: string[];
offset?: number;
limit?: number;
sort?: string[];
subtotals?: string[];
cancelToken?: CancelToken;
}
async function koobCountRequest3(koobId: string,
dimensions: string[],
measures: string[],
allFilters: any,
request: IKoobDataRequest3 = {}) {
const url: string = AppConfig.fixRequestUrl(`/api/v3/koob/count`);
const columns = dimensions.concat(measures);

let filters: any;
if (allFilters && typeof allFilters === 'object') {
filters = {};
Object.keys(allFilters).forEach(key => {
let value = allFilters[key];
if (value === '∀' || value === '*') { // фильтр подразумевающий 'ВСЕ'
return; // просто не выносим в фильтры
} else if (Array.isArray(value) && value[0] === 'IN') { // много где сконфигурировано ['IN', 'a', 'b']
value = ['='].concat(value.slice(1));
}
filters[key] = value;
});
}

const body: any = {
with: koobId,
columns,
filters,
};

if (request.offset) body.offset = request.offset;
if (request.limit) body.limit = request.limit;
if (request.sort) body.sort = request.sort;
if (request.options) body.options = request.options;
if (request.subtotals?.length) body.subtotals = request.subtotals;

if (!measures.length) { // если нет measures, то лучше применить distinct
body.distinct = [];
}

try {
const response = await axios({
url,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/stream+json',
},
data: body,
cancelToken: request.cancelToken,
});

let data = response.data;

if (String(response.headers['content-type']).startsWith('application/stream+json')) {
if (typeof data === 'string') {
data = data.split('\n').filter((line: string) => !!line).map((line: string) => JSON.parse(line));
} else if (data && (typeof data === 'object') && !Array.isArray(data)) {
data = [data];
}
}

return data;
} catch (e) {
AlertsVC.getInstance().pushDangerAlert(extractErrorMessage(e));
return '';
}

}
async function koobDataRequest3(koobId: string,
dimensions: string[],
measures: string[],
allFilters: any,
request: IKoobDataRequest3 = {}) {
const url: string = AppConfig.fixRequestUrl(`/api/v3/koob/data`);
const columns = dimensions.concat(measures);

let filters: any;
if (allFilters && typeof allFilters === 'object') {
filters = {};
Object.keys(allFilters).forEach(key => {
let value = allFilters[key];
if (value === '∀' || value === '*') { // фильтр подразумевающий 'ВСЕ'
return; // просто не выносим в фильтры
} else if (Array.isArray(value) && value[0] === 'IN') { // много где сконфигурировано ['IN', 'a', 'b']
value = ['='].concat(value.slice(1));
}
filters[key] = value;
});
}

const body: any = {
with: koobId,
columns,
filters,
};

if (request.offset) body.offset = request.offset;
if (request.limit) body.limit = request.limit;
if (request.sort) body.sort = request.sort;
if (request.options) body.options = request.options;
if (request.subtotals?.length) body.subtotals = request.subtotals;

if (!measures.length) { // если нет measures, то лучше применить distinct
body.distinct = [];
}

// test
// body.limit = 2;

try {
const response = await axios({
url,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/stream+json',
},
data: body,
cancelToken: request.cancelToken,
});

let data = response.data;

if (String(response.headers['content-type']).startsWith('application/stream+json')) {
if (typeof data === 'string') {
data = data.split('\n').filter((line: string) => !!line).map((line: string) => JSON.parse(line));
} else if (data && (typeof data === 'object') && !Array.isArray(data)) {
data = [data];
}
}

return data;

} catch (e) {
AlertsVC.getInstance().pushDangerAlert(extractErrorMessage(e));
return '';
}

}
interface IKoobDataModel {
loading?: boolean;
error?: string;
dimensions: IKoobDimension[];
measures: IKoobMeasure[];
values: any[];
sort?: string[];
subtotals?: string[];
}
class KoobDataService extends BaseService<IKoobDataModel> {
public static koobDataRequest3 = koobDataRequest3;
public static koobCountRequest3 = koobCountRequest3;
private readonly _koobId: string;
private _dimensions: IKoobDimension[];
private _measures: IKoobMeasure[];
private _filters: any = undefined;
private _loadBy: number | null = null;
private _totalPages: number | null = null;
private _loadedPages: number | null = null;
private _sort: string[] | null = null;
private _subtotals: string[] | null = null;

public constructor(koobId: string,
dimensions: IKoobDimension[],
measures: IKoobMeasure[],
filters: any,
loadBy?: number,
sort?: any,
subtotals?: string[]) {
super({
loading: true,
error: null,
dimensions: [],
measures: [],
values: [],
sort: null,
subtotals: null,
});
this._sort = typeof sort === 'string' ? [sort] : sort;
this._koobId = koobId;
this._dimensions = dimensions;
this._measures = measures;
this._filters = filters;
this._loadBy = loadBy ?? null;
this._subtotals = Array.isArray(subtotals) ? subtotals : null;
this._load();
}

protected _dispose() {
this.abort();
super._dispose();
}

public abort() {
if (this._loadCancel) {
this._loadCancel.cancel('canceled');
this._loadCancel = null;
}
}

private async loadPage(page: number) {
if (this._totalPages !== null) return; // Все загрузилось
if (page < this._loadedPages) return; // уже грузятся

let loadedPages = this._loadedPages;
this._loadedPages = page + 1; // выставим переменную, чтоб знали, что уже грузится

this._updateModel({loading: true});

for (let p = loadedPages; p <= page; p++) {
const newValues = await koobDataRequest3(
this._koobId,
this._dimensions.map(d => d.id),
this._measures.map(m => m.formula),
this._filters,
{offset: page * this._loadBy, limit: this._loadBy, sort: this._sort, subtotals: this._subtotals});
// console.log('Loaded new values with offset=', page * this._loadBy, 'limit=', this._loadBy, 'loaded=', newValues.length);
// сохраняем в данные
let values = this._model.values.slice(0);
for (let i = 0; i < newValues.length; i++) {
values[page * this._loadBy + i] = newValues[i];
}
this._updateModel({values});
if (newValues.length < this._loadBy) { // загрузилось не все - значит, кончилось
this._loadedPages = this._totalPages = p + 1;
break;
}
}
this._updateModel({loading: false});
}

public loadItem(n: number) {
if (!this._loadBy) return; // данные грузятся полностью
if (this._totalPages !== null) return; // есть переменная выставленная - значит все загружено (или грузится)
if (n < this._model.values.length) return; // загружено или грузится

let page = Math.ceil(n / this._loadBy);
this.loadPage(page);
}

public setSort(sort: string | string[] | null) {
this._sort = typeof sort === 'string' ? [sort] : sort;
this._totalPages = null;
this._loadedPages = null;
this._load();
}

private _loadCancel: CancelTokenSource | null = null;

private async _load() {
try {
this._updateWithLoading();

this._loadCancel = axios.CancelToken.source();

const values = await koobDataRequest3(
this._koobId,
this._dimensions.map(d => d?.formula || d.id), // для сгенерированных dimension
this._measures.map(m => m.formula),
this._filters,
{offset: 0, limit: this._loadBy, sort: this._sort, cancelToken: this._loadCancel.token, });

this._loadCancel = null;

if (this._loadBy) {
this._loadedPages = 1;
// console.log('Loadede page 0: total=', values.length);
if (values.length < this._loadBy) {
this._totalPages = 1;
}
}

this._updateWithData({
dimensions: this._dimensions,
measures: this._measures,
values,
sort: this._sort,
});
} catch (err) {
this._updateWithError(extractErrorMessage(err));
}
}

public setFilter(filters: any) {
this._filters = filters;
this._totalPages = null;
this._loadedPages = null;
this._load();
}
}

KoobFilterService

Позволяет выставлять условия на дименшны для любых кубов, у которых названия дименшнов совпадают

throttleTimeout = 3000
interface IKoobFiltersModel {
loading?: boolean;
error?: string;
query?: string;
result: any;
filters: any;
pendingFilters: any;
}

class KoobFiltersService extends BaseService<IKoobFiltersModel> {
private constructor() {
super({
loading: false,
error: null,
query: undefined,
result: {},
filters: {},
pendingFilters: {},
});
UrlState.subscribeAndNotify('_koobFilters f', this._onUrlStateUpdated);
}
protected _dispose() {
UrlState.unsubscribe(this._onUrlStateUpdated);
super._dispose();
}

private _onUrlStateUpdated = (url) => {
this._updateWithData({filters: {...url._koobFilters, ...url.f}});
}
public setFilter(koob: string, dimension: string, filter?: any[]) {
let filters = this._model?.pendingFilters;
if (filter) {
let arr: string[] | undefined = filter?.slice(0);
filters = {...filters, [dimension]: arr};
} else {
filters = {...filters, [dimension]: undefined};
}
this._updateModel({pendingFilters: filters});
this._applyAllFilters();
}

public setFilters(koob: string, newFilters: any) {
let filters = this._model.pendingFilters;
for (let dimension in newFilters) {
let filter = newFilters[dimension];
if (filter) {
let arr: string[] | undefined = filter?.slice(0);
filters = {...filters, [dimension]: arr};
} else {
filters = {...filters, [dimension]: undefined};
}
}
this._updateModel({pendingFilters: filters});
this._applyAllFilters();
}

public applyDimensionFilter(dimension: string, value: string | number, toggleFlag: boolean, allValues: (string | number)[]) {
const filters = this._model.pendingFilters;
const current = filters[dimension];
let arr: (string | number)[];

if (toggleFlag) {
arr = current ?
current.concat(value) :
['=', value];
} else {
arr = current ?
['='].concat(current.slice(1).filter(e => e != value)) :
['='].concat(allValues.filter(e => e != value) as any); // when not set, consider that every was selected
}

const _filters = {...filters, [dimension]: arr};
this._updateModel({pendingFilters: _filters});
this._applyAllFilters();
}

public applyPeriodsFilter(dimension: string, lodate: string | number, hidate: string | number) {
const filters = this._model.pendingFilters;
const _filters = {...filters, [dimension]: ['between', lodate, hidate]};
this._updateModel({pendingFilters: _filters});
this._applyAllFilters();
}

private _applyAllFilters = throttle(() => {
const filters = {...this._model.filters, ...this._model.pendingFilters};
this._updateModel({pendingFilters: {}});

const url = UrlState.getInstance().getModel();
let publicKeys = Object.keys(url.f || {}); // Раскидываем ключи фильтров на две части - публичную и скрытую
const publicFilters = {}, privateFilters = {}; // в публичную попадают ключи, которые уже есть в url
for (let key in filters) {
if (publicKeys.includes(key)) {
publicFilters[key] = filters[key];
} else {
privateFilters[key] = filters[key];
}
}

UrlState.getInstance().updateModel({f: publicFilters, _koobFilters: privateFilters});
}, throttleTimeout);

public static getInstance = createSingleton(() => new KoobFiltersService(), '__koobFiltersService');
}

Рассмотрим пример работы с файлом экселя внутри кастомного компонента.
Прочитав основные руководства по работе с данными на платформе, вы поймете как загружаются кубы на основе данных из экселя. Вы можете вывести этот куб как таблицу или иной вид визуализации в рамках коробочного решения. если вам будет нужно менять значения в этой таблице то нужно будет создать новый дешлет, где будет интерфейс, позволяющий запустить процесс редактирования данных.
Для начала загрузим документ экселя как источник данных. Файл экселя это потенциально несколько листов. Каждый лист считается отдельным источником данных и имеет свой идентификатор.
Загружаем файл экселя через input[type=file], берем при загрузке событие и у него event.target.files

import { AppConfig } from '@luxms/bi-core';
// тут код вашего компонента
// код функции, принимающей список загруженных файлов в форму
const url = AppConfig.fixRequestUrl('/srv/datagate/source/upload');
const files = Array(event.target.files);
const formData = new FormData(); // (https://developer.mozilla.org/ru/docs/Web/API/FormData)
files.forEach(file => formData.append('source', file))

Далее шлем async запрос например через axios:

axios.post(url, formData, { headers: { 'Content-Type': 'multipart/form-data' } })

У ответа проверяем ключа data.documents, где будут храниться название файла, ряд вспомогательных ключей и список листов документа tableContainers. Для получения данных каждого листа нужно в масссиве tableContainers найти tableName и сделать GET запрос на api/db/xdds.t1464/

где t1464 - это tableName нужного листа, а xdds - зарезервированное название пакета всех таблиц, что были загружены из экселя.

Это REST API, для которого PUT, DELETE, GET и прочие запросы работают Используйте filter для тонкой выборки данных:

api/db/xdds.t1464/.filter(a='Дорога')

Отфильтрует таблицу по указанному значению.
Если вы уже загрузили данные в куб, залинковав через административный интерфейс источники данных в виде экселя, то вам достаточно найти нужные таблицы и работая средствами REST API с таблицей xdds.{Имя_листа} вы сможете менять данные прямо в источнике данных. Куб же автоматически подхватит данные из источника.