Рубрика: JS

  • Editor API

    Вводная

    Использовать API Connector в DataLens можно для множества вещей, на этой странице разберем основные подходы, как делать базовые запросы через API и разберем интересные примеры использования.

    Основные шаги подключения API

    Простое видео, как сделать запрос к API внутри DataLens

    То есть, основные шаги

    • Создать API или получить информацию по адресу и формату работы с некоторым API
    • Создать подключение к API
    • Создать чарт Editor
    • Написать и настроить код на Sources для интерактивной работы с API
    • Обработать результат на Prepare

    Пример: простой чат внутри DataLens

    Python Код для API

    import psycopg2
    import os
    import csv
    import io
    
    def handler(event, context):
        params = event.get('multiValueQueryStringParameters', {})
        chat_id   = params.get('chat_id',   [None])[0]
        message   = params.get('message',   [None])[0]
        author    = params.get('author',    [None])[0]
        message_key = params.get('message_key', [None])[0]
    
        if not chat_id:
            return {
                'statusCode': 400,
                'body': "Ошибка: параметр 'chat_id' является обязательным.",
                'headers': {'Content-Type': 'text/csv; charset=UTF-8'},
            }
    
        pg_pass = os.environ.get('PG_PASS')
        try:
            conn = psycopg2.connect(
                user="user1",
                password=pg_pass,
                host="rc1a-***.mdb.yandexcloud.net",
                port="6432",
                database="db1"
            )
            cur = conn.cursor()
    
            # 1. Обработка message_key: проверка и вставка
            if message_key is not None:
                cur.execute(
                    "SELECT 1 FROM chat_messages WHERE chat_id = %s AND message_key = %s",
                    (chat_id, message_key)
                )
                if not cur.fetchone():
                    # ключ не найден — вставляем полную строку
                    cur.execute(
                        "INSERT INTO chat_messages (chat_id, author, message, message_key, dttm) "
                        "VALUES (%s, %s, %s, %s, NOW())",
                        (chat_id, author, message, message_key)
                    )
                    conn.commit()
    
            # 2. Обычная логика добавления сообщения (если нет message_key)
            elif message and author:
                stop_words = ['политика', 'религия', 'другие чувствительные темы']
                if not any(w in message.lower() for w in stop_words):
                    cur.execute(
                        "SELECT 1 FROM chat_messages "
                        "WHERE author = %s AND message = %s AND chat_id = %s "
                        "AND dttm > NOW() - INTERVAL '1 minute'",
                        (author, message, chat_id)
                    )
                    if not cur.fetchone():
                        cur.execute(
                            "INSERT INTO chat_messages (chat_id, author, message, dttm) "
                            "VALUES (%s, %s, %s, NOW())",
                            (chat_id, author, message)
                        )
                        conn.commit()
    
            # 3. Получение последних 30 сообщений
            cur.execute("""
                SELECT author, message, dttm
                FROM (
                    SELECT author, message, dttm
                    FROM chat_messages
                    WHERE chat_id = %s
                    ORDER BY dttm DESC
                    LIMIT 30
                ) AS last_30
                ORDER BY dttm ASC;
            """, (chat_id,))
            rows = cur.fetchall()
    
            output = io.StringIO()
            w = csv.writer(output)
            w.writerow(['author', 'message', 'dttm'])
            if not rows:
                w.writerow(['-', '-', '-'])
            else:
                for r in rows:
                    w.writerow([r[0], r[1], r[2].isoformat()])
    
            return {
                'statusCode': 200,
                'body': output.getvalue(),
                'headers': {'Content-Type': 'text/csv; charset=UTF-8'},
            }
    
        except psycopg2.Error as e:
            return {
                'statusCode': 500,
                'body': f"Ошибка базы данных: {e}",
                'headers': {'Content-Type': 'text/csv; charset=UTF-8'},
            }
        finally:
            if 'cur' in locals():
                cur.close()
            if 'conn' in locals():
                conn.close()
    

    API Editor Chart

    var params = Editor.getParams();
    
    var author = params.user[0] != '' ? params.user[0]:Editor.getUserLogin();
    var message = params.message_to_send[0];
    var chat_id = params.chat_id[0];
    
    var message_key = author + params.send_dttm[0];
    
    if (message != '') {
    module.exports = {
        'myApiDataSource': {
            apiConnectionId: 'mu751arjzpn88',
            path: encodeURI(`?chat_id=${chat_id}&author=${author}&message=${message}&message_key=${message_key}`),
            method: 'GET'
            
            
        }
    }
    }
    else {
        module.exports = {
        'myApiDataSource': {
            apiConnectionId: 'mu751arjzpn88',
            path: `?chat_id=${chat_id}`,
            method: 'GET'
            
        }
    }
    }
    Editor.updateParams({"send_it":[""]});
    
    function print(mess) {
      console.log(mess);
    }
    
    
    function parseCsvToObjects(csvString) {
      const lines = csvString.trim().split('\n');
    
      // Функция, которая разбивает строку на значения с учётом кавычек
      function parseCsvLine(line) {
        const result = [];
        let current = '';
        let inQuotes = false;
    
        for (let i = 0; i < line.length; i++) {
          const char = line[i];
    
          if (char === '"') {
            // Если текущий символ кавычка, проверяем следующий чтобы определить если это экранирование
            if (inQuotes && line[i + 1] === '"') {
              // Экранированная кавычка двойным двойным
              current += '"';
              i++; // пропускаем следующий символ
            } else {
              inQuotes = !inQuotes; // переключаем состояние в/из кавычек
            }
          } else if (char === ',' && !inQuotes) {
            // Разделитель поля только если не внутри кавычек
            result.push(current);
            current = '';
          } else {
            current += char;
          }
        }
        result.push(current);
        return result;
      }
    
      const headers = parseCsvLine(lines[0]).map(h => h.trim());
    
      return lines.slice(1).map(line => {
        const values = parseCsvLine(line);
        const obj = {};
    
        headers.forEach((header, index) => {
          obj[header] = values[index] ? values[index].trim() : '';
        });
    
        return obj;
      });
    }
    
    
    var params = Editor.getParams();
    var current_user = params.user[0] != '' ? params.user[0]:Editor.getUserLogin();
    
    
    function csvToMarkdownChat(csvString) {
      const data = parseCsvToObjects(csvString);
      return data.map(({ author, message, dttm }) => {
        const isCurrentUser = author === current_user;
        const time = new Date(dttm).toLocaleTimeString([], { 
          hour: '2-digit', 
          minute: '2-digit',
          hour12: false // 24-часовой формат
        });
        
        // Иконка пользователя
        const userIcon = isCurrentUser ? '👤' : '👥';
        
        // Строка автора и времени
        const authorTimeLine = isCurrentUser 
          ? `> **${userIcon} Вы (${time})**` 
          : `**${userIcon} ${author} (${time})**`;
        
        // Сообщение с правильным выравниванием
        const messageLine = isCurrentUser 
          ? `{blue}(${message.replace(/\n/g, '\n> ')})` // Сохраняем переносы строк в цитате
          : message.replace(/\n/g, '\n'); // Обычное сообщение
        
        return `${authorTimeLine}\n${messageLine}`;
      }).join('\n\n');
    }
    
    
    
    const response = Editor.getLoadedData();
    print(response['myApiDataSource'].data.body.result);
    print(parseCsvToObjects(response['myApiDataSource'].data.body.result));
    const markdown = csvToMarkdownChat(response['myApiDataSource'].data.body.result);
    
    
    module.exports = {
        markdown
    };

    JS SELECTOR for message

    // Controls Tab
    
    var controls = [];
    var params = Editor.getParams();
    var message = params.message[0];
    
    var key = new Date().getTime() / 1000;
    
    if (message != '') {
    
        controls.push(
        {
            type: 'input',
            param: 'message',
            width:'85%',
            postUpdateOnChange: true
        })
            controls.push(
        {
            type: 'input',
            param: 'message_to_send',
            hidden:true
        })
        controls.push(
        {
            type: 'button',
            label: '💬',
            theme: 'action',
            updateOnChange: true,
            onClick: {
            action: 'setParams',
            args: {
                message_to_send:message,
                message: '',
                send_dttm:key
            }
            
        }
        }
        )
    } 
    else {
        controls.push(
        {
            type: 'input',
            param: 'message',
            width:'85%',
            updateOnChange: true
        })
         controls.push(
        {
            type: 'input',
            param: 'message_to_send',
            hidden:true
        })
    }
    
    module.exports = controls;
  • DataLens Editor

    Зачем?

    В данной статье будут по шагам освещены основные моменты, которые нужно знать, чтобы эффективно делать чарты в Editor в продукте DataLens. Основной способ донесения информации — короткие видеоролики

    Что такое Editor?

    Чарт или селектор Editor — это отдельный объект в DataLens, логика которого описывается JS-кодом.

    • Чарты Editor гибче Wizard, так как позволяет подключаться к нескольким источникам данных и отрисовывать любую требуемую визуализацию через HTML и SVG.
    • Селекторы Editor позволят вам реализовать кодозависимые поля выбора с уникальной логикой.

    Общая схема работы объекта приведена ниже, детально о вкладках и примерах их заполнения смотрите видео.

    Разбираем простой чарт Editor

    В видео ниже будет разобран по шагам простой чарт Editor. По ходу статьи будем усложнять логику, которую можно реализовать в Editor.

    Примеры кода из видео

    // КОД ВКЛАДКИ SOURCES Простого чарта
    const {buildSource} = require('libs/dataset/v2');
    
    const params = Editor.getParams();
    const date_interval = params.date_interval[0];
    const limit = parseInt(params.limit[0]);
    const metrics_list = params.metrics_list;
    
    // создаем пустой массив для заполнения условий
    let where = [];
    // колонки для выбора
    let cols = [];
    // параметры в датасет
    let filled_params = []
    
    
    // Базовое заполнение фильтра по дате как есть
    // поле для фильтрации
    const date_dim = params.date_dimension[0];
    // парсим даты из интервала
    const {from:from_date,to:to_date} = Editor.resolveInterval(date_interval);
    // собираем объект для фильтра
    const dateFilter = {column: date_dim,
                            operation: 'BETWEEN',
                        values: [from_date, to_date]}
    // добавляем в массив where
    where.push(dateFilter)
    
    // добавляем колонку region_delivery
    cols.push('region_delivery');
    
    // добавляем колонки для выбора метрик
    cols.push(...metrics_list);
    
    
    module.exports = {
            dataset: buildSource({
                id: Editor.getId('dataset'),
                columns: cols,
                where: where,
                parameters:filled_params,
                limit:limit,
                order_by:[{'direction':'DESC','column':metrics_list[0]}]
            })
        }
    
    // КОД ВКЛАДКИ PREPARE Простого чарта
    // Импорт необходимых библиотек и получение параметров
    const Dataset = require('libs/dataset/v2'); // Библиотека для обработки данных
    const params = Editor.getParams(); // Получение параметров из редактора
    
    const loadedData = Editor.getLoadedData(); // Получаем загруженные данные
    const preparedData = Dataset.processData(loadedData, 'dataset', Editor); // Обрабатываем данные
    console.log(preparedData)
    
    // Получаем параметры для сводной таблицы
    const metrics_list = params.metrics_list; // Список метрик
    // Измерения для строк (до 4 измерений, пустые отфильтрованы)
    const dimensions = Object.keys(preparedData[0]).filter(key => !metrics_list.includes(key));
    
    // Формируем заголовки
    const head = [
        // Сначала добавляем измерения
        ...dimensions.map(dim => ({
            id: dim,
            name: dim,
            type: 'text'
        })),
        // Затем метрики с индикаторами
        ...metrics_list.map(metric => ({
            id: metric,
            name: metric,
    
        }))
    ];
    
    // Формируем строки
    const rows = preparedData.map(row => ({
        cells: [
            // Ячейки для измерений
            ...dimensions.map(dim => ({
                value: row[dim],
            })),
            // Ячейки для метрик с индикаторами
            ...metrics_list.map(metric => ({
                value: row[metric]
            }))
        ]
    }));
    
    module.exports = {head, rows};
    

    Как в Editor получить значения фильтров и параметров с дашборда

    Для того, чтобы наш чарт был интерактивным, полезным, чтобы мы могли взаимодействовать с ним, как с объектами Wizard, надо, чтобы любые параметры и фильтры, задаваемые в дэше, влияли на отправляемый запрос к датасету и/или подключению. Давайте посмотрим, как получить все передаваемые с дэша параметры и фильтры

    Обработка параметров через встроенные функции объекта Editor

    Когда мы поняли, что всё, что есть на дэше, можно переиспользовать внутри Editor, надо понять, как это сделать оптимально, давайте разберем основные полезные методы объекта Editor

    // парсинг дат
    const {from:from_date,to:to_date} = ce_obj.resolveInterval(date_interval);
    // парсинг полей с операциями
    const {operation:suffx, value:vala} = Editor.resolveOperation(value);
    // парсинг относительных дат
    const val = Editor.resolveRelative(val);

    Посмотрите видео с детальным разъяснением

    Полный пример кода из видео

    // КОД ВКЛАДКИ PREPARE Обработки параметров и фильтров
    const {buildSource} = require('libs/dataset/v2');
    const {dateTimeParse,dateTime,addDays, addUnits,startOf} = require('@gravity-ui/date-utils');
    const FORMAT = 'YYYY-MM-DD'
    
    
    function arrayChecker(some) {
        if (Array.isArray(some)) {
            return some
        }
        else {
            return [some]
            }
    }
    
    function valArrayFromPrefix(arr) {
                let vals = [];
                arr.forEach((element) => {
                    const {operation:suffx, value:vala} = Editor.resolveOperation(element);
                     vals.push(vala);
                });
                return vals;
    }
    
    
    function getDateFilters(ce_obj,params) {
        let date_interval = params.date_interval[0];
        let date_dim = params.filter_date_dimension[0];
        let scale_name = params.date_scale[0];
            let scale_dict = {'day':'D','week':'W','month':'M'};
        
        const {from:from_date,to:to_date} = ce_obj.resolveInterval(date_interval);
        let right_date = to_date;
        let left_date = from_date;
        console.log(right_date,left_date);
        const dateTo = dateTimeParse(right_date).add(1,scale_name).startOf(scale_dict[scale_name]).add(-1,'day').format(FORMAT);
        const dateFrom = dateTimeParse(left_date).startOf(scale_dict[scale_name]).format(FORMAT);
    
        const dateFilter = {column: date_dim,
                                operation: 'BETWEEN',
                            values: [dateFrom, dateTo]}
        return dateFilter;
    }
    
    
        const params = Editor.getParams();
        // для удобства все параметры переносим в переменные без парамс
        const date_interval = params.date_interval[0];
        const limit = parseInt(params.row_limit[0]);
        const metrics_list = params.metrics_list;
        const params_to_send = params.params_to_send[0];
        const filter_fields = params.filter_fields_ids['0'].split('|');
        const filter_mass = params.mass_filter_fields_ids['0'].split('|');
    
    // массив для заполнения условий
    let where = [];
    // колонки для выбора
    let cols = [];
    // передавать параметры в датасет
    let filled_params = []
    
    // 1) Фильтр на даты - отдельная обработка
    let q = getDateFilters(Editor,params);
    where.push(q)
    
    // Фильтры и параметры дэша - все по циклу
    
    for (const [key, value] of Object.entries(params)) {
        // добавляем все параметры из переданных и не пустых
        if (params_to_send.includes(key)) {
          filled_params.push({id:key,value:value.toString()});
        };
    
        if (filter_fields.includes(key) && value != '') {
                let val, suff;
    
    
            if (value[0] && value[0].substr(0, 2) === '__') {
                const {operation:suffx, value:vala} = Editor.resolveOperation(value);
                val = value.length == 1 ? vala:valArrayFromPrefix(value);
                suff = suffx;
    
            } else {
                val = value;
                suff = 'IN';
            }
    
    
            if  (Editor.resolveRelative(val) != null) {
                  val = Editor.resolveRelative(val);
             }
            if  (Editor.resolveInterval(value) != null) {
                  val = [Editor.resolveInterval(value)['from'],Editor.resolveInterval(value)['to']];
                  suff = 'BETWEEN'
             }
    
            if  (filter_mass.includes(key)) {
            suff = 'IN';
            val = value[0].split(' ');
        }
         
        where.push({type:'id',column: key, operation: suff, values: arrayChecker(val)})
      }
      
          if ((key.startsWith('dimension_') || key.startsWith('dim_col'))  && value[0] != '') {
                cols.push(value[0]);
        }
    
    }
    
    // добавляем колонки для выбора данных
    cols.push(...metrics_list);
    
    
    module.exports = {
            dataset: buildSource({
                id: Editor.getId('dataset'),
                columns: cols,
                where: where,
                parameters:filled_params,
                order:params.date_dimension[0],
                limit:limit*2,
                order_by:[{'direction':'DESC','column':metrics_list[0]}]
            })
        }
    

    Обработка событий click и tooltip

    Очень важная часть в дашборде — интерактивность. Рассмотрим, как добавить интерактивность в Advanced-чарты в виде tooltip и обработки кликов на элементы

    // КОД ВКЛАДКИ PREPARE Работа с кликами и тултипами
    // Данные для двух метрик
    const primaryData = [{x: 'Категория A', y: 30, id: 'A'},{x: 'Категория B', y: 80, id: 'B'},{x: 'Категория C', y: 45, id: 'C'},{x: 'Категория D', y: 60, id: 'D'},{x: 'Категория E', y: 20, id: 'E'}];
    const secondaryData = [{x: 'Категория A', y: 50, id: 'A'},{x: 'Категория B', y: 40, id: 'B'},{x: 'Категория C', y: 75, id: 'C'},{x: 'Категория D', y: 30, id: 'D'}];
    const selectedMetric =  'primary';
    
    // Конфигурация
    const config = {
        data: {primaryData:primaryData,secondaryData:secondaryData},
        selectedMetric: selectedMetric
    };
    
    module.exports = {
        render: Editor.wrapFn({
            fn: function(dimensions, config) {
                const {width, height} = dimensions;
    
                const state = Chart.getState() || {};
                const selectedItem = state.selectedItem || config.selectedMetric;
                
                const currentData = selectedItem === 'secondary' ? config.data.secondaryData : config.data.primaryData;
                const sortedData = [...currentData].sort((a, b) => b.y - a.y);
                // Создаем контейнер
                const container = document.createElement('div');container.style.setProperty('display', 'flex');container.style.setProperty('flex-direction', 'column');container.style.setProperty('height', '100%');container.style.setProperty('font-family', 'sans-serif');
                // Блок метрик
                const metricsContainer = document.createElement('div');metricsContainer.style.setProperty('display', 'flex');metricsContainer.style.setProperty('margin', '10px');metricsContainer.style.setProperty('gap', '10px');
    
                // Определяем, какая метрика выбрана
                const isPrimarySelected = selectedItem === 'primary';
    
                // Кнопка "Основная"
                const primaryBtn = document.createElement('div');
                primaryBtn.innerHTML = 'Основная метрика';primaryBtn.style.setProperty('padding', '8px 12px');primaryBtn.style.setProperty('border-radius', '4px');primaryBtn.style.setProperty('cursor', 'pointer');primaryBtn.style.setProperty('user-select', 'none');primaryBtn.style.setProperty('background-color', isPrimarySelected ? '#1e88e5' : '#e0e0e0');
                primaryBtn.style.setProperty('color', isPrimarySelected ? 'white' : 'black');
                primaryBtn.setAttribute('data-id', 'primary');
                metricsContainer.appendChild(primaryBtn);
    
                // Кнопка "Альтернативная"
                const secondaryBtn = document.createElement('div');
                secondaryBtn.innerHTML = 'Альтернативная метрика';
                secondaryBtn.style.setProperty('padding', '8px 12px');secondaryBtn.style.setProperty('border-radius', '4px');secondaryBtn.style.setProperty('cursor', 'pointer');secondaryBtn.style.setProperty('user-select', 'none');
                // СТИЛИ в зависимости от выбора
                secondaryBtn.style.setProperty('background-color', !isPrimarySelected ? '#1e88e5' : '#e0e0e0');
                secondaryBtn.style.setProperty('color', !isPrimarySelected ? 'white' : 'black');
                secondaryBtn.setAttribute('data-id', 'secondary');
                metricsContainer.appendChild(secondaryBtn);
    
                container.appendChild(metricsContainer);
    
                // Блок графика
                const chartContainer = document.createElement('div');
                chartContainer.style.setProperty('flex', '1');
                chartContainer.style.setProperty('position', 'relative');
                chartContainer.style.setProperty('margin', '10px');
    
                // Создаем SVG
                const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
                svg.setAttribute('width', '100%');svg.setAttribute('height', '90%');svg.setAttribute('viewBox', `-20 0 ${width} ${height}`);svg.style.setProperty('overflow', 'visible');
    
                const margin = {top: 20, right: 20, bottom: 40, left: 50};
                const innerWidth = width - margin.left - margin.right;
                const innerHeight = height - margin.top - margin.bottom;
    
                // Группа для отступов
                const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
                g.setAttribute('transform', `translate(${margin.left},${margin.top})`);
                svg.appendChild(g);
    
                // Масштабы: теперь x — это значения, y — категории
                const xScale = d3.scaleLinear()
                    .domain([0, d3.max(sortedData, d => d.y)]).nice()
                    .range([0, innerWidth]);
    
                const yScale = d3.scaleBand()
                    .domain(sortedData.map(d => d.x))
                    .range([0, innerHeight])
                    .padding(0.2);
    
                // Ось X (внизу)
                const xAxis = d3.axisBottom(xScale);
                const xAxisGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
                xAxisGroup.setAttribute('transform', `translate(0,${innerHeight})`);
                g.appendChild(xAxisGroup);
                d3.select(xAxisGroup).call(xAxis);
    
                // Ось Y (слева)
                const yAxis = d3.axisLeft(yScale);
                const yAxisGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
                g.appendChild(yAxisGroup);
                d3.select(yAxisGroup).call(yAxis);
    
                // Столбцы (теперь горизонтальные)
                const bars = document.createElementNS('http://www.w3.org/2000/svg', 'g');
                g.appendChild(bars);
    
                d3.select(bars)
                    .selectAll('rect').data(sortedData).enter().append('rect').attr('y', d => yScale(d.x)).attr('x', 0).attr('height', yScale.bandwidth()).attr('width', d => xScale(d.y)).attr('fill', '#4caf50')
                    .attr('data-id', d => d.id)
                    .attr('cursor', 'pointer');
    
                svg.appendChild(g);
                chartContainer.appendChild(svg);
                container.appendChild(chartContainer);
    
                return Editor.generateHtml(container.outerHTML);
            },
            args: [config],
            libs: ['d3']
        }),
        events: {
            click: Editor.wrapFn({
                fn: function(event, config) {
                    const clickedId = event.target?.getAttribute('data-id');
                    if (!clickedId) return;
    
                    // Проверяем, кликнули ли по кнопке метрики
                    if (clickedId === 'primary' || clickedId === 'secondary') {
                        console.log('got')
                        // Обновляем параметры действия для фильтрации
                        Chart.setState({ selectedItem: clickedId });
                    }
                },
                args: [config]
            })
        },
        tooltip: {
            renderer: Editor.wrapFn({
                fn: function(event, config) {
                    const dataId = event.target?.getAttribute('data-id');
                    if (!dataId) return null;
    
                    // Проверяем, не является ли это кнопкой метрики
                    if (dataId === 'primary' || dataId === 'secondary') {
                        return null;
                    }
    
                    // Определяем, какая метрика выбрана
                    const state = Chart.getState() || {};
                    const selectedItem = state.selectedItem || config.selectedMetric;
                    const currentData = selectedItem === 'secondary' ? config.data.secondaryData : config.data.primaryData;
    
                    const item = currentData.find(d => d.id === dataId);
                    if (!item) return null;
    
                    return Editor.generateHtml(`
                        <div style="padding: 10px; font-family: sans-serif; font-size: 14px;">
                            <div><strong>Категория:</strong> ${item.x}</div>
                            <div><strong>Значение:</strong> ${item.y}</div>
                        </div>
                    `);
                },
                args: [config]
            })
        }
    };
    
    

    JS Selectors

    С помощью селекторов JS можно поддержать совершенно уникальные различные сценарии скрытия / показа селекторов, разрыв связей с селекторами, сортировку и многое другое. Начнем с простого — как с целом сделать JS селектор с Датасетом

    const { buildSource } = require('libs/dataset/v2');
    
    const params = Editor.getParams();
    
    // Для удобства все параметры переносим в переменные без params
    const selectors_date_interval = params.selectors_date_interval[0];
    const filter_fields_ids = params.filter_fields_ids['0'].split('|');
    const filter_fields_names = params.filter_fields_names['0'].split('|');
    
    const filterObject = filter_fields_names.reduce((acc, id, index) => {
        acc[id] = filter_fields_ids[index];
        return acc;
    }, {});
    
    const selector_aliases = params.selector_aliases['0'].split('|');
    
    function ensureArray(value) {
        return Array.isArray(value) ? value : [value];
    }
    
    // определение фильтра по дате - всегда добавляем в селект
    function getDateFilterPoP(params) {
        const date_dim = params.fast_date_dimension[0];
    
        const {from:from_date, to: to_date } = Editor.resolveInterval(selectors_date_interval);
        const right_date = to_date;
    
        let    dateFilter = {
                column: date_dim,
                operation: 'BETWEEN',
                values: [from_date, to_date],
            };
        return dateFilter;
    }
    
    
    // Массив для заполнения условий фильтрации
    const where = [];
    // Колонки для выбора
    const cols = [];
    // Параметры для передачи в датасет
    const filled_params = [];
    
    // 1) Добавляем фильтр дат для PoP (Period over Period) сравнения
    where.push(getDateFilterPoP(params));
    
    // Обрабатываем все параметры
    for (const [key, value] of Object.entries(params)) {
        // Добавляем все параметры из переданных и не пустых
        // Обрабатываем фильтры
        if (filter_fields_ids.includes(key) && value[0] !== '' && value.length != 0) {
            let val, suff;
    
            // Проверяем формат входных данных
            if (value[0] && value[0].substr(0, 2) === '__') {
                const {operation:suffx, value:vala} = Editor.resolveOperation(value);
                val = vala;
                suff = suffx;
    
            } else {
                val = value;
                suff = 'IN';
            }
            
            // Добавляем условие в массив where
            where.push({
                type:'id',
                column: key,
                operation: suff,
                values: ensureArray(val)
            });
        }
    
    }
    
    
    // Экспортируем настройки для buildSource
    let all_filters = {}
    selector_aliases.forEach((item, index) => {
        console.log(filterObject[item])
        all_filters[item] =
        buildSource({
            id: Editor.getId("dataset"),
            columns: [item],
            where: where.filter(object => {return object.column !== filterObject[item];}),
        })
    });
        
    
    console.log(all_filters);
    
    module.exports = all_filters;
    

    const Dataset = require('libs/dataset/v2');
    
    let params = Editor.getParams();
    
    let all_data = Editor.getLoadedData(); // Получаем загруженные данные;
    console.log(all_data)
    
    const commonSettings = {
        width: '100%',
        labelPlacement: 'top',
    };
    
    let selectors_shown = [];
    
    for (const [key, value] of Object.entries(all_data)) {
      let selector_arr = value.result_data[0].rows;
      let dataset_fields = value.fields;
      let selector_id = dataset_fields.find(item => item.title === key).id;
      selectors_shown.push(
        {
            type: 'select',
            param: selector_id,
            label: key,
            content: Object.values(selector_arr).map((value) => ({title: value.data[0], value: value.data[0]})),
            multiselect: true,
            searchable: true,
            ...commonSettings,
            updateOnChange:true
        }
      )
    }
    
    
    
    module.exports = selectors_shown;
    

    Добавить расчетное поле на вкладке sources

    // пустой массив расчетных полей 
    let upd = [];
    // добавляем поле с именем orField и туда в формулу прописываем что и как хотим
    upd.push({
        action:'add_field',
        field: {
            'title':"orField",
            data_type:'string',
            type:'MEASURE',
            formula:"isnull([region_delivery]) or [region_delivery] = 'Саратовская область'"
        }})
    // добавляем в where это поле как тру логическое
    const orFilter = {column: 'orField',
                            operation: 'EQ',
                        values: [true]}
    
    where.push(orFilter)
    module.exports = {
            dataset: buildSource({
                id: Editor.getId('dataset'),
                columns: cols,
                where: where,
                parameters:filled_params,
                limit:limit,
                // раздел updates
                updates:upd,
                order_by:[{'direction':'DESC','column':metrics_list[0]}]
            })
        }