lodash#clone TypeScript Examples

The following examples show how to use lodash#clone. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: gui-icon.ts    From S2 with MIT License 6 votes vote down vote up
private render() {
    const { name, fill } = this.cfg;
    const attrs = clone(this.cfg);
    const imageShapeAttrs: ShapeAttrs = {
      ...omit(attrs, 'fill'),
      type: GuiIcon.type,
    };
    const image = new Shape.Image({
      attrs: imageShapeAttrs,
    });

    const cacheKey = `${name}-${fill}`;
    const img = ImageCache[cacheKey];
    if (img) {
      // already in cache
      image.attr('img', img);
      this.addShape('image', image);
    } else {
      this.getImage(name, cacheKey, fill)
        .then((value: HTMLImageElement) => {
          image.attr('img', value);
          this.addShape('image', image);
        })
        .catch((err: Event) => {
          // eslint-disable-next-line no-console
          console.warn(`GuiIcon ${name} load error`, err);
        });
    }
    this.iconImageShape = image;
  }
Example #2
Source File: row-column-resize.ts    From S2 with MIT License 6 votes vote down vote up
private resizeMouseMove = (event: CanvasEvent) => {
    if (!this.resizeReferenceGroup?.get('visible')) {
      return;
    }
    event?.preventDefault?.();

    const originalEvent = event.originalEvent as MouseEvent;
    const resizeInfo = this.getResizeInfo();
    const resizeShapes = this.resizeReferenceGroup.get('children') as IShape[];

    if (isEmpty(resizeShapes)) {
      return;
    }

    const [, endGuideLineShape] = resizeShapes;
    const [guideLineStart, guideLineEnd]: ResizeGuideLinePath[] = clone(
      endGuideLineShape.attr('path'),
    );

    if (resizeInfo.type === ResizeDirectionType.Horizontal) {
      this.updateHorizontalResizingEndGuideLinePosition(
        originalEvent,
        resizeInfo,
        guideLineStart,
        guideLineEnd,
      );
    } else {
      this.updateVerticalResizingEndGuideLinePosition(
        originalEvent,
        resizeInfo,
        guideLineStart,
        guideLineEnd,
      );
    }
    endGuideLineShape.attr('path', [guideLineStart, guideLineEnd]);
  };
Example #3
Source File: pivot-sheet.ts    From S2 with MIT License 6 votes vote down vote up
public groupSortByMethod(sortMethod: SortMethod, meta: Node) {
    const { rows, columns } = this.dataCfg.fields;
    const ifHideMeasureColumn = this.options.style.colCfg.hideMeasureColumn;
    const sortFieldId = this.isValueInCols() ? last(rows) : last(columns);
    const { query, value } = meta;
    const sortQuery = clone(query);
    let sortValue = value;
    // 数值置于列头且隐藏了指标列头的情况, 会默认取第一个指标做组内排序, 需要还原指标列的query, 所以多指标时请不要这么用……
    if (ifHideMeasureColumn && this.isValueInCols()) {
      sortValue = this.dataSet.fields.values[0];
      sortQuery[EXTRA_FIELD] = sortValue;
    }

    const sortParam: SortParam = {
      sortFieldId,
      sortMethod,
      sortByMeasure: sortValue,
      query: sortQuery,
    };
    const prevSortParams = this.dataCfg.sortParams.filter(
      (item) => item?.sortFieldId !== sortFieldId,
    );
    // 触发排序事件
    this.emit(S2Event.RANGE_SORT, [...prevSortParams, sortParam]);
    this.setDataCfg({
      ...this.dataCfg,
      sortParams: [...prevSortParams, sortParam],
    });
    this.render();
  }
Example #4
Source File: index.tsx    From next-basics with GNU General Public License v3.0 6 votes vote down vote up
_updateValues = (values: any) => {
    let cloneData = this.value && this.value.length ? clone(this.value) : [];
    if (this._isEdit) {
      cloneData[this._editIndex] = values;
    } else {
      cloneData = [...cloneData, values];
    }
    this.value = cloneData;

    this._close();
    this._handleChange();
    this._updateAddBtnDisabled();
  };
Example #5
Source File: drill-down.ts    From S2 with MIT License 5 votes vote down vote up
handleDrillDown = (params: DrillDownParams) => {
  const { fetchData, spreadsheet, drillFields, drillItemsNum } = params;
  spreadsheet.store.set('drillItemsNum', drillItemsNum);
  const meta = spreadsheet.store.get('drillDownNode');
  const { drillDownDataCache, drillDownCurrentCache } = getDrillDownCache(
    spreadsheet,
    meta,
  );
  let newDrillDownDataCache = clone(drillDownDataCache);
  // 如果当前节点已有下钻缓存,需要清除
  if (drillDownCurrentCache) {
    newDrillDownDataCache = filter(
      drillDownDataCache,
      (cache) => cache.rowId !== meta.id,
    );
  }
  fetchData(meta, drillFields).then((info) => {
    const { drillData, drillField } = info;
    (spreadsheet.dataSet as PivotDataSet).transformDrillDownData(
      drillField,
      drillData,
      meta,
    );

    if (!isEmpty(drillData)) {
      // 缓存到表实例中
      const drillLevel = meta.level + 1;
      const newDrillDownData = {
        rowId: meta.id,
        drillLevel,
        drillData,
        drillField,
      };
      newDrillDownDataCache.push(newDrillDownData);
      spreadsheet.store.set('drillDownDataCache', newDrillDownDataCache);
    }

    // 重置当前交互
    spreadsheet.interaction.reset();
    spreadsheet.render(false);
  });
}
Example #6
Source File: index.tsx    From next-basics with GNU General Public License v3.0 5 votes vote down vote up
_deleteItem(): void {
    const data = clone(this.value);
    pullAt(data, this._editIndex);
    this.value = data;
    this._updateAddBtnDisabled();
  }
Example #7
Source File: get-sorted-nodes.ts    From prettier-plugin-sort-imports with Apache License 2.0 4 votes vote down vote up
getSortedNodes: GetSortedNodes = (nodes, options) => {
    naturalSort.insensitive = options.importOrderCaseInsensitive;

    let { importOrder } = options;
    const {
        importOrderSeparation,
        importOrderSortSpecifiers,
        importOrderGroupNamespaceSpecifiers,
    } = options;

    const originalNodes = nodes.map(clone);
    const finalNodes: ImportOrLine[] = [];

    if (!importOrder.includes(THIRD_PARTY_MODULES_SPECIAL_WORD)) {
        importOrder = [THIRD_PARTY_MODULES_SPECIAL_WORD, ...importOrder];
    }

    const importOrderGroups = importOrder.reduce<ImportGroups>(
        (groups, regexp) => ({
            ...groups,
            [regexp]: [],
        }),
        {},
    );

    const importOrderWithOutThirdPartyPlaceholder = importOrder.filter(
        (group) => group !== THIRD_PARTY_MODULES_SPECIAL_WORD,
    );

    for (const node of originalNodes) {
        const matchedGroup = getImportNodesMatchedGroup(
            node,
            importOrderWithOutThirdPartyPlaceholder,
        );
        importOrderGroups[matchedGroup].push(node);
    }

    for (const group of importOrder) {
        const groupNodes = importOrderGroups[group];

        if (groupNodes.length === 0) continue;

        const sortedInsideGroup = getSortedNodesGroup(groupNodes, {
            importOrderGroupNamespaceSpecifiers,
        });

        // Sort the import specifiers
        if (importOrderSortSpecifiers) {
            sortedInsideGroup.forEach((node) =>
                getSortedImportSpecifiers(node),
            );
        }

        finalNodes.push(...sortedInsideGroup);

        if (importOrderSeparation) {
            finalNodes.push(newLineNode);
        }
    }

    if (finalNodes.length > 0 && !importOrderSeparation) {
        // a newline after all imports
        finalNodes.push(newLineNode);
    }

    // maintain a copy of the nodes to extract comments from
    const finalNodesClone = finalNodes.map(clone);

    const firstNodesComments = nodes[0].leadingComments;

    // Remove all comments from sorted nodes
    finalNodes.forEach(removeComments);

    // insert comments other than the first comments
    finalNodes.forEach((node, index) => {
        if (isEqual(nodes[0].loc, node.loc)) return;

        addComments(
            node,
            'leading',
            finalNodesClone[index].leadingComments || [],
        );
    });

    if (firstNodesComments) {
        addComments(finalNodes[0], 'leading', firstNodesComments);
    }

    return finalNodes;
}
Example #8
Source File: resolver.ts    From one-platform with MIT License 4 votes vote down vote up
AppsResolver = <IResolvers<App, IAppsContext>> {
  Query: {
    apps: () => Apps.find().exec(),
    myApps: (parent, args, { rhatUUID }) => {
      if (!rhatUUID) {
        throw new Error('Anonymous user unauthorized to view my apps');
      }
      return Apps.find({ ownerId: rhatUUID }).exec();
    },
    findApps: (parent, { selectors }) => {
      const appSelector = selectors;
      // eslint-disable-next-line @typescript-eslint/naming-convention
      const _id = appSelector.id;
      delete appSelector.id;

      return Apps.find({
        ...selectors,
        ...(_id && { _id }),
      }).exec();
    },
    app: (parent, { id, appId }) => {
      if (!id && !appId) {
        throw new Error('Please provide atleast one argument for id or appId');
      }
      return Apps.findOne({
        ...(appId && { appId }),
        ...(id && { _id: id }),
      }).exec();
    },
  },
  Mutation: {
    createApp: async (parent, { app }, ctx) => {
      if (!ctx.rhatUUID) {
        throw new Error('Anonymous user unauthorized to create new app');
      }
      const newApp = await new Apps({
        ...app,
        name: app.name.trim(),
        description: app.description.trim(),
        ownerId: ctx.rhatUUID,
        createdBy: ctx.rhatUUID,
        updatedBy: ctx.rhatUUID,
      }).save();

      if (newApp) {
        const transformedData = AppsHelper.formatSearchInput(newApp);
        AppsHelper.manageSearchIndex(transformedData, 'index');
      }

      return newApp;
    },
    updateApp: async (parent, { id, app }, ctx) => {
      const appRecord = app;
      if (!Apps.isAuthorized(id, ctx.rhatUUID)) {
        throw new Error('User unauthorized to update the app');
      }
      if (appRecord.path) {
        appRecord.appId = uniqueIdFromPath(app.path);
      }
      const updatedApp = await Apps
        .findByIdAndUpdate(id, {
          ...appRecord,
          updatedBy: ctx.rhatUUID,
          updatedOn: new Date(),
        }, { new: true })
        .exec();

      if (updatedApp) {
        const transformedData = AppsHelper.formatSearchInput(updatedApp);
        AppsHelper.manageSearchIndex(transformedData, 'index');
      }

      return updatedApp;
    },
    deleteApp: async (parent, { id }, ctx) => {
      if (!Apps.isAuthorized(id, ctx.rhatUUID)) {
        throw new Error('User unauthorized to delete the app');
      }
      const app = await Apps.findByIdAndRemove(id).exec();

      if (app) {
        const input = {
          dataSource: 'oneportal',
          documents: [{ id: app._id }],
        };
        AppsHelper.manageSearchIndex(input, 'delete');
      }

      return app;
    },
    transferAppOwnership: async (parent, { id, ownerId }, { rhatUUID }) => {
      if (!Apps.isAuthorized(id, rhatUUID)) {
        throw new Error('User unauthorized to transfer ownership');
      }
      const app = await Apps.findById(id).exec();
      if (!app) {
        throw new Error(`App not found for id: "${id}"`);
      }
      if (app.ownerId !== rhatUUID) {
        throw new Error('User not authorized to transfer ownership.');
      }
      if (app.ownerId === ownerId) {
        return app;
      }

      const owner = await getUser(app.ownerId);
      const newOwner = await getUser(ownerId);
      if (!owner || !newOwner) {
        throw new Error('There was some problem. Please try again.');
      }

      let permissions = clone(app.permissions);
      /* Make the previous owner an editor */
      permissions.unshift({
        name: owner.name,
        email: owner.email,
        refId: owner.uuid,
        refType: App.PermissionsRefType.USER,
        role: App.PermissionsRole.EDIT,
      });
      /* Remove duplicates, if any */
      permissions = permissions
        .filter((permission, index, arr) => (
          arr.findIndex((perm) => perm.refId === permission.refId) === index
          && permission.refId !== ownerId
        ));

      return Apps.findByIdAndUpdate(id, {
        ownerId,
        permissions,
        updatedOn: new Date(),
        updatedBy: rhatUUID,
      }, { new: true }).exec();
    },
    createAppDatabase: async (parent, {
      id, databaseName, description, permissions,
    }, { rhatUUID }) => {
      if (!Apps.isAuthorized(id, rhatUUID)) {
        throw new Error('User unauthorized to create database for the app');
      }

      const app = await Apps.findById(id);
      if (!app) {
        throw new Error('App not found');
      }
      const database = {
        name: databaseName,
        description,
        permissions,
      };

      if (isEmpty(database.permissions)) {
        /* TODO: Add default users from the app permission model */
        database.permissions = { admins: [`user:${app.ownerId}`], users: [`user:${app.ownerId}`, 'op-users'] };
      }

      try {
        /* Create the database on couchdb */
        await createDatabase(database.name);
        /* Set the default permissions */
        await setDatabasePermissions(database.name, database.permissions);
      } catch (err) {
        logger.error('[CouchDB Error]:', JSON.stringify(err));
        throw new Error(`Database could not be created: ${JSON.stringify(err)}`);
      }

      const databaseConfig = {
        isEnabled: app.database.isEnabled,
        databases: [
          ...app.database.databases.filter((db) => db.name !== databaseName),
          database,
        ],
      };
      return Apps.findByIdAndUpdate(app.id, { database: databaseConfig }, { new: true }).exec();
    },
    deleteAppDatabase: async (parent, { id, databaseName }, { rhatUUID }) => {
      if (!Apps.isAuthorized(id, rhatUUID)) {
        throw new Error('User unauthorized to delete database from the app');
      }

      const app = await Apps.findById(id);
      if (!app) {
        throw new Error('App not found');
      }

      try {
        /* Delete the database from couchdb */
        await deleteDatabase(databaseName);
      } catch (err) {
        logger.error('[CouchDB Error]:', JSON.stringify(err));
        throw new Error(`Database could not be deleted: ${JSON.stringify(err)}`);
      }

      const databaseConfig = app.database;
      databaseConfig.databases = databaseConfig.databases.filter((db) => db.name !== databaseName);
      return Apps.findByIdAndUpdate(app.id, { database: databaseConfig }, { new: true }).exec();
    },
    manageAppDatabase: async (parent, {
      id, databaseName, description, permissions,
    }, { rhatUUID }) => {
      if (!Apps.isAuthorized(id, rhatUUID)) {
        throw new Error('User unauthorized to manage the database');
      }

      if (!description && isEmpty(permissions)) {
        throw new Error('Provide at least one field: "description" or "permissions".');
      }

      const app = await Apps.findById(id);
      if (!app) {
        throw new Error('App not found');
      }

      const databaseConfig = app.database;
      const dbIndex = app.database.databases.findIndex((db) => db.name === databaseName);
      if (dbIndex === -1) {
        throw new Error(`The database "${databaseName}" does not exist for the given app.`);
      }

      if (description) {
        databaseConfig.databases[dbIndex].description = description;
      }

      if (permissions) {
        await setDatabasePermissions(databaseConfig.databases[dbIndex].name, permissions);
        databaseConfig.databases[dbIndex].permissions = permissions;
      }

      return Apps.findByIdAndUpdate(app.id, { database: databaseConfig }, { new: true }).exec();
    },
  },
}
Example #9
Source File: paginate.spec.ts    From nestjs-paginate with MIT License 4 votes vote down vote up
describe('paginate', () => {
    let connection: Connection
    let catRepo: Repository<CatEntity>
    let catToyRepo: Repository<CatToyEntity>
    let catHomeRepo: Repository<CatHomeEntity>
    let cats: CatEntity[]
    let catToys: CatToyEntity[]
    let catHomes: CatHomeEntity[]

    beforeAll(async () => {
        connection = await createConnection({
            type: 'sqlite',
            database: ':memory:',
            synchronize: true,
            logging: false,
            entities: [CatEntity, CatToyEntity, CatHomeEntity],
        })
        catRepo = connection.getRepository(CatEntity)
        catToyRepo = connection.getRepository(CatToyEntity)
        catHomeRepo = connection.getRepository(CatHomeEntity)
        cats = await catRepo.save([
            catRepo.create({ name: 'Milo', color: 'brown', age: 6 }),
            catRepo.create({ name: 'Garfield', color: 'ginger', age: 5 }),
            catRepo.create({ name: 'Shadow', color: 'black', age: 4 }),
            catRepo.create({ name: 'George', color: 'white', age: 3 }),
            catRepo.create({ name: 'Leche', color: 'white', age: null }),
        ])
        catToys = await catToyRepo.save([
            catToyRepo.create({ name: 'Fuzzy Thing', cat: cats[0] }),
            catToyRepo.create({ name: 'Stuffed Mouse', cat: cats[0] }),
            catToyRepo.create({ name: 'Mouse', cat: cats[0] }),
            catToyRepo.create({ name: 'String', cat: cats[1] }),
        ])
        catHomes = await catHomeRepo.save([
            catHomeRepo.create({ name: 'Box', cat: cats[0] }),
            catHomeRepo.create({ name: 'House', cat: cats[1] }),
        ])
    })

    it('should return an instance of Paginated', async () => {
        const config: PaginateConfig<CatEntity> = {
            sortableColumns: ['id'],
            defaultSortBy: [['id', 'ASC']],
            defaultLimit: 1,
        }
        const query: PaginateQuery = {
            path: '',
        }

        const result = await paginate<CatEntity>(query, catRepo, config)

        expect(result).toBeInstanceOf(Paginated)
        expect(result.data).toStrictEqual(cats.slice(0, 1))
    })

    it('should accept a query builder', async () => {
        const config: PaginateConfig<CatEntity> = {
            sortableColumns: ['id'],
            defaultSortBy: [['id', 'ASC']],
            defaultLimit: 1,
        }
        const query: PaginateQuery = {
            path: '',
        }

        const queryBuilder = await catRepo.createQueryBuilder('cats')

        const result = await paginate<CatEntity>(query, queryBuilder, config)

        expect(result.data).toStrictEqual(cats.slice(0, 1))
    })

    it('should accept a query builder with custom condition', async () => {
        const config: PaginateConfig<CatEntity> = {
            sortableColumns: ['id'],
            defaultSortBy: [['id', 'ASC']],
            defaultLimit: 1,
        }
        const query: PaginateQuery = {
            path: '',
        }

        const queryBuilder = await connection
            .createQueryBuilder()
            .select('cats')
            .from(CatEntity, 'cats')
            .where('cats.color = :color', { color: 'white' })

        const result = await paginate<CatEntity>(query, queryBuilder, config)

        expect(result.data).toStrictEqual(cats.slice(3, 4))
    })

    it('should default to page 1, if negative page is given', async () => {
        const config: PaginateConfig<CatEntity> = {
            sortableColumns: ['id'],
            defaultLimit: 1,
        }
        const query: PaginateQuery = {
            path: '',
            page: -1,
        }

        const result = await paginate<CatEntity>(query, catRepo, config)

        expect(result.meta.currentPage).toBe(1)
        expect(result.data).toStrictEqual(cats.slice(0, 1))
    })

    it('should default to limit maxLimit, if more than maxLimit is given', async () => {
        const config: PaginateConfig<CatEntity> = {
            sortableColumns: ['id'],
            defaultLimit: 5,
            maxLimit: 2,
        }
        const query: PaginateQuery = {
            path: '',
            page: 1,
            limit: 20,
        }

        const result = await paginate<CatEntity>(query, catRepo, config)

        expect(result.data).toStrictEqual(cats.slice(0, 2))
    })

    it('should return correct links for some results', async () => {
        const config: PaginateConfig<CatEntity> = {
            sortableColumns: ['id'],
        }
        const query: PaginateQuery = {
            path: '',
            page: 2,
            limit: 2,
        }

        const { links } = await paginate<CatEntity>(query, catRepo, config)

        expect(links.first).toBe('?page=1&limit=2&sortBy=id:ASC')
        expect(links.previous).toBe('?page=1&limit=2&sortBy=id:ASC')
        expect(links.current).toBe('?page=2&limit=2&sortBy=id:ASC')
        expect(links.next).toBe('?page=3&limit=2&sortBy=id:ASC')
        expect(links.last).toBe('?page=3&limit=2&sortBy=id:ASC')
    })

    it('should return only current link if zero results', async () => {
        const config: PaginateConfig<CatEntity> = {
            sortableColumns: ['id'],
            searchableColumns: ['name'],
        }
        const query: PaginateQuery = {
            path: '',
            page: 1,
            limit: 2,
            search: 'Pluto',
        }

        const { links } = await paginate<CatEntity>(query, catRepo, config)

        expect(links.first).toBe(undefined)
        expect(links.previous).toBe(undefined)
        expect(links.current).toBe('?page=1&limit=2&sortBy=id:ASC&search=Pluto')
        expect(links.next).toBe(undefined)
        expect(links.last).toBe(undefined)
    })

    it('should default to defaultSortBy if query sortBy does not exist', async () => {
        const config: PaginateConfig<CatEntity> = {
            sortableColumns: ['id', 'createdAt'],
            defaultSortBy: [['id', 'DESC']],
        }
        const query: PaginateQuery = {
            path: '',
        }

        const result = await paginate<CatEntity>(query, catRepo, config)

        expect(result.meta.sortBy).toStrictEqual([['id', 'DESC']])
        expect(result.data).toStrictEqual(cats.slice(0).reverse())
    })

    it('should sort result by multiple columns', async () => {
        const config: PaginateConfig<CatEntity> = {
            sortableColumns: ['name', 'color'],
        }
        const query: PaginateQuery = {
            path: '',
            sortBy: [
                ['color', 'DESC'],
                ['name', 'ASC'],
            ],
        }

        const result = await paginate<CatEntity>(query, catRepo, config)

        expect(result.meta.sortBy).toStrictEqual([
            ['color', 'DESC'],
            ['name', 'ASC'],
        ])
        expect(result.data).toStrictEqual([cats[3], cats[4], cats[1], cats[0], cats[2]])
    })

    it('should return result based on search term', async () => {
        const config: PaginateConfig<CatEntity> = {
            sortableColumns: ['id', 'name', 'color'],
            searchableColumns: ['name', 'color'],
        }
        const query: PaginateQuery = {
            path: '',
            search: 'i',
        }

        const result = await paginate<CatEntity>(query, catRepo, config)

        expect(result.meta.search).toStrictEqual('i')
        expect(result.data).toStrictEqual([cats[0], cats[1], cats[3], cats[4]])
        expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&search=i')
    })

    it('should return result based on search term on many-to-one relation', async () => {
        const config: PaginateConfig<CatToyEntity> = {
            relations: ['cat'],
            sortableColumns: ['id', 'name'],
            searchableColumns: ['name', 'cat.name'],
        }
        const query: PaginateQuery = {
            path: '',
            search: 'Milo',
        }

        const result = await paginate<CatToyEntity>(query, catToyRepo, config)

        expect(result.meta.search).toStrictEqual('Milo')
        expect(result.data).toStrictEqual([catToys[0], catToys[1], catToys[2]])
        expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&search=Milo')
    })

    it('should return result based on search term on one-to-many relation', async () => {
        const config: PaginateConfig<CatEntity> = {
            relations: ['toys'],
            sortableColumns: ['id', 'name'],
            searchableColumns: ['name', 'toys.name'],
        }
        const query: PaginateQuery = {
            path: '',
            search: 'Mouse',
        }

        const result = await paginate<CatEntity>(query, catRepo, config)

        expect(result.meta.search).toStrictEqual('Mouse')
        const toy = clone(catToys[1])
        delete toy.cat
        const toy2 = clone(catToys[2])
        delete toy2.cat
        expect(result.data).toStrictEqual([Object.assign(clone(cats[0]), { toys: [toy, toy2] })])
        expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&search=Mouse')
    })

    it('should return result based on search term on one-to-one relation', async () => {
        const config: PaginateConfig<CatHomeEntity> = {
            relations: ['cat'],
            sortableColumns: ['id', 'name', 'cat.id'],
        }
        const query: PaginateQuery = {
            path: '',
            sortBy: [['cat.id', 'DESC']],
        }

        const result = await paginate<CatHomeEntity>(query, catHomeRepo, config)
        expect(result.meta.sortBy).toStrictEqual([['cat.id', 'DESC']])
        expect(result.data).toStrictEqual([catHomes[0], catHomes[1]].sort((a, b) => b.cat.id - a.cat.id))
        expect(result.links.current).toBe('?page=1&limit=20&sortBy=cat.id:DESC')
    })

    it('should return result based on sort and search on many-to-one relation', async () => {
        const config: PaginateConfig<CatToyEntity> = {
            relations: ['cat'],
            sortableColumns: ['id', 'name', 'cat.id'],
            searchableColumns: ['name', 'cat.name'],
        }
        const query: PaginateQuery = {
            path: '',
            sortBy: [['cat.id', 'DESC']],
            search: 'Milo',
        }

        const result = await paginate<CatToyEntity>(query, catToyRepo, config)

        expect(result.meta.search).toStrictEqual('Milo')
        expect(result.data).toStrictEqual([catToys[0], catToys[1], catToys[2]].sort((a, b) => b.cat.id - a.cat.id))
        expect(result.links.current).toBe('?page=1&limit=20&sortBy=cat.id:DESC&search=Milo')
    })

    it('should return result based on sort on one-to-many relation', async () => {
        const config: PaginateConfig<CatEntity> = {
            relations: ['toys'],
            sortableColumns: ['id', 'name', 'toys.id'],
            searchableColumns: ['name', 'toys.name'],
        }
        const query: PaginateQuery = {
            path: '',
            sortBy: [['toys.id', 'DESC']],
            search: 'Mouse',
        }

        const result = await paginate<CatEntity>(query, catRepo, config)

        expect(result.meta.search).toStrictEqual('Mouse')
        const toy1 = clone(catToys[1])
        delete toy1.cat
        const toy2 = clone(catToys[2])
        delete toy2.cat
        expect(result.data).toStrictEqual([Object.assign(clone(cats[0]), { toys: [toy2, toy1] })])
        expect(result.links.current).toBe('?page=1&limit=20&sortBy=toys.id:DESC&search=Mouse')
    })

    it('should return result based on sort on one-to-one relation', async () => {
        const config: PaginateConfig<CatHomeEntity> = {
            relations: ['cat'],
            sortableColumns: ['id', 'name'],
            searchableColumns: ['name', 'cat.name'],
        }
        const query: PaginateQuery = {
            path: '',
            search: 'Garfield',
        }

        const result = await paginate<CatHomeEntity>(query, catHomeRepo, config)

        expect(result.meta.search).toStrictEqual('Garfield')
        expect(result.data).toStrictEqual([catHomes[1]])
        expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&search=Garfield')
    })

    it('should return result based on search term and searchBy columns', async () => {
        const config: PaginateConfig<CatEntity> = {
            sortableColumns: ['id', 'name', 'color'],
            searchableColumns: ['name', 'color'],
        }

        const searchTerm = 'white'
        const expectedResultData = cats.filter((cat: CatEntity) => cat.color === searchTerm)

        const query: PaginateQuery = {
            path: '',
            search: searchTerm,
            searchBy: ['color'],
        }

        const result = await paginate<CatEntity>(query, catRepo, config)

        expect(result.meta.search).toStrictEqual(searchTerm)
        expect(result.meta.searchBy).toStrictEqual(['color'])
        expect(result.data).toStrictEqual(expectedResultData)
        expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&search=white&searchBy=color')
    })

    it('should return result based on where config and filter', async () => {
        const config: PaginateConfig<CatEntity> = {
            sortableColumns: ['id'],
            where: {
                color: 'white',
            },
            filterableColumns: {
                name: [FilterOperator.NOT],
            },
        }
        const query: PaginateQuery = {
            path: '',
            filter: {
                name: '$not:Leche',
            },
        }

        const result = await paginate<CatEntity>(query, catRepo, config)

        expect(result.meta.filter).toStrictEqual({
            name: '$not:Leche',
        })
        expect(result.data).toStrictEqual([cats[3]])
        expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.name=$not:Leche')
    })

    it('should return result based on filter on many-to-one relation', async () => {
        const config: PaginateConfig<CatToyEntity> = {
            relations: ['cat'],
            sortableColumns: ['id', 'name'],
            filterableColumns: {
                'cat.name': [FilterOperator.NOT],
            },
        }
        const query: PaginateQuery = {
            path: '',
            filter: {
                'cat.name': '$not:Milo',
            },
        }

        const result = await paginate<CatToyEntity>(query, catToyRepo, config)

        expect(result.meta.filter).toStrictEqual({
            'cat.name': '$not:Milo',
        })
        expect(result.data).toStrictEqual([catToys[3]])
        expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.cat.name=$not:Milo')
    })

    it('should return result based on filter on one-to-many relation', async () => {
        const config: PaginateConfig<CatEntity> = {
            relations: ['toys'],
            sortableColumns: ['id', 'name'],
            filterableColumns: {
                'toys.name': [FilterOperator.NOT],
            },
        }
        const query: PaginateQuery = {
            path: '',
            filter: {
                'toys.name': '$not:Stuffed Mouse',
            },
        }

        const result = await paginate<CatEntity>(query, catRepo, config)

        const cat1 = clone(cats[0])
        const cat2 = clone(cats[1])
        const catToys1 = clone(catToys[0])
        const catToys2 = clone(catToys[2])
        const catToys3 = clone(catToys[3])
        delete catToys1.cat
        delete catToys2.cat
        delete catToys3.cat
        cat1.toys = [catToys1, catToys2]
        cat2.toys = [catToys3]

        expect(result.meta.filter).toStrictEqual({
            'toys.name': '$not:Stuffed Mouse',
        })
        expect(result.data).toStrictEqual([cat1, cat2])
        expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.toys.name=$not:Stuffed Mouse')
    })

    it('should return result based on filter on one-to-one relation', async () => {
        const config: PaginateConfig<CatHomeEntity> = {
            relations: ['cat'],
            sortableColumns: ['id', 'name'],
            filterableColumns: {
                'cat.name': [FilterOperator.NOT],
            },
        }
        const query: PaginateQuery = {
            path: '',
            filter: {
                'cat.name': '$not:Garfield',
            },
        }

        const result = await paginate<CatHomeEntity>(query, catHomeRepo, config)

        expect(result.meta.filter).toStrictEqual({
            'cat.name': '$not:Garfield',
        })
        expect(result.data).toStrictEqual([catHomes[0]])
        expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.cat.name=$not:Garfield')
    })

    it('should return result based on $in filter on one-to-one relation', async () => {
        const config: PaginateConfig<CatHomeEntity> = {
            relations: ['cat'],
            sortableColumns: ['id', 'name'],
            filterableColumns: {
                'cat.age': [FilterOperator.IN],
            },
        }
        const query: PaginateQuery = {
            path: '',
            filter: {
                'cat.age': '$in:4,6',
            },
        }

        const result = await paginate<CatHomeEntity>(query, catHomeRepo, config)

        expect(result.meta.filter).toStrictEqual({
            'cat.age': '$in:4,6',
        })
        expect(result.data).toStrictEqual([catHomes[0]])
        expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.cat.age=$in:4,6')
    })

    it('should return result based on $btw filter on one-to-one relation', async () => {
        const config: PaginateConfig<CatHomeEntity> = {
            relations: ['cat'],
            sortableColumns: ['id', 'name'],
            filterableColumns: {
                createdAt: [FilterOperator.BTW],
            },
        }
        const query: PaginateQuery = {
            path: '',
            filter: {
                'cat.age': '$btw:6,10',
            },
        }

        const result = await paginate<CatHomeEntity>(query, catHomeRepo, config)

        expect(result.meta.filter).toStrictEqual({
            'cat.age': '$btw:6,10',
        })
        expect(result.data).toStrictEqual([catHomes[0], catHomes[1]])
        expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.cat.age=$btw:6,10')
    })

    it('should return result based on where array and filter', async () => {
        const config: PaginateConfig<CatEntity> = {
            sortableColumns: ['id'],
            where: [
                {
                    color: 'white',
                },
                {
                    age: 4,
                },
            ],
            filterableColumns: {
                name: [FilterOperator.NOT],
            },
        }
        const query: PaginateQuery = {
            path: '',
            filter: {
                name: '$not:Leche',
            },
        }

        const result = await paginate<CatEntity>(query, catRepo, config)

        expect(result.meta.filter).toStrictEqual({
            name: '$not:Leche',
        })
        expect(result.data).toStrictEqual([cats[2], cats[3]])
        expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.name=$not:Leche')
    })

    it('should return result based on multiple filter', async () => {
        const config: PaginateConfig<CatEntity> = {
            sortableColumns: ['id'],
            filterableColumns: {
                name: [FilterOperator.NOT],
                color: [FilterOperator.EQ],
            },
        }
        const query: PaginateQuery = {
            path: '',
            filter: {
                name: '$not:Leche',
                color: 'white',
            },
        }

        const result = await paginate<CatEntity>(query, catRepo, config)

        expect(result.meta.filter).toStrictEqual({
            name: '$not:Leche',
            color: 'white',
        })
        expect(result.data).toStrictEqual([cats[3]])
        expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.name=$not:Leche&filter.color=white')
    })

    it('should return result based on filter and search term', async () => {
        const config: PaginateConfig<CatEntity> = {
            sortableColumns: ['id'],
            searchableColumns: ['name', 'color'],
            filterableColumns: {
                id: [FilterOperator.NOT, FilterOperator.IN],
            },
        }
        const query: PaginateQuery = {
            path: '',
            search: 'white',
            filter: {
                id: '$not:$in:1,2,5',
            },
        }

        const result = await paginate<CatEntity>(query, catRepo, config)

        expect(result.meta.search).toStrictEqual('white')
        expect(result.meta.filter).toStrictEqual({ id: '$not:$in:1,2,5' })
        expect(result.data).toStrictEqual([cats[3]])
        expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&search=white&filter.id=$not:$in:1,2,5')
    })

    it('should return result based on filter and where config', async () => {
        const config: PaginateConfig<CatEntity> = {
            sortableColumns: ['id'],
            where: {
                color: In(['black', 'white']),
            },
            filterableColumns: {
                id: [FilterOperator.NOT, FilterOperator.IN],
            },
        }
        const query: PaginateQuery = {
            path: '',
            filter: {
                id: '$not:$in:1,2,5',
            },
        }

        const result = await paginate<CatEntity>(query, catRepo, config)

        expect(result.data).toStrictEqual([cats[2], cats[3]])
        expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.id=$not:$in:1,2,5')
    })

    it('should return result based on range filter', async () => {
        const config: PaginateConfig<CatEntity> = {
            sortableColumns: ['id'],
            filterableColumns: {
                age: [FilterOperator.GTE],
            },
        }
        const query: PaginateQuery = {
            path: '',
            filter: {
                age: '$gte:4',
            },
        }

        const result = await paginate<CatEntity>(query, catRepo, config)

        expect(result.data).toStrictEqual([cats[0], cats[1], cats[2]])
        expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.age=$gte:4')
    })

    it('should return result based on between range filter', async () => {
        const config: PaginateConfig<CatEntity> = {
            sortableColumns: ['id'],
            filterableColumns: {
                age: [FilterOperator.BTW],
            },
        }
        const query: PaginateQuery = {
            path: '',
            filter: {
                age: '$btw:4,5',
            },
        }

        const result = await paginate<CatEntity>(query, catRepo, config)

        expect(result.data).toStrictEqual([cats[1], cats[2]])
        expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.age=$btw:4,5')
    })

    it('should return result based on is null query', async () => {
        const config: PaginateConfig<CatEntity> = {
            sortableColumns: ['id'],
            filterableColumns: {
                age: [FilterOperator.NULL],
            },
        }
        const query: PaginateQuery = {
            path: '',
            filter: {
                age: '$null',
            },
        }

        const result = await paginate<CatEntity>(query, catRepo, config)

        expect(result.data).toStrictEqual([cats[4]])
        expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.age=$null')
    })

    it('should return result based on not null query', async () => {
        const config: PaginateConfig<CatEntity> = {
            sortableColumns: ['id'],
            filterableColumns: {
                age: [FilterOperator.NOT, FilterOperator.NULL],
            },
        }
        const query: PaginateQuery = {
            path: '',
            filter: {
                age: '$not:$null',
            },
        }

        const result = await paginate<CatEntity>(query, catRepo, config)

        expect(result.data).toStrictEqual([cats[0], cats[1], cats[2], cats[3]])
        expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.age=$not:$null')
    })

    it('should ignore filterable column which is not configured', async () => {
        const config: PaginateConfig<CatEntity> = {
            sortableColumns: ['id'],
            filterableColumns: {
                name: [FilterOperator.NOT, FilterOperator.NULL],
            },
        }
        const query: PaginateQuery = {
            path: '',
            filter: {
                age: '$not:$null',
            },
        }

        const result = await paginate<CatEntity>(query, catRepo, config)

        expect(result.data).toStrictEqual(cats)
        expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.age=$not:$null')
    })

    it('should ignore filter operator which is not configured', async () => {
        const config: PaginateConfig<CatEntity> = {
            sortableColumns: ['id'],
            filterableColumns: {
                age: [FilterOperator.NOT],
            },
        }
        const query: PaginateQuery = {
            path: '',
            filter: {
                age: '$not:$null',
            },
        }

        const result = await paginate<CatEntity>(query, catRepo, config)

        expect(result.data).toStrictEqual(cats)
        expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&filter.age=$not:$null')
    })

    it('should throw an error when no sortableColumns', async () => {
        const config: PaginateConfig<CatEntity> = {
            sortableColumns: [],
        }
        const query: PaginateQuery = {
            path: '',
        }

        try {
            await paginate<CatEntity>(query, catRepo, config)
        } catch (err) {
            expect(err).toBeInstanceOf(HttpException)
        }
    })

    it.each([
        { operator: '$eq', result: true },
        { operator: '$gte', result: true },
        { operator: '$gt', result: true },
        { operator: '$in', result: true },
        { operator: '$null', result: true },
        { operator: '$lt', result: true },
        { operator: '$lte', result: true },
        { operator: '$btw', result: true },
        { operator: '$not', result: true },
        { operator: '$fake', result: false },
    ])('should check operator "$operator" valid is $result', ({ operator, result }) => {
        expect(isOperator(operator)).toStrictEqual(result)
    })

    it.each([
        { operator: '$eq', name: 'Equal' },
        { operator: '$gt', name: 'MoreThan' },
        { operator: '$gte', name: 'MoreThanOrEqual' },
        { operator: '$in', name: 'In' },
        { operator: '$null', name: 'IsNull' },
        { operator: '$lt', name: 'LessThan' },
        { operator: '$lte', name: 'LessThanOrEqual' },
        { operator: '$btw', name: 'Between' },
        { operator: '$not', name: 'Not' },
    ])('should get operator function $name for "$operator"', ({ operator, name }) => {
        const func = OperatorSymbolToFunction.get(operator as FilterOperator)
        expect(func.name).toStrictEqual(name)
    })

    it.each([
        { string: '$eq:value', tokens: [null, '$eq', 'value'] },
        { string: '$eq:val:ue', tokens: [null, '$eq', 'val:ue'] },
        { string: '$in:value1,value2,value3', tokens: [null, '$in', 'value1,value2,value3'] },
        { string: '$not:$in:value1:a,value2:b,value3:c', tokens: ['$not', '$in', 'value1:a,value2:b,value3:c'] },
        { string: 'value', tokens: [null, '$eq', 'value'] },
        { string: 'val:ue', tokens: [null, '$eq', 'val:ue'] },
        { string: '$not:value', tokens: [null, '$not', 'value'] },
        { string: '$eq:$not:value', tokens: ['$eq', '$not', 'value'] },
        { string: '$eq:$null', tokens: ['$eq', '$null'] },
        { string: '$null', tokens: [null, '$null'] },
        { string: '', tokens: [null, '$eq', ''] },
        { string: '$eq:$not:$in:value', tokens: [] },
    ])('should get filter tokens for "$string"', ({ string, tokens }) => {
        expect(getFilterTokens(string)).toStrictEqual(tokens)
    })

    it('should return all items even if deleted', async () => {
        const config: PaginateConfig<CatEntity> = {
            sortableColumns: ['id'],
            withDeleted: true,
        }
        const query: PaginateQuery = {
            path: '',
        }
        await catRepo.softDelete({ id: cats[0].id })
        const result = await paginate<CatEntity>(query, catRepo, config)
        expect(result.meta.totalItems).toBe(cats.length)
    })

    it('should return only undeleted items', async () => {
        const config: PaginateConfig<CatEntity> = {
            sortableColumns: ['id'],
            withDeleted: false,
        }
        const query: PaginateQuery = {
            path: '',
        }
        await catRepo.softDelete({ id: cats[0].id })
        const result = await paginate<CatEntity>(query, catRepo, config)
        expect(result.meta.totalItems).toBe(cats.length - 1)
    })
})
Example #10
Source File: AdminQueryEngine.tsx    From querybook with Apache License 2.0 4 votes vote down vote up
AdminQueryEngine: React.FunctionComponent<IProps> = ({
    queryEngines,
    metastores,
    loadQueryEngines,
    loadMetastores,
}) => {
    const { id: queryEngineId } = useParams();

    const { data: queryEngineTemplates } = useResource(
        AdminQueryEngineResource.getTemplates
    );

    const { data: engineStatusCheckerNames } = useResource(
        AdminQueryEngineResource.getCheckerNames
    );

    const { data: tableUploadExporterNames } = useResource(
        AdminQueryEngineResource.getTableUploadExporterNames
    );

    const querybookLanguages: string[] = React.useMemo(
        () => [
            ...new Set(
                (queryEngineTemplates || []).map(
                    (executor) => executor.language
                )
            ),
        ],
        [queryEngineTemplates]
    );
    const querybookLanguageOptions = React.useMemo(
        () =>
            querybookLanguages.map((l) => ({
                label: titleize(l),
                value: l,
            })),
        [querybookLanguages]
    );

    const executorByLanguage: Record<string, string[]> = React.useMemo(
        () =>
            (queryEngineTemplates || []).reduce((hash, executor) => {
                if (!(executor.language in hash)) {
                    hash[executor.language] = [];
                }
                hash[executor.language].push(executor.name);
                return hash;
            }, {}),
        [queryEngineTemplates]
    );
    const executorTemplate: Record<string, AllFormField> = React.useMemo(
        () =>
            (queryEngineTemplates || []).reduce((hash, executor) => {
                hash[executor.name] = executor.template;
                return hash;
            }, {}),
        [queryEngineTemplates]
    );

    React.useEffect(() => {
        if (!metastores) {
            loadMetastores();
        }
    }, []);

    const createQueryEngine = React.useCallback(
        async (queryEngine: IAdminQueryEngine) => {
            const { data } = await AdminQueryEngineResource.create(queryEngine);

            await loadQueryEngines();
            history.push(`/admin/query_engine/${data.id}/`);

            return data;
        },
        []
    );

    const saveQueryEngine = React.useCallback(
        async (queryEngine: Partial<IAdminQueryEngine>) => {
            const { data } = await AdminQueryEngineResource.update(
                queryEngineId,
                queryEngine
            );
            return data;
        },
        [queryEngineId]
    );

    const deleteQueryEngine = React.useCallback(
        (queryEngine: IAdminQueryEngine) =>
            new Promise((resolve, reject) => {
                sendConfirm({
                    header: 'Archive Query Engine?',
                    message: (
                        <Content>
                            <p>
                                Once archived, the engine will become{' '}
                                <b>read-only</b>, and it can be recovered later
                                in the{' '}
                                <Link to="/admin/query_engine/deleted/">
                                    trash
                                </Link>
                                .
                            </p>
                            <br />
                            <p>Archiving this query engine means:</p>
                            <ul>
                                <li>
                                    New queries using this engine will fail.
                                </li>
                                <li>
                                    Users will not be able to see this engine in
                                    any environment.
                                </li>
                                <li>
                                    Users' past queries and query results are
                                    preserved and accessible.
                                </li>
                            </ul>
                            <p>
                                If you want to revoke all users' access to their
                                past query results executed by the engine,
                                please remove the engine from all environments
                                before archiving.
                            </p>
                        </Content>
                    ),
                    onConfirm: () =>
                        AdminQueryEngineResource.delete(queryEngine.id).then(
                            resolve
                        ),
                    onDismiss: reject,
                });
            }),
        []
    );

    const recoverQueryEngine = React.useCallback(async (engineId: number) => {
        const { data } = await AdminQueryEngineResource.recover(engineId);

        await loadQueryEngines();
        history.push(`/admin/query_engine/${engineId}/`);

        return data;
    }, []);

    const itemValidator = React.useCallback(
        (queryEngine: IAdminQueryEngine) => {
            const errors: Partial<Record<keyof IAdminQueryEngine, string>> = {};
            if ((queryEngine.description || '').length > 500) {
                errors.description = 'Description is too long';
            }

            if (!querybookLanguages.includes(queryEngine.language)) {
                errors.language = 'Unsupported language';
            }

            if (
                !executorByLanguage[queryEngine.language].includes(
                    queryEngine.executor
                )
            ) {
                errors.executor = 'Unsupported executor';
            }
            if (executorTemplate) {
                const formValid = validateForm(
                    queryEngine.executor_params,
                    executorTemplate[queryEngine.executor]
                );
                if (!formValid[0]) {
                    errors.executor_params = `Error found in ${formValid[2]}: ${formValid[1]}`;
                }
            }

            return errors;
        },
        [querybookLanguages, executorByLanguage, executorTemplate]
    );

    const renderQueryEngineExecutorParams = (
        template: TemplatedForm,
        executorParams: Record<string, any>,
        onChange: (
            fieldName: string,
            fieldValue: any,
            item?: IAdminQueryEngine
        ) => void
    ) => (
        <SmartForm
            formField={template}
            value={executorParams}
            onChange={(path, value) =>
                onChange(
                    'executor_params',
                    updateValue(executorParams, path, value)
                )
            }
        />
    );

    const renderQueryEngineItem = (
        item: IAdminQueryEngine,
        onChange: (fieldName: string, fieldValue: any) => void
    ) => {
        const updateExecutor = (executor: string) => {
            onChange('executor', executor);
            onChange(
                'executor_params',
                getDefaultFormValue(executorTemplate[executor])
            );
        };

        const environmentDOM = item.id != null && (
            <div className="AdminForm-section">
                <div className="AdminForm-section-top flex-row">
                    <div className="AdminForm-section-title">Environments</div>
                </div>
                <div className="AdminForm-section-content">
                    <p>
                        This section is read only. Please add the query engine
                        to the environment in the{' '}
                        <Link to="/admin/environment/">environment config</Link>
                        .
                    </p>
                    <div className="AdmingQueryEngine-environment-list mt8 p8">
                        {item.environments?.length
                            ? item.environments.map((environment) => (
                                  <div key={environment.id}>
                                      <Link
                                          to={`/admin/environment/${environment.id}/`}
                                      >
                                          {environment.name}
                                      </Link>
                                  </div>
                              ))
                            : 'This query engine does not belong to any environments.'}
                    </div>
                </div>
            </div>
        );

        const logDOM = item.id != null && (
            <div className="right-align">
                <AdminAuditLogButton itemType="query_engine" itemId={item.id} />
            </div>
        );

        return (
            <>
                <div className="AdminForm-top">
                    {logDOM}
                    <SimpleField stacked name="name" type="input" />
                </div>
                <div className="AdminForm-main">
                    <div className="AdminForm-left">
                        <SimpleField
                            stacked
                            name="description"
                            type="textarea"
                            rows={3}
                        />
                        <div className="flex-row stacked-fields flex0-children">
                            <SimpleField
                                stacked
                                name="metastore_id"
                                label="Metastore"
                                type="select"
                                options={(metastores || []).map(
                                    (metastore: IAdminMetastore) => ({
                                        key: metastore.id,
                                        value: metastore.name,
                                    })
                                )}
                                onChange={(value) => {
                                    onChange(
                                        'metastore_id',
                                        value !== '' ? value : null
                                    );
                                }}
                                withDeselect
                            />
                            <SimpleField
                                stacked
                                help={() => (
                                    <div>
                                        Didn’t find your engine? Querybook
                                        supports more, click{' '}
                                        <Link
                                            newTab
                                            to="https://www.querybook.org/docs/setup_guide/connect_to_query_engines/"
                                        >
                                            here
                                        </Link>{' '}
                                        to find out how to easily enable it.
                                    </div>
                                )}
                                name="language"
                                type="react-select"
                                options={querybookLanguageOptions}
                                onChange={(language) => {
                                    onChange('language', language);
                                    updateExecutor(
                                        executorByLanguage[language][0]
                                    );
                                }}
                                className="flex1"
                            />
                            <SimpleField
                                stacked
                                name="executor"
                                type="select"
                                options={executorByLanguage[item.language]}
                                onChange={updateExecutor}
                            />
                        </div>
                        <div className="AdminForm-section">
                            <div className="AdminForm-section-top flex-row">
                                <div className="AdminForm-section-title">
                                    Executor Params
                                </div>
                            </div>
                            <div className="AdminForm-section-content">
                                <Loader
                                    renderer={() =>
                                        renderQueryEngineExecutorParams(
                                            executorTemplate[item.executor],
                                            item.executor_params,
                                            onChange
                                        )
                                    }
                                    item={executorTemplate[item.executor]}
                                    itemLoader={NOOP}
                                />
                            </div>
                        </div>

                        <div className="AdminForm-section">
                            <div className="AdminForm-section-top flex-row">
                                <div className="AdminForm-section-title">
                                    Additional Features
                                </div>
                            </div>
                            <div className="AdminForm-section-content">
                                <SimpleField
                                    stacked
                                    name="feature_params.status_checker"
                                    type="react-select"
                                    options={engineStatusCheckerNames}
                                    withDeselect
                                />

                                <SimpleField
                                    stacked
                                    name="feature_params.upload_exporter"
                                    type="react-select"
                                    label="(Experimental) Table Upload Exporter"
                                    options={tableUploadExporterNames}
                                    withDeselect
                                />
                            </div>
                        </div>

                        {environmentDOM}
                    </div>
                </div>
            </>
        );
    };

    const renderQueryEngineItemActions = (item: IAdminQueryEngine) => {
        const hasStatusChecker = Boolean(item.feature_params?.status_checker);
        const handleTestConnectionClick = () =>
            toast.promise(
                AdminQueryEngineResource.testConnection(item).then(
                    ({ data }) => {
                        if (data.status === QueryEngineStatus.GOOD) {
                            return Promise.resolve(data.messages);
                        } else {
                            return Promise.reject(data.messages);
                        }
                    }
                ),
                {
                    loading: 'Checking status...',
                    success: (msg) => msg.join('\n'),
                    error: (msg) => msg.join('\n'),
                }
            );
        return (
            <>
                <AsyncButton
                    icon="PlugZap"
                    title="Test Connection"
                    disabled={!hasStatusChecker}
                    aria-label={
                        hasStatusChecker
                            ? null
                            : `Must specify Status Checker to test connection`
                    }
                    data-balloon-pos={'up'}
                    onClick={handleTestConnectionClick}
                    disableWhileAsync
                />
            </>
        );
    };

    if (queryEngineId === 'new') {
        if (querybookLanguages && executorByLanguage && executorTemplate) {
            const defaultLanguage = querybookLanguages[0];
            const defaultExecutor = executorByLanguage[defaultLanguage]?.[0];
            const defaultExecutorTemplate = executorTemplate[defaultExecutor];

            const newQueryEngine: IAdminQueryEngine = {
                id: null,
                created_at: moment().unix(),
                updated_at: moment().unix(),
                deleted_at: null,
                name: '',
                language: defaultLanguage,
                description: '',
                metastore_id: null,
                executor: defaultExecutor,

                // Since defaultExecutorTemplate has to be StructForm
                executor_params: (defaultExecutorTemplate &&
                    getDefaultFormValue(defaultExecutorTemplate)) as Record<
                    string,
                    any
                >,
                feature_params: {},
            };
            return (
                <div className="AdminQueryEngine">
                    <div className="AdminForm">
                        <GenericCRUD
                            item={newQueryEngine}
                            createItem={createQueryEngine}
                            renderItem={renderQueryEngineItem}
                            renderActions={renderQueryEngineItemActions}
                            validate={itemValidator}
                        />
                    </div>
                </div>
            );
        } else {
            return <Loading />;
        }
    }

    const queryEngineItem = queryEngines?.find(
        (engine) => Number(queryEngineId) === engine.id
    );

    if (
        queryEngineId === 'deleted' ||
        (queryEngineItem && queryEngineItem.deleted_at !== null)
    ) {
        const deletedQueryEngines = queryEngineItem?.deleted_at
            ? [queryEngineItem]
            : queryEngines?.filter((eng) => eng.deleted_at);
        return (
            <div className="AdminQueryEngine">
                <div className="AdminLanding-top">
                    <div className="AdminLanding-desc">
                        Deleted query engines can be recovered.
                    </div>
                </div>
                <div className="AdminLanding-content">
                    <AdminDeletedList
                        items={deletedQueryEngines}
                        onRecover={recoverQueryEngine}
                        keysToShow={[
                            'created_at',
                            'deleted_at',
                            'executor',
                            'executor_params',
                        ]}
                    />
                </div>
            </div>
        );
    }

    if (queryEngineItem) {
        if (querybookLanguages && executorByLanguage && executorTemplate) {
            return (
                <div className="AdminQueryEngine">
                    <div className="AdminForm">
                        <GenericCRUD
                            item={queryEngineItem}
                            deleteItem={deleteQueryEngine}
                            updateItem={saveQueryEngine}
                            validate={itemValidator}
                            renderItem={renderQueryEngineItem}
                            onItemCUD={loadQueryEngines}
                            renderActions={renderQueryEngineItemActions}
                            onDelete={() =>
                                history.push('/admin/query_engine/')
                            }
                        />
                    </div>
                </div>
            );
        } else {
            return <Loading />;
        }
    } else {
        const getCardDOM = () =>
            clone(queryEngines)
                .filter((eng) => eng.deleted_at == null)
                .sort((e1, e2) => e2.updated_at - e1.updated_at)
                .slice(0, 5)
                .map((e) => (
                    <Card
                        key={e.id}
                        title={e.name}
                        onClick={() =>
                            history.push(`/admin/query_engine/${e.id}/`)
                        }
                        height="160px"
                        width="240px"
                    >
                        <div className="AdminLanding-card-content">
                            <div className="AdminLanding-card-content-top">
                                Last Updated
                            </div>
                            <div className="AdminLanding-card-content-date">
                                {generateFormattedDate(e.updated_at)}
                            </div>
                        </div>
                    </Card>
                ));
        return (
            <div className="AdminQueryEngine">
                <div className="AdminLanding">
                    <div className="AdminLanding-top">
                        <Level>
                            <div className="AdminLanding-title">
                                Query Engine
                            </div>

                            <AdminAuditLogButton itemType={'query_engine'} />
                        </Level>
                        <div className="AdminLanding-desc">
                            Explore data in any language, from any data source.
                        </div>
                    </div>
                    <div className="AdminLanding-content">
                        <div className="AdminLanding-cards flex-row">
                            {queryEngines && getCardDOM()}
                            <Card
                                title="+"
                                onClick={() =>
                                    history.push('/admin/query_engine/new/')
                                }
                                height="160px"
                                width="240px"
                            >
                                create a new query engine
                            </Card>
                        </div>
                    </div>
                </div>
            </div>
        );
    }
}
Example #11
Source File: AdminMetastore.tsx    From querybook with Apache License 2.0 4 votes vote down vote up
AdminMetastore: React.FunctionComponent<IProps> = ({
    metastores,
    loadMetastores,
}) => {
    const { id: metastoreId } = useParams();

    const [showTaskEditor, setShowTaskEditor] = React.useState<boolean>(false);

    const { data: metastoreLoaders } = useResource(
        AdminMetastoreResource.getAllLoaders
    );

    const {
        data: metastoreUpdateSchedule,
        forceFetch: loadMetastoreUpdateSchedule,
    } = useResource(
        React.useCallback(
            () => AdminMetastoreResource.getUpdateSchedule(metastoreId),
            [metastoreId]
        )
    );

    React.useEffect(() => {
        if (metastoreUpdateSchedule?.id) {
            setShowTaskEditor(true);
        }
    }, [metastoreUpdateSchedule]);

    const createMetastore = React.useCallback(
        async (metastore: IAdminMetastore) => {
            const { data } = await AdminMetastoreResource.create(
                metastore.name,
                metastore.metastore_params,
                metastore.loader,
                metastore.acl_control
            );

            await loadMetastores();
            history.push(`/admin/metastore/${data.id}/`);

            return data;
        },
        []
    );

    const saveMetastore = React.useCallback(
        async (metastore: Partial<IAdminMetastore>) => {
            const { data } = await AdminMetastoreResource.update(
                metastoreId,
                metastore
            );

            return data as IAdminMetastore;
        },
        [metastoreId]
    );

    const deleteMetastore = React.useCallback(
        (metastore: IAdminMetastore) =>
            AdminMetastoreResource.delete(metastore.id),
        []
    );

    const recoverMetastore = React.useCallback(async (mId: number) => {
        const { data } = await AdminMetastoreResource.recover(mId);

        await loadMetastores();
        history.push(`/admin/metastore/${mId}/`);

        return data;
    }, []);

    const itemValidator = React.useCallback(
        (metastore: IAdminMetastore) => {
            const errors: Partial<Record<keyof IAdminMetastore, string>> = {};

            if ((metastore.name || '').length === 0) {
                errors.name = 'Name cannot be empty';
            } else if ((metastore.name || '').length > 255) {
                errors.name = 'Name is too long';
            }

            const loader = (metastoreLoaders || []).find(
                (l) => l.name === metastore.loader
            );
            if (!loader) {
                errors.loader = 'Invalid loader';
            }
            const formValid = validateForm(
                metastore.metastore_params,
                loader.template
            );
            if (!formValid[0]) {
                errors.metastore_params = `Error found in loader params ${formValid[2]}: ${formValid[1]}`;
            }

            if (metastore.acl_control.type) {
                for (const [
                    index,
                    table,
                ] of metastore.acl_control.tables.entries()) {
                    if (!table) {
                        errors.acl_control = `Table at index ${index} is empty`;
                        break;
                    }
                }
            }
            return errors;
        },
        [metastoreLoaders]
    );

    const getMetastoreACLControlDOM = (
        aclControl: IAdminACLControl,
        onChange: (fieldName: string, fieldValue: any) => void
    ) => {
        if (aclControl.type == null) {
            return (
                <div className="AdminMetastore-acl-button">
                    <TextButton
                        onClick={() =>
                            onChange('acl_control', {
                                type: 'denylist',
                                tables: [],
                            })
                        }
                        title="Create Allowlist/Denylist"
                    />
                </div>
            );
        }

        const tablesDOM = (
            <SmartForm
                formField={{
                    field_type: 'list',
                    of: {
                        description:
                            aclControl.type === 'denylist'
                                ? 'Table to Denylist'
                                : 'Table to Allowlist',
                        field_type: 'string',
                        helper: '',
                        hidden: false,
                        required: true,
                    },
                    max: null,
                    min: 1,
                }}
                value={aclControl.tables}
                onChange={(path, value) =>
                    onChange(
                        `acl_control.tables`,
                        updateValue(aclControl.tables, path, value)
                    )
                }
            />
        );
        return (
            <>
                <div className="AdminMetastore-acl-warning flex-row">
                    <Icon name="AlertOctagon" />
                    {aclControl.type === 'denylist'
                        ? 'All tables will be allowed unless specified.'
                        : 'All tables will be denied unless specified.'}
                </div>
                <div className="AdminMetastore-acl-top horizontal-space-between">
                    <Tabs
                        selectedTabKey={aclControl.type}
                        items={[
                            { name: 'Denylist', key: 'denylist' },
                            { name: 'Allowlist', key: 'allowlist' },
                        ]}
                        onSelect={(key) => {
                            onChange('acl_control', { type: key, tables: [] });
                        }}
                    />
                    <TextButton
                        title={
                            aclControl.type === 'denylist'
                                ? 'Remove Denylist'
                                : 'Remove Allowlist'
                        }
                        onClick={() => onChange('acl_control', {})}
                    />
                </div>
                {tablesDOM}
            </>
        );
    };

    const renderMetastoreItem = (
        item: IAdminMetastore,
        onChange: (fieldName: string, fieldValue: any) => void
    ) => {
        const loader = (metastoreLoaders || []).find(
            (l) => l.name === item.loader
        );

        const updateLoader = (loaderName: string) => {
            const newLoader = (metastoreLoaders || []).find(
                (l) => l.name === loaderName
            );
            if (newLoader) {
                onChange(
                    'metastore_params',
                    getDefaultFormValue(newLoader.template)
                );
                onChange('loader', newLoader.name);
            }
        };

        const logDOM = item.id != null && (
            <div className="right-align">
                <AdminAuditLogButton
                    itemType="query_metastore"
                    itemId={item.id}
                />
            </div>
        );

        return (
            <>
                <div className="AdminForm-top">
                    {logDOM}
                    <SimpleField stacked name="name" type="input" />
                </div>
                <div className="AdminForm-main">
                    <div className="AdminForm-left">
                        <SimpleField
                            stacked
                            name="loader"
                            type="react-select"
                            options={Object.values(metastoreLoaders).map(
                                (l) => ({
                                    value: l.name,
                                    label: l.name,
                                })
                            )}
                            onChange={updateLoader}
                        />

                        {loader && (
                            <div className="AdminForm-section">
                                <div className="AdminForm-section-top flex-row">
                                    <div className="AdminForm-section-title">
                                        Loader Params
                                    </div>
                                </div>
                                <div className="AdminForm-section-content">
                                    <SmartForm
                                        formField={loader.template}
                                        value={item.metastore_params}
                                        onChange={(path, value) =>
                                            onChange(
                                                'metastore_params',
                                                updateValue(
                                                    item.metastore_params,
                                                    path,
                                                    value
                                                )
                                            )
                                        }
                                    />
                                </div>
                            </div>
                        )}
                        <div className="AdminForm-section">
                            <div className="AdminForm-section-top flex-row">
                                <div className="AdminForm-section-title">
                                    ACL Control
                                </div>
                            </div>
                            <div className="AdminForm-section-content">
                                {getMetastoreACLControlDOM(
                                    item.acl_control,
                                    onChange
                                )}
                            </div>
                        </div>
                        {metastoreId !== 'new' && (
                            <div className="AdminForm-section">
                                <div className="AdminForm-section-top flex-row">
                                    <div className="AdminForm-section-title">
                                        Update Schedule
                                    </div>
                                </div>
                                <div className="AdminForm-section-content">
                                    {showTaskEditor ? (
                                        <div className="AdminMetastore-TaskEditor">
                                            <TaskEditor
                                                task={
                                                    metastoreUpdateSchedule ?? {
                                                        cron: '0 0 * * *',
                                                        name: `update_metastore_${metastoreId}`,
                                                        task:
                                                            'tasks.update_metastore.update_metastore',
                                                        task_type: 'prod',
                                                        enabled: true,
                                                        args: [
                                                            Number(metastoreId),
                                                        ],
                                                    }
                                                }
                                                onTaskCreate={
                                                    loadMetastoreUpdateSchedule
                                                }
                                            />
                                        </div>
                                    ) : (
                                        <div className="AdminMetastore-TaskEditor-button center-align">
                                            <TextButton
                                                title="Create Schedule"
                                                onClick={() =>
                                                    setShowTaskEditor(true)
                                                }
                                            />
                                        </div>
                                    )}
                                </div>
                            </div>
                        )}
                    </div>
                </div>
            </>
        );
    };

    if (metastoreId === 'new') {
        if (metastoreLoaders) {
            const defaultLoader = metastoreLoaders[0];

            const newMetastore: IAdminMetastore = {
                id: null,
                created_at: moment().unix(),
                updated_at: moment().unix(),
                deleted_at: null,
                name: '',
                loader: defaultLoader.name,
                // Only StructForm form for template
                metastore_params: getDefaultFormValue(
                    defaultLoader.template
                ) as Record<string, unknown>,
                acl_control: {},
            };
            return (
                <div className="AdminMetastore">
                    <div className="AdminForm">
                        <GenericCRUD
                            item={newMetastore}
                            createItem={createMetastore}
                            renderItem={renderMetastoreItem}
                            validate={itemValidator}
                        />
                    </div>
                </div>
            );
        } else {
            return <Loading />;
        }
    }

    const metastoreItem = metastores?.find(
        (metastore) => Number(metastoreId) === metastore.id
    );

    if (
        metastoreId === 'deleted' ||
        (metastoreItem && metastoreItem.deleted_at !== null)
    ) {
        const deletedMetastores = metastoreItem?.deleted_at
            ? [metastoreItem]
            : metastores?.filter((ms) => ms.deleted_at);
        return (
            <div className="AdminMetastore">
                <div className="AdminLanding-top">
                    <div className="AdminLanding-desc">
                        Deleted metastores can be recovered.
                    </div>
                </div>
                <div className="AdminLanding-content">
                    <AdminDeletedList
                        items={deletedMetastores}
                        onRecover={recoverMetastore}
                        keysToShow={[
                            'created_at',
                            'deleted_at',
                            'loader',
                            'metastore_params',
                        ]}
                    />
                </div>
            </div>
        );
    }

    if (metastoreItem) {
        if (metastoreLoaders) {
            return (
                <div className="AdminMetastore">
                    <div className="AdminForm">
                        <GenericCRUD
                            item={metastoreItem}
                            deleteItem={deleteMetastore}
                            onDelete={() => history.push('/admin/metastore/')}
                            updateItem={saveMetastore}
                            validate={itemValidator}
                            renderItem={renderMetastoreItem}
                            onItemCUD={loadMetastores}
                        />
                    </div>
                </div>
            );
        } else {
            return <Loading />;
        }
    } else {
        const getCardDOM = () =>
            clone(metastores)
                .filter((ms) => ms.deleted_at == null)
                .sort((m1, m2) => m2.updated_at - m1.updated_at)
                .slice(0, 5)
                .map((m) => (
                    <Card
                        key={m.id}
                        title={m.name}
                        onClick={() =>
                            history.push(`/admin/metastore/${m.id}/`)
                        }
                        height="160px"
                        width="240px"
                    >
                        {' '}
                        <div className="AdminLanding-card-content">
                            <div className="AdminLanding-card-content-top">
                                Last Updated
                            </div>
                            <div className="AdminLanding-card-content-date">
                                {generateFormattedDate(m.updated_at)}
                            </div>
                        </div>
                    </Card>
                ));
        return (
            <div className="AdminMetastore">
                <div className="AdminLanding">
                    <div className="AdminLanding-top">
                        <Level>
                            <div className="AdminLanding-title">Metastore</div>
                            <AdminAuditLogButton itemType="query_metastore" />
                        </Level>
                        <div className="AdminLanding-desc">
                            Metastores hold metadata for the tables, such as
                            schemas and deny/allowlists.
                        </div>
                    </div>
                    <div className="AdminLanding-content">
                        <div className="AdminLanding-cards flex-row">
                            {metastores && getCardDOM()}
                            <Card
                                title="+"
                                onClick={() =>
                                    history.push('/admin/metastore/new/')
                                }
                                height="160px"
                                width="240px"
                            >
                                create a new metastore
                            </Card>
                        </div>
                    </div>
                </div>
            </div>
        );
    }
}
Example #12
Source File: AdminEnvironment.tsx    From querybook with Apache License 2.0 4 votes vote down vote up
AdminEnvironment: React.FunctionComponent<IProps> = ({
    environments,
    queryEngines,
    loadEnvironments,
    loadQueryEngines,
}) => {
    const { id: environmentId } = useParams();
    const createEnvironment = React.useCallback(
        async (environment: IAdminEnvironment) => {
            const { data } = await AdminEnvironmentResource.create(
                environment.name,
                environment.description,
                environment.image,
                environment.public,
                environment.hidden,
                environment.shareable
            );

            await loadEnvironments();
            history.push(`/admin/environment/${data.id}/`);

            return data;
        },
        []
    );

    const saveEnvironment = React.useCallback(
        async (environment: Partial<IAdminEnvironment>) => {
            const { data } = await AdminEnvironmentResource.update(
                environmentId,
                environment
            );

            return data as IAdminEnvironment;
        },
        [environmentId]
    );

    const deleteEnvironment = React.useCallback(
        (environment: IAdminEnvironment) =>
            AdminEnvironmentResource.delete(environment.id),
        []
    );

    const recoverEnvironment = React.useCallback(async (envId: number) => {
        const { data } = await AdminEnvironmentResource.recover(envId);

        await loadEnvironments();
        history.push(`/admin/environment/${envId}/`);

        return data;
    }, []);

    const renderEnvironmentItem = (item: IAdminEnvironment) => {
        const logDOM = item.id != null && (
            <div className="right-align">
                <AdminAuditLogButton itemType="environment" itemId={item.id} />
            </div>
        );
        return (
            <>
                <div className="AdminForm-top">
                    {logDOM}
                    <SimpleField
                        stacked
                        name="name"
                        label="Key"
                        type="input"
                        help="Lower case alphanumeric and underscore only"
                        required
                    />
                </div>
                <div className="AdminForm-main">
                    <div className="AdminForm-left">
                        <SimpleField
                            stacked
                            name="description"
                            type="textarea"
                            placeholder="Describe your environment here."
                            rows={3}
                        />
                        <SimpleField
                            stacked
                            name="image"
                            type="input"
                            label="Logo Url"
                        />

                        {environmentId !== 'new' && (
                            <>
                                <AdminEnvironmentQueryEngine
                                    queryEngines={queryEngines}
                                    environmentId={environmentId}
                                    loadQueryEngines={loadQueryEngines}
                                />
                                <div className="AdminForm-section">
                                    <div className="AdminForm-section-top flex-row">
                                        <div className="AdminForm-section-title">
                                            Access Control
                                        </div>
                                    </div>
                                    <div className="AdminForm-section-content">
                                        <UserEnvironmentEditor
                                            environmentId={item.id}
                                        />
                                    </div>
                                </div>
                            </>
                        )}
                    </div>
                    <div className="AdminForm-right">
                        <SimpleField
                            name="public"
                            type="toggle"
                            help="If public, all users on Querybook can access this environment."
                        />
                        <SimpleField
                            name="hidden"
                            type="toggle"
                            help="Hidden environments will not be shown to unauthorized users in their environment picker."
                        />
                        <SimpleField
                            name="shareable"
                            type="toggle"
                            help={
                                "If true, all docs and query executions in the environment are readable to users even if they don't have access. " +
                                'If false, users would need to be explicitly invited to view docs/queries'
                            }
                        />
                    </div>
                </div>
            </>
        );
    };

    if (environmentId === 'new') {
        const newEnvironment: IAdminEnvironment = {
            id: null,
            name: '',
            description: '',
            image: '',
            public: true,
            hidden: false,
            shareable: true,
            deleted_at: null,
        };
        return (
            <div className="AdminEnvironment">
                <div className="AdminForm">
                    <GenericCRUD
                        item={newEnvironment}
                        createItem={createEnvironment}
                        renderItem={renderEnvironmentItem}
                        validationSchema={environmentSchema}
                    />
                </div>
            </div>
        );
    }

    const environmentItem = environments?.find(
        (engine) => Number(environmentId) === engine.id
    );

    if (
        environmentId === 'deleted' ||
        (environmentItem && environmentItem.deleted_at !== null)
    ) {
        const deletedEnvironments = environmentItem?.deleted_at
            ? [environmentItem]
            : environments?.filter((env) => env.deleted_at);
        return (
            <div className="AdminEnvironment">
                <div className="AdminLanding-top">
                    <div className="AdminLanding-desc">
                        Deleted environments can be recovered.
                    </div>
                </div>
                <div className="AdminLanding-content">
                    <AdminDeletedList
                        items={deletedEnvironments}
                        onRecover={recoverEnvironment}
                        keysToShow={[
                            'deleted_at',
                            'description',
                            'hidden',
                            'public',
                        ]}
                    />
                </div>
            </div>
        );
    }

    if (environmentItem) {
        return (
            <div className="AdminEnvironment">
                <div className="AdminForm">
                    <GenericCRUD
                        item={environmentItem}
                        deleteItem={deleteEnvironment}
                        updateItem={saveEnvironment}
                        validationSchema={environmentSchema}
                        renderItem={renderEnvironmentItem}
                        onItemCUD={loadEnvironments}
                        onDelete={() => history.push('/admin/environment/')}
                    />
                </div>
            </div>
        );
    } else {
        const getCardDOM = () =>
            clone(environments)
                .filter((env) => env.deleted_at == null)
                .sort((env1, env2) => env1.id - env2.id)
                .slice(0, 5)
                .map((env) => (
                    <Card
                        key={env.id}
                        title={env.name}
                        onClick={() =>
                            history.push(`/admin/environment/${env.id}/`)
                        }
                        height="160px"
                        width="240px"
                    >
                        <div className="AdminLanding-card-content">
                            {env.description}
                        </div>
                    </Card>
                ));
        return (
            <div className="AdminEnvironment">
                <div className="AdminLanding">
                    <div className="AdminLanding-top">
                        <Level>
                            <div className="AdminLanding-title">
                                Environment
                            </div>
                            <AdminAuditLogButton itemType={'environment'} />
                        </Level>
                        <div className="AdminLanding-desc">
                            Querybook provides environments for access control
                            and scoped workspaces.
                        </div>
                    </div>
                    <div className="AdminLanding-content">
                        <div className="AdminLanding-cards flex-row">
                            {environments && getCardDOM()}
                            <Card
                                title="+"
                                onClick={() =>
                                    history.push('/admin/environment/new/')
                                }
                                height="160px"
                                width="240px"
                            >
                                create a new environment
                            </Card>
                        </div>
                    </div>
                </div>
            </div>
        );
    }
}
Example #13
Source File: buildStoryboardV2.spec.ts    From next-basics with GNU General Public License v3.0 4 votes vote down vote up
describe("buildStoryboardV2", () => {
  afterEach(() => {
    jest.clearAllMocks();
  });

  it.each<
    [
      string,
      Parameters<typeof buildStoryboardV2>[0],
      ReturnType<typeof buildStoryboardV2>
    ]
  >([
    [
      "",
      // Input
      {
        routeList: [
          {
            id: "R-01",
            instanceId: "instance-r01",
            path: "/a",
            type: "bricks",
            parent: [], // Empty parent also works.
            providers: '["p1"]',
            segues: null,
            children: [
              {
                id: "B-01",
                instanceId: "instance-b01",
                type: "brick",
                brick: "m",
                parent: [{ id: "R-01" }],
                if: "false",
                lifeCycle: undefined,
                children: [
                  {
                    id: "R-04",
                    instanceId: "instance-r04",
                    path: "/a/d",
                    type: "bricks",
                    mountPoint: "m2",
                  },
                  {
                    id: "R-05",
                    instanceId: "instance-r05",
                    path: "/a/e",
                    type: "bricks",
                    mountPoint: "m2",
                  },
                  {
                    id: "B-04",
                    instanceId: "instance-b04",
                    type: "brick",
                    brick: "p",
                    parent: [{ id: "B-01" }],
                    mountPoint: "m1",
                  },
                  {
                    id: "B-05",
                    instanceId: "instance-b05",
                    type: "template",
                    brick: "q",
                    parent: [{ id: "B-01" }],
                    mountPoint: "m1",
                  },
                ],
              },
              {
                id: "B-02",
                instanceId: "instance-b02",
                type: "brick",
                brick: "n",
              },
            ],
            // Fields should be removed.
            _ts: 123,
            org: 1,
          },
          {
            id: "R-02",
            instanceId: "instance-r02",
            path: "/b",
            type: "routes",
            permissionsPreCheck:
              '["<% `cmdb:${QUERY.objectId}_instance_create` %>"]',
            children: [
              {
                id: "R-03",
                instanceId: "instance-r03",
                path: "/b/c",
                type: "bricks",
                children: [
                  {
                    id: "B-03",
                    instanceId: "instance-b03",
                    type: "brick",
                    brick: "o",
                  },
                ],
              },
            ],
          },
        ],
        templateList: [
          {
            templateId: "tpl-01",
            id: "B-T-01",
            proxy: {
              properties: {
                one: {
                  ref: "ref-01",
                  refProperty: "two",
                },
              },
            },
            state: [
              {
                name: "myState",
                value: "any data",
              },
            ],
            children: [
              {
                id: "T-B-01",
                instanceId: "instance-t-b01",
                type: "brick",
                brick: "z",
                children: [
                  {
                    id: "T-B-02",
                    instanceId: "instance-t-b02",
                    type: "brick",
                    brick: "y",
                    ref: "two",
                    mountPoint: "m5",
                    children: [
                      {
                        id: "T-B-03",
                        instanceId: "instance-t-b03",
                        type: "brick",
                        brick: "x",
                        mountPoint: "m6",
                      },
                    ],
                  },
                ],
              },
            ],
          },
        ],
        menus: [
          {
            menuId: "menu-a",
            items: [
              {
                text: "Menu Item 1",
              },
              {
                text: "Menu Item 2",
                children: [
                  {
                    text: "Menu Item 2-1",
                    children: [{ text: "Menu Item 2-1-1" }],
                  },
                ],
              },
            ],
          },
          {
            menuId: "menu-b",
            dynamicItems: true,
            itemsResolve: {
              useProvider: "my.menu-provider",
            },
          },
        ],
        i18n: [
          {
            name: "FILES",
            en: "Files",
            zh: "文件",
          },
          {
            name: "SETTINGS",
            en: "Settings",
            zh: "设置",
          },
        ],
        functions: [
          {
            name: "sayHello",
            source: "function sayHello() {}",
            description: "Say hello",
          },
          {
            name: "sayExclamation",
            source: "function sayExclamation() {}",
            description: "Say exclamation",
            typescript: true,
          },
        ],
        dependsAll: false,
      },
      // Output
      {
        routes: [
          {
            path: "/a",
            type: "bricks",
            providers: ["p1"],
            bricks: [
              {
                iid: "instance-b01",
                brick: "m",
                if: false,
                slots: {
                  m1: {
                    type: "bricks",
                    bricks: [
                      { iid: "instance-b04", brick: "p" },
                      { iid: "instance-b05", template: "q" },
                    ],
                  },
                  m2: {
                    type: "routes",
                    routes: [
                      {
                        path: "/a/d",
                        type: "bricks",
                        bricks: [],
                      },
                      {
                        path: "/a/e",
                        type: "bricks",
                        bricks: [],
                      },
                    ],
                  },
                },
              },
              { iid: "instance-b02", brick: "n" },
            ],
          },
          {
            path: "/b",
            type: "routes",
            permissionsPreCheck: [
              "<% `cmdb:${QUERY.objectId}_instance_create` %>",
            ],
            routes: [
              {
                path: "/b/c",
                type: "bricks",
                bricks: [{ iid: "instance-b03", brick: "o" }],
              },
            ],
          },
        ],
        meta: {
          customTemplates: [
            {
              name: "tpl-01",
              proxy: {
                properties: {
                  one: {
                    ref: "ref-01",
                    refProperty: "two",
                  },
                },
              },
              state: [
                {
                  name: "myState",
                  value: "any data",
                },
              ],
              bricks: [
                {
                  iid: "instance-t-b01",
                  brick: "z",
                  slots: {
                    m5: {
                      type: "bricks",
                      bricks: [
                        {
                          iid: "instance-t-b02",
                          brick: "y",
                          ref: "two",
                          slots: {
                            m6: {
                              type: "bricks",
                              bricks: [{ iid: "instance-t-b03", brick: "x" }],
                            },
                          },
                        },
                      ],
                    },
                  },
                },
              ],
            },
          ],
          menus: [
            {
              menuId: "menu-a",
              items: [
                {
                  text: "Menu Item 1",
                },
                {
                  text: "Menu Item 2",
                  children: [
                    {
                      text: "Menu Item 2-1",
                      children: [
                        {
                          text: "Menu Item 2-1-1",
                        },
                      ],
                    },
                  ],
                },
              ],
            },
            {
              menuId: "menu-b",
              dynamicItems: true,
              itemsResolve: {
                useProvider: "my.menu-provider",
              },
            },
          ],
          i18n: {
            en: {
              FILES: "Files",
              SETTINGS: "Settings",
            },
            zh: {
              FILES: "文件",
              SETTINGS: "设置",
            },
          },
          functions: [
            {
              name: "sayHello",
              source: "function sayHello() {}",
            },
            {
              name: "sayExclamation",
              source: "function sayExclamation() {}",
              typescript: true,
            },
          ],
        },
        dependsAll: false,
      },
    ],
    [
      "test permissionPreCheck",
      // Input
      {
        routeList: [
          {
            id: "R-01",
            path: "/a",
            type: "bricks",
            permissionsPreCheck:
              '["<% `cmdb:${QUERY.objectId}_instance_create` %>"]',
            children: [
              {
                id: "B-01",
                type: "brick",
                brick: "n",
                permissionsPreCheck: '["<% CTX.action %>"]',
              },
            ],
          },
        ],
        templateList: [
          {
            templateId: "tpl-01",
            children: [
              {
                id: "T-B-01",
                type: "brick",
                brick: "z",
                permissionsPreCheck: "",
                children: [
                  {
                    id: "T-B-02",
                    type: "brick",
                    brick: "y",
                    ref: "two",
                    permissionsPreCheck: '["<% CTX.action %>"]',
                    mountPoint: "m5",
                  },
                ],
              },
            ],
          },
        ],
      },
      // Output
      {
        routes: [
          {
            path: "/a",
            type: "bricks",
            permissionsPreCheck: [
              "<% `cmdb:${QUERY.objectId}_instance_create` %>",
            ],
            bricks: [
              {
                brick: "n",
                permissionsPreCheck: ["<% CTX.action %>"],
              },
            ],
          },
        ],
        meta: {
          customTemplates: [
            {
              name: "tpl-01",
              bricks: [
                {
                  brick: "z",
                  slots: {
                    m5: {
                      type: "bricks",
                      bricks: [
                        {
                          brick: "y",
                          ref: "two",
                          permissionsPreCheck: ["<% CTX.action %>"],
                        },
                      ],
                    },
                  },
                },
              ],
            },
          ],
        },
      },
    ],
    [
      "when a custom template has no bricks",
      // Input
      {
        routeList: [],
        templateList: [
          {
            templateId: "menu-a",
          },
        ],
      },
      // Output
      {
        routes: [],
        meta: {
          customTemplates: [
            {
              name: "menu-a",
              bricks: [],
            },
          ],
        },
      },
    ],
    [
      "when it's all empty",
      // Input
      {
        routeList: [],
        templateList: [],
        menus: [],
        i18n: [],
        dependsAll: false,
      },
      // Output
      {
        routes: [],
        meta: {
          customTemplates: [],
          menus: [],
          i18n: {
            en: {},
            zh: {},
          },
        },
        dependsAll: false,
      },
    ],
    [
      "test keepIds",
      // Input
      {
        routeList: [
          {
            id: "R-01",
            path: "/a",
            type: "bricks",
            parent: [], // Empty parent also works.
            providers: '["p1"]',
            segues: null,
            children: [
              {
                id: "B-01",
                instanceId: "instance-b01",
                type: "brick",
                brick: "m",
                if: "false",
                lifeCycle: undefined,
              },
            ],
          },
        ],
        templateList: [
          {
            templateId: "menu-a",
            id: "B-T-01",
            children: [
              {
                id: "T-B-01",
                instanceId: "instance-t-b01",
                type: "brick",
                brick: "z",
              },
            ],
          },
        ],
        menus: [],
        i18n: [],
        dependsAll: false,
        options: {
          keepIds: true,
        },
      },
      // Output,
      {
        routes: [
          {
            [symbolForNodeId]: "R-01",
            path: "/a",
            type: "bricks",
            providers: ["p1"],
            bricks: [
              {
                [symbolForNodeId]: "B-01",
                [symbolForNodeInstanceId]: "instance-b01",
                iid: "instance-b01",
                brick: "m",
                if: false,
              },
            ],
          },
        ],
        meta: {
          customTemplates: [
            {
              [symbolForNodeId]: "B-T-01",
              name: "menu-a",
              bricks: [
                {
                  [symbolForNodeId]: "T-B-01",
                  [symbolForNodeInstanceId]: "instance-t-b01",
                  iid: "instance-t-b01",
                  brick: "z",
                },
              ],
            },
          ],
          menus: [],
          i18n: {
            en: {},
            zh: {},
          },
        },
        dependsAll: false,
      } as any,
    ],
    [
      "useChildren single",
      {
        routeList: [
          {
            id: "R-01",
            path: "/a",
            type: "bricks",
            providers: '["p1"]',
            children: [
              {
                brick: "presentational-bricks.brick-table",
                id: "B-45235",
                instanceId: "5c4de59f26f55",
                mountPoint: "content",
                portal: false,
                properties: '{"useChildren": "[state]","otherFields":["yes"]} ',
                type: "brick",
                [symbolForNodeId]: "B-45235",
                [symbolForNodeInstanceId]: "5c4de59f26f55",
                children: [
                  {
                    brick: "presentational-bricks.brick-value-mapping",
                    id: "B-02",
                    instanceId: "instance-b02",
                    mountPoint: "[state]",
                    type: "brick",
                    properties: '{\n  "fields": "state"}',
                    if: "false",
                    lifeCycle: undefined,
                  },
                ],
              } as any,
            ],
          },
        ],
        templateList: [],
        menus: [],
        i18n: [],
        dependsAll: false,
        options: {
          keepIds: true,
        },
      },
      {
        dependsAll: false,
        meta: {
          customTemplates: [],
          i18n: {
            en: {},
            zh: {},
          },
          menus: [],
        },
        routes: [
          {
            [symbolForNodeId]: "R-01",
            path: "/a",
            providers: ["p1"],
            type: "bricks",
            segues: undefined,
            bricks: [
              {
                iid: "5c4de59f26f55",
                brick: "presentational-bricks.brick-table",
                portal: false,
                properties: {
                  otherFields: ["yes"],
                  useBrick: {
                    iid: "instance-b02",
                    brick: "presentational-bricks.brick-value-mapping",
                    if: false,
                    lifeCycle: undefined,
                    properties: {
                      fields: "state",
                    },
                    [symbolForNodeId]: "B-02",
                    [symbolForNodeInstanceId]: "instance-b02",
                  },
                },
                slots: {},
                [symbolForNodeId]: "B-45235",
                [symbolForNodeInstanceId]: "5c4de59f26f55",
              },
            ],
          },
        ],
      } as any,
    ],
    [
      "useChildren is array",
      {
        routeList: [
          {
            id: "R-01",
            path: "/a",
            type: "bricks",
            providers: '["p1"]',
            children: [
              {
                brick: "presentational-bricks.brick-table",
                id: "B-45235",
                instanceId: "5c4de59f26f55",
                mountPoint: "content",
                portal: false,
                properties: '{\n  "useChildren": "[state]"} ',
                type: "brick",
                children: [
                  {
                    brick: "presentational-bricks.brick-value-mapping",
                    id: "B-02",
                    instanceId: "instance-b02",
                    mountPoint: "[state]",
                    type: "brick",
                    properties: '{\n  "fields": "state"}',
                    if: "false",
                    lifeCycle: undefined,
                  },
                  {
                    brick: "presentational-bricks.icon-select",
                    id: "B-03",
                    instanceId: "instance-b03",
                    mountPoint: "[state]",
                    type: "brick",
                    properties: '{\n  "fields": "select"}',
                    if: "false",
                    lifeCycle: undefined,
                  },
                  {
                    brick: "presentational-bricks.more-select",
                    id: "B-04",
                    instanceId: "instance-b04",
                    mountPoint: "[state]",
                    type: "brick",
                    properties: '{\n  "fields": "more"}',
                    if: "false",
                    lifeCycle: undefined,
                  },
                ],
              },
            ],
          },
        ],
        templateList: [],
        menus: [],
        i18n: [],
        dependsAll: false,
        options: {
          keepIds: true,
        },
      },
      {
        dependsAll: false,
        meta: {
          customTemplates: [],
          i18n: {
            en: {},
            zh: {},
          },
          menus: [],
        },
        routes: [
          {
            bricks: [
              {
                iid: "5c4de59f26f55",
                brick: "presentational-bricks.brick-table",
                portal: false,
                properties: {
                  useBrick: [
                    {
                      iid: "instance-b02",
                      brick: "presentational-bricks.brick-value-mapping",
                      if: false,
                      lifeCycle: undefined,
                      properties: {
                        fields: "state",
                      },
                      [symbolForNodeId]: "B-02",
                      [symbolForNodeInstanceId]: "instance-b02",
                    },
                    {
                      iid: "instance-b03",
                      brick: "presentational-bricks.icon-select",
                      if: false,
                      lifeCycle: undefined,
                      properties: {
                        fields: "select",
                      },
                      [symbolForNodeId]: "B-03",
                      [symbolForNodeInstanceId]: "instance-b03",
                    },
                    {
                      iid: "instance-b04",
                      brick: "presentational-bricks.more-select",
                      if: false,
                      lifeCycle: undefined,
                      properties: {
                        fields: "more",
                      },
                      [symbolForNodeId]: "B-04",
                      [symbolForNodeInstanceId]: "instance-b04",
                    },
                  ],
                },
                slots: {},
                [symbolForNodeId]: "B-45235",
                [symbolForNodeInstanceId]: "5c4de59f26f55",
              },
            ],
            path: "/a",
            providers: ["p1"],
            segues: undefined,
            type: "bricks",
            [symbolForNodeId]: "R-01",
          },
        ],
      } as any,
    ],
    [
      "useChildren not found children",
      {
        routeList: [
          {
            id: "R-01",
            path: "/a",
            type: "bricks",
            providers: '["p1"]',
            children: [
              {
                brick: "presentational-bricks.brick-table",
                id: "B-45235",
                instanceId: "5c4de59f26f55",
                mountPoint: "content",
                portal: false,
                properties: '{\n  "useChildren": "state"} ',
                type: "brick",
                children: [
                  {
                    brick: "presentational-bricks.brick-value-mapping",
                    id: "B-02",
                    instanceId: "instance-b02",
                    mountPoint: "[state]",
                    type: "brick",
                    properties: '{\n  "fields": "state"}',
                    if: "false",
                    lifeCycle: undefined,
                  },
                ],
              },
            ],
          },
        ],
        templateList: [],
        menus: [],
        i18n: [],
        dependsAll: false,
        options: {
          keepIds: true,
        },
      },
      {
        dependsAll: false,
        meta: {
          customTemplates: [],
          i18n: {
            en: {},
            zh: {},
          },
          menus: [],
        },
        routes: [
          {
            bricks: [
              {
                [symbolForNodeId]: "B-45235",
                [symbolForNodeInstanceId]: "5c4de59f26f55",
                iid: "5c4de59f26f55",
                brick: "presentational-bricks.brick-table",
                portal: false,
                properties: {
                  useChildren: "state",
                },
                slots: {},
              },
            ],
            path: "/a",
            providers: ["p1"],
            segues: undefined,
            type: "bricks",
            [symbolForNodeId]: "R-01",
          },
        ],
      } as any,
    ],
    [
      "collect contracts",
      {
        app: {
          id: "test-app",
          name: "test-app",
          homepage: "/test-app",
        },
        routeList: [
          {
            id: "R-01",
            instanceId: "instance-r01",
            path: "/a",
            type: "bricks",
            parent: [], // Empty parent also works.
            providers: '["p1"]',
            segues: null,
            context: [
              {
                name: "ttttt",
                resolve: {
                  useProvider: "easyops.api.cmdb.instance@PostSearch:1.1.0",
                  args: ["APP"],
                },
              },
            ],
            // Fields should be removed.
            _ts: 123,
            org: 1,
          },
        ],
        templateList: [],
        menus: [],
        i18n: [],
        functions: [
          {
            name: "sayHello",
            source: "function sayHello() {}",
            description: "Say hello",
          },
          {
            name: "sayExclamation",
            source: "function sayExclamation() {}",
            description: "Say exclamation",
            typescript: true,
          },
        ],
        dependsAll: false,
      },
      // Output
      {
        dependsAll: false,
        meta: {
          contracts: [
            {
              contract: "easyops.api.cmdb.instance.PostSearch",
              type: "contract",
              version: "1.1.0",
            },
          ],
          customTemplates: [],
          functions: [
            {
              name: "sayHello",
              source: "function sayHello() {}",
              typescript: undefined,
            },
            {
              name: "sayExclamation",
              source: "function sayExclamation() {}",
              typescript: true,
            },
          ],
          i18n: {
            en: {},
            zh: {},
          },
          menus: [],
        },
        routes: [
          {
            bricks: [],
            context: [
              {
                name: "ttttt",
                resolve: {
                  args: ["APP"],
                  useProvider: "easyops.api.cmdb.instance@PostSearch:1.1.0",
                },
              },
            ],
            path: "/a",
            providers: ["p1"],
            segues: undefined,
            type: "bricks",
          },
        ],
      },
    ],
  ])("buildStoryboardV2 should work %s", (condition, input, output) => {
    const cloneOfInput = clone(input);
    expect(buildStoryboardV2(input)).toEqual(output);
    // `input` should never be mutated.
    expect(input).toEqual(cloneOfInput);
    expect(consoleError).not.toBeCalled();
  });

  it.each<[string, string, Parameters<typeof buildStoryboardV2>[0]]>([
    [
      "Slot type error",
      "mix bricks in routes mount point",
      {
        routeList: [
          {
            id: "R-01",
            path: "/a",
            type: "bricks",
            children: [
              {
                id: "B-01",
                type: "brick",
                brick: "x",
                children: [
                  {
                    id: "R-02",
                    type: "routes",
                    path: "/b",
                  },
                  {
                    id: "B-02",
                    type: "brick",
                    brick: "y",
                  },
                ],
              },
            ],
          },
        ],
      },
    ],
  ])(
    "buildStoryboardV2 should warn `%s` if %s",
    (message, condition, input) => {
      jest
        .spyOn(dataProvider, "ScanBricksAndTemplates")
        .mockReturnValue({ contractData: "" } as any);
      buildStoryboardV2(input);
      expect(consoleError).toBeCalledTimes(1);
      expect(consoleError.mock.calls[0][0]).toBe(message);
    }
  );
});
Example #14
Source File: buildStoryboard.spec.ts    From next-basics with GNU General Public License v3.0 4 votes vote down vote up
describe("buildStoryboard", () => {
  afterEach(() => {
    jest.clearAllMocks();
  });

  it.each<
    [
      string,
      Parameters<typeof buildStoryboard>[0],
      ReturnType<typeof buildStoryboard>
    ]
  >([
    [
      "",
      // Input
      {
        routeList: [
          {
            id: "R-01",
            instanceId: "instance-r01",
            path: "/a",
            type: "bricks",
            parent: [], // Empty parent also works.
            providers: '["p1"]',
            segues: null,
            // Fields should be removed.
            _ts: 123,
            org: 1,
          },
          {
            id: "R-02",
            instanceId: "instance-r02",
            path: "/b",
            type: "routes",
            permissionsPreCheck:
              '["<% `cmdb:${QUERY.objectId}_instance_create` %>"]',
          },
          {
            id: "R-03",
            instanceId: "instance-r03",
            path: "/b/c",
            type: "bricks",
            parent: [{ id: "R-02" }],
          },
          {
            id: "R-04",
            instanceId: "instance-r04",
            path: "/a/d",
            type: "bricks",
            parent: [{ id: "B-01" }],
            mountPoint: "m2",
          },
          {
            id: "R-05",
            instanceId: "instance-r05",
            path: "/a/e",
            type: "bricks",
            parent: [{ id: "B-01" }],
            mountPoint: "m2",
          },
        ],
        brickList: [
          {
            id: "B-01",
            instanceId: "instance-b01",
            type: "brick",
            brick: "m",
            parent: [{ id: "R-01" }],
            if: "false",
            lifeCycle: undefined,
          },
          {
            id: "B-02",
            instanceId: "instance-b02",
            type: "brick",
            brick: "n",
            parent: [{ id: "R-01" }],
          },
          {
            id: "B-03",
            instanceId: "instance-b03",
            type: "brick",
            brick: "o",
            parent: [{ id: "R-03" }],
          },
          {
            id: "B-04",
            instanceId: "instance-b04",
            type: "brick",
            brick: "p",
            parent: [{ id: "B-01" }],
            mountPoint: "m1",
          },
          {
            id: "B-05",
            instanceId: "instance-b05",
            type: "template",
            brick: "q",
            parent: [{ id: "B-01" }],
            mountPoint: "m1",
          },
          {
            // This brick's parent not found.
            id: "T-01",
            instanceId: "instance-x01",
            type: "brick",
            brick: "t1",
            parent: [{ id: "R-00" }],
          },
          {
            // This brick's grand-parent not found.
            id: "T-02",
            instanceId: "instance-x02",
            type: "brick",
            brick: "t2",
            parent: [{ id: "T-01" }],
          },
        ],
        templateList: [
          {
            templateId: "tpl-01",
            id: "B-T-01",
            proxy: {
              properties: {
                one: {
                  ref: "ref-01",
                  refProperty: "two",
                },
              },
            },
            state: [
              {
                name: "myState",
                value: "any data",
              },
            ],
            children: [
              {
                id: "T-B-01",
                instanceId: "instance-t-b01",
                type: "brick",
                brick: "z",
                children: [
                  {
                    id: "T-B-02",
                    instanceId: "instance-t-b02",
                    type: "brick",
                    brick: "y",
                    ref: "two",
                    mountPoint: "m5",
                    children: [
                      {
                        id: "T-B-03",
                        instanceId: "instance-t-b03",
                        type: "brick",
                        brick: "x",
                        mountPoint: "m6",
                      },
                    ],
                  },
                ],
              },
            ],
          },
        ],
        menus: [
          {
            menuId: "menu-a",
            items: [
              {
                text: "Menu Item 1",
              },
              {
                text: "Menu Item 2",
                children: [
                  {
                    text: "Menu Item 2-1",
                    children: [{ text: "Menu Item 2-1-1" }],
                  },
                ],
              },
            ],
          },
          {
            menuId: "menu-b",
            dynamicItems: true,
            itemsResolve: {
              useProvider: "my.menu-provider",
            },
          },
        ],
        i18n: [
          {
            name: "FILES",
            en: "Files",
            zh: "文件",
          },
          {
            name: "SETTINGS",
            en: "Settings",
            zh: "设置",
          },
        ],
        functions: [
          {
            name: "sayHello",
            source: "function sayHello() {}",
            description: "Say hello",
          },
          {
            name: "sayExclamation",
            source: "function sayExclamation() {}",
            description: "Say exclamation",
            typescript: true,
          },
        ],
        dependsAll: false,
      },
      // Output
      {
        routes: [
          {
            path: "/a",
            type: "bricks",
            providers: ["p1"],
            bricks: [
              {
                iid: "instance-b01",
                brick: "m",
                if: false,
                slots: {
                  m1: {
                    type: "bricks",
                    bricks: [
                      { iid: "instance-b04", brick: "p" },
                      { iid: "instance-b05", template: "q" },
                    ],
                  },
                  m2: {
                    type: "routes",
                    routes: [
                      {
                        path: "/a/d",
                        type: "bricks",
                        bricks: [],
                      },
                      {
                        path: "/a/e",
                        type: "bricks",
                        bricks: [],
                      },
                    ],
                  },
                },
              },
              { iid: "instance-b02", brick: "n" },
            ],
          },
          {
            path: "/b",
            type: "routes",
            permissionsPreCheck: [
              "<% `cmdb:${QUERY.objectId}_instance_create` %>",
            ],
            routes: [
              {
                path: "/b/c",
                type: "bricks",
                bricks: [{ iid: "instance-b03", brick: "o" }],
              },
            ],
          },
        ],
        meta: {
          customTemplates: [
            {
              name: "tpl-01",
              proxy: {
                properties: {
                  one: {
                    ref: "ref-01",
                    refProperty: "two",
                  },
                },
              },
              state: [
                {
                  name: "myState",
                  value: "any data",
                },
              ],
              bricks: [
                {
                  iid: "instance-t-b01",
                  brick: "z",
                  slots: {
                    m5: {
                      type: "bricks",
                      bricks: [
                        {
                          iid: "instance-t-b02",
                          brick: "y",
                          ref: "two",
                          slots: {
                            m6: {
                              type: "bricks",
                              bricks: [{ iid: "instance-t-b03", brick: "x" }],
                            },
                          },
                        },
                      ],
                    },
                  },
                },
              ],
            },
          ],
          menus: [
            {
              menuId: "menu-a",
              items: [
                {
                  text: "Menu Item 1",
                },
                {
                  text: "Menu Item 2",
                  children: [
                    {
                      text: "Menu Item 2-1",
                      children: [
                        {
                          text: "Menu Item 2-1-1",
                        },
                      ],
                    },
                  ],
                },
              ],
            },
            {
              menuId: "menu-b",
              dynamicItems: true,
              itemsResolve: {
                useProvider: "my.menu-provider",
              },
            },
          ],
          i18n: {
            en: {
              FILES: "Files",
              SETTINGS: "Settings",
            },
            zh: {
              FILES: "文件",
              SETTINGS: "设置",
            },
          },
          functions: [
            {
              name: "sayHello",
              source: "function sayHello() {}",
            },
            {
              name: "sayExclamation",
              source: "function sayExclamation() {}",
              typescript: true,
            },
          ],
        },
        dependsAll: false,
      },
    ],
    [
      "test permissionPreCheck",
      // Input
      {
        routeList: [
          {
            id: "R-01",
            path: "/a",
            type: "bricks",
            permissionsPreCheck:
              '["<% `cmdb:${QUERY.objectId}_instance_create` %>"]',
          },
        ],
        brickList: [
          {
            id: "B-01",
            type: "brick",
            brick: "n",
            parent: [{ id: "R-01" }],
            permissionsPreCheck: '["<% CTX.action %>"]',
          },
        ],
        templateList: [
          {
            templateId: "tpl-01",
            children: [
              {
                id: "T-B-01",
                type: "brick",
                brick: "z",
                permissionsPreCheck: "",
                children: [
                  {
                    id: "T-B-02",
                    type: "brick",
                    brick: "y",
                    ref: "two",
                    permissionsPreCheck: '["<% CTX.action %>"]',
                    mountPoint: "m5",
                  },
                ],
              },
            ],
          },
        ],
      },
      // Output
      {
        routes: [
          {
            path: "/a",
            type: "bricks",
            permissionsPreCheck: [
              "<% `cmdb:${QUERY.objectId}_instance_create` %>",
            ],
            bricks: [
              {
                brick: "n",
                permissionsPreCheck: ["<% CTX.action %>"],
              },
            ],
          },
        ],
        meta: {
          customTemplates: [
            {
              name: "tpl-01",
              bricks: [
                {
                  brick: "z",
                  slots: {
                    m5: {
                      type: "bricks",
                      bricks: [
                        {
                          brick: "y",
                          ref: "two",
                          permissionsPreCheck: ["<% CTX.action %>"],
                        },
                      ],
                    },
                  },
                },
              ],
            },
          ],
        },
      },
    ],
    [
      "when a custom template has no bricks",
      // Input
      {
        routeList: [],
        brickList: [],
        templateList: [
          {
            templateId: "menu-a",
          },
        ],
      },
      // Output
      {
        routes: [],
        meta: {
          customTemplates: [
            {
              name: "menu-a",
              bricks: [],
            },
          ],
        },
      },
    ],
    [
      "when it's all empty",
      // Input
      {
        routeList: [],
        brickList: [],
        templateList: [],
        menus: [],
        i18n: [],
        dependsAll: false,
      },
      // Output
      {
        routes: [],
        meta: {
          customTemplates: [],
          menus: [],
          i18n: {
            en: {},
            zh: {},
          },
        },
        dependsAll: false,
      },
    ],
    [
      "test keepIds",
      // Input
      {
        routeList: [
          {
            id: "R-01",
            path: "/a",
            type: "bricks",
            parent: [], // Empty parent also works.
            providers: '["p1"]',
            segues: null,
            // Fields should be removed.
            _ts: 123,
            org: 1,
          },
        ],
        brickList: [
          {
            id: "B-01",
            instanceId: "instance-b01",
            type: "brick",
            brick: "m",
            parent: [{ id: "R-01" }],
            if: "false",
            lifeCycle: undefined,
          },
        ],
        templateList: [
          {
            templateId: "menu-a",
            id: "B-T-01",
            children: [
              {
                id: "T-B-01",
                instanceId: "instance-t-b01",
                type: "brick",
                brick: "z",
              },
            ],
          },
        ],
        menus: [],
        i18n: [],
        dependsAll: false,
        options: {
          keepIds: true,
        },
      },
      // Output,
      {
        routes: [
          {
            [symbolForNodeId]: "R-01",
            path: "/a",
            type: "bricks",
            providers: ["p1"],
            bricks: [
              {
                [symbolForNodeId]: "B-01",
                [symbolForNodeInstanceId]: "instance-b01",
                brick: "m",
                if: false,
                iid: "instance-b01",
              },
            ],
          },
        ],
        meta: {
          customTemplates: [
            {
              [symbolForNodeId]: "B-T-01",
              name: "menu-a",
              bricks: [
                {
                  [symbolForNodeId]: "T-B-01",
                  [symbolForNodeInstanceId]: "instance-t-b01",
                  brick: "z",
                  iid: "instance-t-b01",
                },
              ],
            },
          ],
          menus: [],
          i18n: {
            en: {},
            zh: {},
          },
        },
        dependsAll: false,
      } as any,
    ],
    [
      "useChildren single",
      {
        routeList: [
          {
            id: "R-01",
            path: "/a",
            type: "bricks",
            parent: [], // Empty parent also works.
            providers: '["p1"]',
            segues: null,
            // Fields should be removed.
            _ts: 123,
            org: 1,
          },
        ],
        brickList: [
          {
            brick: "presentational-bricks.brick-table",
            id: "B-45235",
            instanceId: "5c4de59f26f55",
            mountPoint: "content",
            portal: false,
            parent: [{ id: "R-01" }],
            properties: '{\n  "useChildren": "[state]"} ',
            type: "brick",
            slots: {},
            [symbolForNodeId]: "B-45235",
            [symbolForNodeInstanceId]: "5c4de59f26f55",
          },
          {
            brick: "presentational-bricks.brick-value-mapping",
            id: "B-02",
            instanceId: "instance-b02",
            mountPoint: "[state]",
            type: "brick",
            parent: [{ id: "B-45235" }],
            properties: '{\n  "fields": "state"}',
            if: "false",
            lifeCycle: undefined,
          },
        ],
        templateList: [],
        menus: [],
        i18n: [],
        dependsAll: false,
        options: {
          keepIds: true,
        },
      },
      {
        dependsAll: false,
        meta: {
          customTemplates: [],
          i18n: {
            en: {},
            zh: {},
          },
          menus: [],
        },
        routes: [
          {
            [symbolForNodeId]: "R-01",
            path: "/a",
            providers: ["p1"],
            type: "bricks",
            segues: undefined,
            bricks: [
              {
                iid: "5c4de59f26f55",
                brick: "presentational-bricks.brick-table",
                portal: false,
                properties: {
                  useBrick: {
                    iid: "instance-b02",
                    brick: "presentational-bricks.brick-value-mapping",
                    if: false,
                    lifeCycle: undefined,
                    properties: {
                      fields: "state",
                    },
                    [symbolForNodeId]: "B-02",
                    [symbolForNodeInstanceId]: "instance-b02",
                  },
                  [symbolForNodeUseChildren]: "[state]",
                },
                slots: {},
                [symbolForNodeId]: "B-45235",
                [symbolForNodeInstanceId]: "5c4de59f26f55",
              },
            ],
          },
        ],
      } as any,
    ],
    [
      "useChildren is array",
      {
        routeList: [
          {
            id: "R-01",
            path: "/a",
            type: "bricks",
            parent: [], // Empty parent also works.
            providers: '["p1"]',
            segues: null,
            // Fields should be removed.
            _ts: 123,
            org: 1,
          },
        ],
        brickList: [
          {
            brick: "presentational-bricks.brick-table",
            id: "B-45235",
            instanceId: "5c4de59f26f55",
            mountPoint: "content",
            portal: false,
            parent: [{ id: "R-01" }],
            properties: '{\n  "useChildren": "[state]"} ',
            type: "brick",
          },
          {
            brick: "presentational-bricks.brick-value-mapping",
            id: "B-02",
            instanceId: "instance-b02",
            mountPoint: "[state]",
            type: "brick",
            parent: [{ id: "B-45235" }],
            properties: '{\n  "fields": "state"}',
            if: "false",
            lifeCycle: undefined,
          },
          {
            brick: "presentational-bricks.icon-select",
            id: "B-03",
            instanceId: "instance-b03",
            mountPoint: "[state]",
            type: "brick",
            parent: [{ id: "B-45235" }],
            properties: '{\n  "fields": "select"}',
            if: "false",
            lifeCycle: undefined,
          },
          {
            brick: "presentational-bricks.more-select",
            id: "B-04",
            instanceId: "instance-b04",
            mountPoint: "[state]",
            type: "brick",
            parent: [{ id: "B-45235" }],
            properties: '{\n  "fields": "more"}',
            if: "false",
            lifeCycle: undefined,
          },
        ],
        templateList: [],
        menus: [],
        i18n: [],
        dependsAll: false,
        options: {
          keepIds: true,
        },
      },
      {
        dependsAll: false,
        meta: {
          customTemplates: [],
          i18n: {
            en: {},
            zh: {},
          },
          menus: [],
        },
        routes: [
          {
            bricks: [
              {
                iid: "5c4de59f26f55",
                brick: "presentational-bricks.brick-table",
                portal: false,
                properties: {
                  useBrick: [
                    {
                      iid: "instance-b02",
                      brick: "presentational-bricks.brick-value-mapping",
                      if: false,
                      lifeCycle: undefined,
                      properties: {
                        fields: "state",
                      },
                      [symbolForNodeId]: "B-02",
                      [symbolForNodeInstanceId]: "instance-b02",
                    },
                    {
                      iid: "instance-b03",
                      brick: "presentational-bricks.icon-select",
                      if: false,
                      lifeCycle: undefined,
                      properties: {
                        fields: "select",
                      },
                      [symbolForNodeId]: "B-03",
                      [symbolForNodeInstanceId]: "instance-b03",
                    },
                    {
                      iid: "instance-b04",
                      brick: "presentational-bricks.more-select",
                      if: false,
                      lifeCycle: undefined,
                      properties: {
                        fields: "more",
                      },
                      [symbolForNodeId]: "B-04",
                      [symbolForNodeInstanceId]: "instance-b04",
                    },
                  ],
                  [symbolForNodeUseChildren]: "[state]",
                },
                slots: {},
                [symbolForNodeId]: "B-45235",
                [symbolForNodeInstanceId]: "5c4de59f26f55",
              },
            ],
            path: "/a",
            providers: ["p1"],
            segues: undefined,
            type: "bricks",
            [symbolForNodeId]: "R-01",
          },
        ],
      } as any,
    ],
    [
      "useChildren not found children",
      {
        routeList: [
          {
            id: "R-01",
            path: "/a",
            type: "bricks",
            parent: [], // Empty parent also works.
            providers: '["p1"]',
            segues: null,
            // Fields should be removed.
            _ts: 123,
            org: 1,
          },
        ],
        brickList: [
          {
            brick: "presentational-bricks.brick-table",
            id: "B-45235",
            instanceId: "5c4de59f26f55",
            mountPoint: "content",
            portal: false,
            parent: [{ id: "R-01" }],
            properties: '{\n  "useChildren": "state"} ',
            type: "brick",
          },
          {
            brick: "presentational-bricks.brick-value-mapping",
            id: "B-02",
            instanceId: "instance-b02",
            mountPoint: "[state]",
            type: "brick",
            parent: [{ id: "B-45235" }],
            properties: '{\n  "fields": "state"}',
            if: "false",
            lifeCycle: undefined,
          },
        ],
        templateList: [],
        menus: [],
        i18n: [],
        dependsAll: false,
        options: {
          keepIds: true,
        },
      },
      {
        dependsAll: false,
        meta: {
          customTemplates: [],
          i18n: {
            en: {},
            zh: {},
          },
          menus: [],
        },
        routes: [
          {
            bricks: [
              {
                [symbolForNodeId]: "B-45235",
                [symbolForNodeInstanceId]: "5c4de59f26f55",
                iid: "5c4de59f26f55",
                brick: "presentational-bricks.brick-table",
                portal: false,
                properties: {
                  useChildren: "state",
                },
                slots: {
                  "[state]": {
                    bricks: [
                      {
                        iid: "instance-b02",
                        brick: "presentational-bricks.brick-value-mapping",
                        if: false,
                        lifeCycle: undefined,
                        properties: {
                          fields: "state",
                        },
                        [symbolForNodeId]: "B-02",
                        [symbolForNodeInstanceId]: "instance-b02",
                      },
                    ],
                    type: "bricks",
                  },
                },
              },
            ],
            path: "/a",
            providers: ["p1"],
            segues: undefined,
            type: "bricks",
            [symbolForNodeId]: "R-01",
          },
        ],
      } as any,
    ],
  ])("buildStoryboard should work %s", (condition, input, output) => {
    const cloneOfInput = clone(input);
    expect(buildStoryboard(input)).toEqual(output);
    // `input` should never be mutated.
    expect(input).toEqual(cloneOfInput);
    expect(consoleError).not.toBeCalled();
  });

  it.each<[string, string, Parameters<typeof buildStoryboard>[0]]>([
    [
      "Parent error",
      "parent not found",
      {
        routeList: [
          // Ignored if missing `path`.
          {
            id: "R-00",
            type: "bricks",
          } as any,
          {
            id: "R-01",
            path: "/a",
            type: "bricks",
            parent: [{ id: "R-00" }],
          },
        ],
        brickList: [],
      },
    ],
    [
      "Mount type error",
      "parent invalid",
      {
        routeList: [
          {
            id: "R-01",
            path: "/a",
            type: "redirect",
            redirect: '"/a/b"',
          },
          {
            id: "R-02",
            path: "/a/b",
            type: "bricks",
            parent: [{ id: "R-01" }],
          },
        ],
        brickList: [],
      },
    ],
    [
      "Mount type error",
      "child route missing path",
      {
        routeList: [
          {
            id: "R-01",
            path: "/a",
            type: "routes",
          },
          {
            // Missing path.
            id: "R-02",
            type: "bricks",
            parent: [{ id: "R-01" }],
          },
        ],
        brickList: [],
      },
    ],
    [
      "Mount type error",
      "child brick missing brick",
      {
        routeList: [
          {
            id: "R-01",
            path: "/a",
            type: "bricks",
          },
        ],
        brickList: [
          {
            // Missing brick.
            id: "B-01",
            type: "brick",
            parent: [{ id: "R-01" }],
          } as any,
        ],
      },
    ],
    [
      "Slot type error",
      "child brick mount on route slot",
      {
        routeList: [
          {
            id: "R-01",
            path: "/a",
            type: "bricks",
          },
          {
            id: "R-02",
            path: "/a/b",
            type: "bricks",
            parent: [{ id: "B-01" }],
            mountPoint: "m1",
          },
        ],
        brickList: [
          {
            id: "B-01",
            type: "brick",
            brick: "a",
            parent: [{ id: "R-01" }],
          },
          {
            id: "B-02",
            type: "brick",
            brick: "b",
            parent: [{ id: "B-01" }],
            mountPoint: "m1",
          },
        ],
      },
    ],
    [
      "JSON.parse() failed",
      "json field invalid",
      {
        routeList: [
          {
            id: "R-01",
            path: "/a",
            type: "bricks",
            providers: "p1",
          },
        ],
        brickList: [],
      },
    ],
    [
      "Failed to parse yaml string",
      "yaml field invalid",
      {
        routeList: [
          {
            id: "R-01",
            path: "/a",
            type: "bricks",
            permissionsPreCheck: "['a]",
          },
        ],
        brickList: [],
      },
    ],
    [
      "Mount type error",
      "child of a brick missing both path and brick",
      {
        routeList: [
          {
            id: "R-01",
            path: "/a",
            type: "bricks",
          },
        ],
        brickList: [
          {
            id: "B-01",
            type: "brick",
            brick: "a",
            parent: [{ id: "R-01" }],
          },
          {
            // Missing brick.
            id: "B-02",
            type: "brick",
            parent: [{ id: "B-01" }],
            mountPoint: "m1",
          } as any,
        ],
      },
    ],
  ])("buildStoryboard should warn `%s` if %s", (message, condition, input) => {
    buildStoryboard(input);
    expect(consoleError).toBeCalledTimes(1);
    expect(consoleError.mock.calls[0][0]).toBe(message);
  });

  it("should throw if direct circular nodes found", () => {
    expect(() => {
      buildStoryboard({
        brickList: [
          {
            id: "B-01",
            type: "brick",
            brick: "a",
            parent: [{ id: "B-01" }],
          },
        ],
        routeList: [],
      });
    }).toThrowError("Circular nodes found: B-01,B-01");
  });

  it("should throw if indirect circular nodes found", () => {
    expect(() => {
      buildStoryboard({
        brickList: [
          {
            id: "B-01",
            type: "brick",
            brick: "a",
            parent: [{ id: "B-02" }],
          },
          {
            id: "B-02",
            type: "brick",
            brick: "b",
            parent: [{ id: "B-01" }],
          },
        ],
        routeList: [],
      });
    }).toThrowError("Circular nodes found: B-01,B-02,B-01");
  });
});
Example #15
Source File: text.ts    From S2 with MIT License 4 votes vote down vote up
drawObjectText = (
  cell: S2CellType,
  multiData?: MultiData,
  // 绘制指标列头需要禁用
  disabledConditions?: boolean,
) => {
  const { x } = cell.getTextAndIconPosition(0).text;
  const {
    y,
    height: totalTextHeight,
    width: totalTextWidth,
  } = cell.getContentArea();
  const text = multiData || (cell.getMeta().fieldValue as MultiData);
  const { values: textValues } = text;
  const { valuesCfg } = cell?.getMeta().spreadsheet.options.style.cellCfg;
  const textCondition = disabledConditions ? null : valuesCfg?.conditions?.text;
  if (!isArray(textValues)) {
    drawBullet(textValues, cell);
    return;
  }

  const widthPercent = valuesCfg?.widthPercent;
  const dataCellStyle = cell.getStyle(CellTypes.DATA_CELL);
  const { textAlign } = dataCellStyle.text;
  const padding = dataCellStyle.cell.padding;

  const realHeight = totalTextHeight / (textValues.length + 1);
  let labelHeight = 0;
  // 绘制单元格主标题
  if (text?.label) {
    labelHeight = realHeight / 2;
    const labelStyle = dataCellStyle.bolderText;

    renderText(
      cell,
      [],
      calX(x, padding.right),
      y + labelHeight,
      getEllipsisText({
        text: text.label,
        maxWidth: totalTextWidth,
        fontParam: labelStyle,
      }),
      labelStyle,
    );
  }

  // 绘制指标
  let curText: string | number;
  let curX: number;
  let curY: number = y + realHeight / 2;
  let curWidth: number;
  let totalWidth = 0;
  for (let i = 0; i < textValues.length; i++) {
    curY = y + realHeight * (i + 1) + labelHeight; // 加上label的高度
    totalWidth = 0;
    const measures = clone(textValues[i]);
    if (textAlign === 'right') {
      reverse(measures); // 右对齐拿到的x坐标为最右坐标,指标顺序需要反过来
    }

    for (let j = 0; j < measures.length; j++) {
      curText = measures[j];
      const curStyle = getTextStyle(
        i,
        j,
        cell?.getMeta() as ViewMeta,
        curText,
        dataCellStyle,
        textCondition,
      );
      curWidth = !isEmpty(widthPercent)
        ? totalTextWidth * (widthPercent[j] / 100)
        : totalTextWidth / text.values[0].length; // 指标个数相同,任取其一即可

      curX = calX(x, padding.right, totalWidth, textAlign);
      totalWidth += curWidth;
      const { placeholder } = cell?.getMeta().spreadsheet.options;
      const emptyPlaceholder = getEmptyPlaceholder(
        cell?.getMeta(),
        placeholder,
      );
      renderText(
        cell,
        [],
        curX,
        curY,
        getEllipsisText({
          text: curText,
          maxWidth: curWidth,
          fontParam: curStyle,
          placeholder: emptyPlaceholder,
        }),
        curStyle,
      );
    }
  }
}
Example #16
Source File: index.ts    From S2 with MIT License 4 votes vote down vote up
copyData = (
  sheetInstance: SpreadSheet,
  split: string,
  formatOptions?: FormatOptions,
): string => {
  const { isFormatHeader, isFormatData } = getFormatOptions(formatOptions);
  const { rowsHierarchy, rowLeafNodes, colLeafNodes, getCellMeta } =
    sheetInstance?.facet?.layoutResult;
  const { maxLevel } = rowsHierarchy;
  const { valueInCols } = sheetInstance.dataCfg.fields;
  // Generate the table header.
  const rowsHeader = rowsHierarchy.sampleNodesForAllLevels.map((item) =>
    sheetInstance.dataSet.getFieldName(item.key),
  );

  // get max query property length
  const rowLength = rowLeafNodes.reduce((pre, cur) => {
    const length = cur.query ? Object.keys(cur.query).length : 0;
    return length > pre ? length : pre;
  }, 0);

  // Generate the table body.
  let detailRows = [];
  let maxRowLength = 0;

  if (!sheetInstance.isPivotMode()) {
    detailRows = processValueInDetail(sheetInstance, split, isFormatData);
  } else {
    // Filter out the related row head leaf nodes.
    const caredRowLeafNodes = rowLeafNodes.filter((row) => row.height !== 0);

    for (const rowNode of caredRowLeafNodes) {
      let tempLine = [];
      if (isFormatHeader) {
        tempLine = getRowNodeFormatData(rowNode);
      } else {
        // Removing the space at the beginning of the line of the label.
        rowNode.label = trim(rowNode?.label);
        const id = rowNode.id.replace(ROOT_BEGINNING_REGEX, '');
        tempLine = id.split(ID_SEPARATOR);
      }
      // TODO 兼容下钻,需要获取下钻最大层级
      const totalLevel = maxLevel + 1;
      const emptyLength = totalLevel - tempLine.length;
      if (emptyLength > 0) {
        tempLine.push(...new Array(emptyLength));
      }

      // 指标挂行头且为平铺模式下,获取指标名称
      const lastLabel = sheetInstance.dataSet.getFieldName(last(tempLine));
      tempLine[tempLine.length - 1] = lastLabel;

      for (const colNode of colLeafNodes) {
        if (valueInCols) {
          const viewMeta = getCellMeta(rowNode.rowIndex, colNode.colIndex);
          tempLine.push(
            processValueInCol(viewMeta, sheetInstance, isFormatData),
          );
        } else {
          const viewMeta = getCellMeta(rowNode.rowIndex, colNode.colIndex);
          const lintItem = processValueInRow(
            viewMeta,
            sheetInstance,
            isFormatData,
          );
          if (isArray(lintItem)) {
            tempLine = tempLine.concat(...lintItem);
          } else {
            tempLine.push(lintItem);
          }
        }
      }
      maxRowLength = max([tempLine.length, maxRowLength]);
      const lineString = tempLine
        .map((value) => getCsvString(value))
        .join(split);

      detailRows.push(lineString);
    }
  }

  // Generate the table header.
  let headers: string[][] = [];

  if (isEmpty(colLeafNodes) && !sheetInstance.isPivotMode()) {
    // when there is no column in detail mode
    headers = [rowsHeader];
  } else {
    // 当列头label为array时用于补全其他层级的label
    let arrayLength = 0;
    // Get the table header of Columns.
    let tempColHeader = clone(colLeafNodes).map((colItem) => {
      let curColItem = colItem;

      const tempCol = [];

      // Generate the column dimensions.
      while (curColItem.level !== undefined) {
        let label = getHeaderLabel(curColItem.label);
        if (isArray(label)) {
          arrayLength = max([arrayLength, size(label)]);
        } else {
          // label 为数组时不进行格式化
          label = isFormatHeader ? getNodeFormatLabel(curColItem) : label;
        }
        tempCol.push(label);
        curColItem = curColItem.parent;
      }
      return tempCol;
    });

    if (arrayLength > 1) {
      tempColHeader = processColHeaders(tempColHeader, arrayLength);
    }

    const colLevels = tempColHeader.map((colHeader) => colHeader.length);
    const colLevel = max(colLevels);

    const colHeader: string[][] = [];
    // Convert the number of column dimension levels to the corresponding array.
    for (let i = colLevel - 1; i >= 0; i -= 1) {
      // The map of data set: key-name
      const colHeaderItem = tempColHeader
        // total col completion
        .map((item) =>
          item.length < colLevel
            ? [...new Array(colLevel - item.length), ...item]
            : item,
        )
        .map((item) => item[i])
        .map((colItem) => sheetInstance.dataSet.getFieldName(colItem));
      colHeader.push(flatten(colHeaderItem));
    }

    // Generate the table header.
    headers = colHeader.map((item, index) => {
      if (sheetInstance.isPivotMode()) {
        const { columns, rows, data } = sheetInstance.facet.cornerHeader.cfg;
        const colNodes = data.filter(
          ({ cornerType }) => cornerType === CornerNodeType.Col,
        );
        const rowNodes = data.filter(
          ({ cornerType }) => cornerType === CornerNodeType.Row,
        );

        if (index < colHeader.length - 1) {
          return [
            ...Array(rowLength - 1).fill(''),
            colNodes.find(({ field }) => field === columns[index])?.label || '',
            ...item,
          ];
        }
        if (index < colHeader.length) {
          return [
            ...rows.map(
              (row) => rowNodes.find(({ field }) => field === row)?.label || '',
            ),
            ...item,
          ];
        }

        return rowsHeader.concat(...item);
      }

      return index < colHeader.length
        ? Array(rowLength)
            .fill('')
            .concat(...item)
        : rowsHeader.concat(...item);
    });
  }

  const headerRow = headers
    .map((header) => {
      const emptyLength = maxRowLength - header.length;
      if (emptyLength > 0) {
        header.unshift(...new Array(emptyLength));
      }
      return header.map((h) => getCsvString(h)).join(split);
    })
    .join('\r\n');

  const data = [headerRow].concat(detailRows);
  const result = data.join('\r\n');
  return result;
}
Example #17
Source File: classes-spec.ts    From ui5-language-assistant with Apache License 2.0 4 votes vote down vote up
describe("The ui5-language-assistant xml-views-completion", () => {
  let ui5Model: UI5SemanticModel;
  before(async function () {
    ui5Model = await generateModel({
      version: "1.74.0",
      modelGenerator: generate,
    });
  });

  context("UI5 Classes Suggestions", () => {
    context("applicable scenarios", () => {
      context("classes at the document's root", () => {
        context("no prefix", () => {
          it("will suggest **all** Controls at the top level", () => {
            const xmlSnippet = `
              <⇶
            `;

            testSuggestionsScenario({
              model: ui5Model,
              xmlText: xmlSnippet,
              providers: {
                elementName: [classesSuggestions],
              },
              assertion: (suggestions) => {
                const baseControl = ui5Model.classes["sap.ui.core.Control"];
                expect(suggestions).to.have.length.greaterThan(200);
                forEach(suggestions, (_) => {
                  expect(_.ui5Node.kind).to.equal("UI5Class");
                  const superClasses = getSuperClasses(_.ui5Node as UI5Class);
                  // Chai's `.include` is super slow, we must implement it ourselves...
                  const doesSuggestionExtendsControl =
                    find(superClasses, baseControl) !== undefined;
                  expect(
                    doesSuggestionExtendsControl || _.ui5Node === baseControl
                  ).to.be.true;
                });
              },
            });
          });
        });

        context("prefix without xmlns", () => {
          it("will suggest **only** classes matching `sap.ui.core.Control` (not Element) type and the **prefix**", () => {
            const xmlSnippet = `
              <QuickView⇶
            `;

            testSuggestionsScenario({
              model: ui5Model,
              xmlText: xmlSnippet,
              providers: {
                elementName: [classesSuggestions],
              },
              assertion: (suggestions) => {
                assertSuggestionProperties(suggestions, undefined);
                const suggestionNames = map(suggestions, (_) =>
                  ui5NodeToFQN(_.ui5Node)
                );
                expect(suggestionNames).to.deep.equalInAnyOrder([
                  "sap.m.QuickView",
                  "sap.m.QuickViewBase",
                  "sap.m.QuickViewCard",
                  "sap.m.QuickViewPage",
                  "sap.ui.ux3.QuickView",
                ]);

                const quickViewGroup = ui5Model.classes["sap.m.QuickViewGroup"];
                expect(quickViewGroup).to.exist;
                expect(quickViewGroup.extends).to.equal(
                  ui5Model.classes["sap.ui.core.Element"]
                );
                expect(suggestionNames).to.not.include([
                  "sap.m.QuickViewGroup",
                ]);
              },
            });
          });
        });
      });

      context("classes under (implicit) default aggregations", () => {
        context("no prefix", () => {
          it("will suggest **all** classes matching the type of the default aggregation", () => {
            const xmlSnippet = `
            <mvc:View
              xmlns:mvc="sap.ui.core.mvc"
              xmlns="sap.m">
              <ActionSheet>
                <⇶
              </ActionSheet>
            </mvc:View>`;

            testSuggestionsScenario({
              model: ui5Model,
              xmlText: xmlSnippet,
              providers: {
                elementName: [classesSuggestions],
              },
              assertion: (suggestions) => {
                assertSuggestionProperties(suggestions, "ActionSheet");
                const suggestionNames = map(suggestions, (_) =>
                  ui5NodeToFQN(_.ui5Node)
                );
                // Can "manually" traverse expected graph of `sap.m.Button` subClasses here:
                //   - https://sapui5.hana.ondemand.com/1.74.0/#/api/sap.m.Button
                expect(suggestionNames).to.deep.equalInAnyOrder([
                  "sap.m.Button",
                  "sap.m.OverflowToolbarButton",
                  "sap.m.OverflowToolbarToggleButton",
                  "sap.m.ToggleButton",
                  "sap.uxap.ObjectPageHeaderActionButton",
                  "sap.suite.ui.commons.ProcessFlowConnectionLabel",
                  "sap.ushell.ui.footerbar.AddBookmarkButton",
                ]);
              },
            });
          });
        });

        context("prefix without xmlns", () => {
          it("will suggest **only** classes matching **both** the type of the default aggregation and the **prefix**", () => {
            const xmlSnippet = `
            <mvc:View
              xmlns:mvc="sap.ui.core.mvc"
              xmlns="sap.m">
              <ActionSheet>
                <Overflow⇶
              </ActionSheet>
            </mvc:View>`;

            testSuggestionsScenario({
              model: ui5Model,
              xmlText: xmlSnippet,
              providers: {
                elementName: [classesSuggestions],
              },
              assertion: (suggestions) => {
                assertSuggestionProperties(suggestions, "ActionSheet");
                const suggestionNames = map(suggestions, (_) =>
                  ui5NodeToFQN(_.ui5Node)
                );
                // Can "manually" traverse expected graph of `sap.m.Button` subClasses here:
                //   - https://ui5.sap.com/1.74.0/#/api/sap.m.Button
                expect(suggestionNames).to.deep.equalInAnyOrder([
                  "sap.m.OverflowToolbarButton",
                  "sap.m.OverflowToolbarToggleButton",
                ]);
              },
            });
          });
        });

        context("prefix with xmlns", () => {
          it("will suggest **only** classes matching **both** the type of the default aggregation and the **xmlns prefix**", () => {
            const xmlSnippet = `
            <mvc:View
              xmlns:mvc="sap.ui.core.mvc"
              xmlns:bamba="sap.m">
              <bamba:ActionSheet>
                <bamba:Overflow⇶
              </bamba:ActionSheet>
            </mvc:View>`;

            testSuggestionsScenario({
              model: ui5Model,
              xmlText: xmlSnippet,
              providers: {
                elementName: [classesSuggestions],
              },
              assertion: (suggestions) => {
                assertSuggestionProperties(suggestions, "ActionSheet");
                const suggestionNames = map(suggestions, (_) =>
                  ui5NodeToFQN(_.ui5Node)
                );
                // Can "manually" traverse expected graph of `sap.m.Button` subClasses here:
                //   - https://ui5.sap.com/1.74.0/#/api/sap.m.Button
                expect(suggestionNames).to.deep.equalInAnyOrder([
                  "sap.m.OverflowToolbarButton",
                  "sap.m.OverflowToolbarToggleButton",
                ]);
              },
            });
          });
        });
      });

      context("classes under an explicit aggregation", () => {
        context("no prefix", () => {
          it("will suggest **all** classes matching the type of the **explicit aggregation**", () => {
            const xmlSnippet = `
            <mvc:View
              xmlns:mvc="sap.ui.core.mvc"
              xmlns="sap.m">
              <mvc:layoutData>
                <⇶
              </mvc:layoutData>
            </mvc:View>`;

            testSuggestionsScenario({
              model: ui5Model,
              xmlText: xmlSnippet,
              providers: {
                elementName: [classesSuggestions],
              },
              assertion: (suggestions) => {
                assertSuggestionProperties(suggestions, "layoutData");
                const suggestionNames = map(suggestions, (_) =>
                  ui5NodeToFQN(_.ui5Node)
                );
                // Can "manually" traverse expected graph of `sap.ui.core.LayoutData` subClasses here:
                //   - https://sapui5.hana.ondemand.com/1.74.0/#/api/sap.ui.core.LayoutData
                expect(suggestionNames).to.deep.equalInAnyOrder([
                  "sap.ui.core.VariantLayoutData",
                  "sap.f.GridContainerItemLayoutData",
                  "sap.m.FlexItemData",
                  "sap.m.ToolbarLayoutData",
                  "sap.m.OverflowToolbarLayoutData",
                  "sap.ui.layout.BlockLayoutCellData",
                  "sap.ui.layout.cssgrid.GridItemLayoutData",
                  "sap.ui.layout.cssgrid.ResponsiveColumnItemLayoutData",
                  "sap.ui.layout.form.ColumnContainerData",
                  "sap.ui.layout.form.ColumnElementData",
                  "sap.ui.layout.form.GridContainerData",
                  "sap.ui.commons.form.GridContainerData",
                  "sap.ui.layout.form.GridElementData",
                  "sap.ui.commons.form.GridElementData",
                  "sap.ui.layout.GridData",
                  "sap.ui.layout.ResponsiveFlowLayoutData",
                  "sap.ui.commons.layout.ResponsiveFlowLayoutData",
                  "sap.ui.layout.SplitterLayoutData",
                  "sap.uxap.ObjectPageHeaderLayoutData",
                  "sap.ui.vk.FlexibleControlLayoutData",
                ]);
              },
            });
          });

          it("will suggest classes that implement the interface when the aggregation has an interface type", () => {
            const xmlSnippet = `
            <mvc:View
              xmlns:mvc="sap.ui.core.mvc"
              xmlns="sap.m">
              <Page>
                <footer>
                  <⇶
                </footer>
                </Page>
            </mvc:View>`;

            testSuggestionsScenario({
              model: ui5Model,
              xmlText: xmlSnippet,
              providers: {
                elementName: [classesSuggestions],
              },
              assertion: (suggestions) => {
                assertSuggestionProperties(suggestions, "footer");
                const suggestionNames = map(suggestions, (_) =>
                  ui5NodeToFQN(_.ui5Node)
                );
                expect(suggestionNames).to.deep.equalInAnyOrder([
                  "sap.f.ShellBar",
                  "sap.m.Bar",
                  "sap.m.OverflowToolbar",
                  "sap.gantt.simple.ContainerToolbar",
                  "sap.tnt.ToolHeader",
                  "sap.uxap.AnchorBar",
                  "sap.m.Toolbar",
                ]);
              },
            });
          });
        });

        context("prefix without xmlns", () => {
          it("will suggest **all** classes matching the type of the **explicit aggregation**", () => {
            const xmlSnippet = `
            <mvc:View
              xmlns:mvc="sap.ui.core.mvc"
              xmlns="sap.m">
              <mvc:layoutData>
                <GridContainer⇶
              </mvc:layoutData>
            </mvc:View>`;

            testSuggestionsScenario({
              model: ui5Model,
              xmlText: xmlSnippet,
              providers: {
                elementName: [classesSuggestions],
              },
              assertion: (suggestions) => {
                assertSuggestionProperties(suggestions, "layoutData");
                const suggestionNames = map(suggestions, (_) =>
                  ui5NodeToFQN(_.ui5Node)
                );
                // Can "manually" traverse expected graph of `sap.ui.core.LayoutData` subClasses here:
                //   - https://sapui5.hana.ondemand.com/1.74.0/#/api/sap.ui.core.LayoutData
                expect(suggestionNames).to.deep.equalInAnyOrder([
                  "sap.f.GridContainerItemLayoutData",
                  "sap.ui.layout.form.GridContainerData",
                  "sap.ui.commons.form.GridContainerData",
                ]);
              },
            });
          });
        });

        context("prefix with xmlns", () => {
          context("xmlns usage with text after the colon", () => {
            it("will suggest **only** classes matching **both** the type of the default aggregation and the **xmlns prefix**", () => {
              const xmlSnippet = `
            <mvc:View
              xmlns:mvc="sap.ui.core.mvc"
              xmlns="sap.m"
              xmlns:forms="sap.ui.commons.form"
              >
              <mvc:layoutData>
                <forms:GridContainer⇶
              </mvc:layoutData>
            </mvc:View>`;

              testSuggestionsScenario({
                model: ui5Model,
                xmlText: xmlSnippet,
                providers: {
                  elementName: [classesSuggestions],
                },
                assertion: (suggestions) => {
                  assertSuggestionProperties(suggestions, "layoutData");
                  const suggestionNames = map(suggestions, (_) =>
                    ui5NodeToFQN(_.ui5Node)
                  );
                  // Can "manually" traverse expected graph of `sap.ui.core.LayoutData` subClasses here:
                  //   - https://sapui5.hana.ondemand.com/1.74.0/#/api/sap.ui.core.LayoutData
                  expect(suggestionNames).to.deep.equalInAnyOrder([
                    "sap.ui.commons.form.GridContainerData",
                  ]);
                },
              });
            });

            it("will only suggest classes from the specified namespace and not its sub-namespace", () => {
              const xmlSnippet = `
                <mvc:View
                  xmlns:mvc="sap.ui.core.mvc"
                  xmlns:core="sap.ui.core"
                  >
                  <mvc:content>
                    <core:Con⇶
                  </mvc:content>
                </mvc:View>`;

              testSuggestionsScenario({
                model: ui5Model,
                xmlText: xmlSnippet,
                providers: {
                  elementName: [classesSuggestions],
                },
                assertion: (suggestions) => {
                  assertSuggestionProperties(suggestions, "content");
                  const suggestionNames = map(suggestions, (_) =>
                    ui5NodeToFQN(_.ui5Node)
                  );
                  expect(suggestionNames).to.deep.equalInAnyOrder([
                    "sap.ui.core.ComponentContainer",
                  ]);
                  expect(suggestionNames).to.not.include(
                    "sap.ui.core.tmpl.TemplateControl"
                  );
                },
              });
            });
          });

          context(
            "xmlns usage with the prefix only (nothing after colon)",
            () => {
              it("will suggest **only** classes matching **both** the type of the default aggregation and the **xmlns prefix**", () => {
                const xmlSnippet = `
            <mvc:View
              xmlns:mvc="sap.ui.core.mvc"
              xmlns="sap.m"
              xmlns:forms="sap.ui.commons.form"
              >
              <mvc:layoutData>
                <forms:⇶
              </mvc:layoutData>
            </mvc:View>`;

                testSuggestionsScenario({
                  model: ui5Model,
                  xmlText: xmlSnippet,
                  providers: {
                    elementName: [classesSuggestions],
                  },
                  assertion: (suggestions) => {
                    assertSuggestionProperties(suggestions, "layoutData");
                    const suggestionNames = map(suggestions, (_) =>
                      ui5NodeToFQN(_.ui5Node)
                    );
                    // Can "manually" traverse expected graph of `sap.ui.core.LayoutData` subClasses here:
                    //   - https://sapui5.hana.ondemand.com/1.74.0/#/api/sap.ui.core.LayoutData
                    expect(suggestionNames).to.deep.equalInAnyOrder([
                      "sap.ui.commons.form.GridContainerData",
                      "sap.ui.commons.form.GridElementData",
                    ]);
                  },
                });
              });

              it("will only suggest classes from the specified namespace and not its sub-namespaces", () => {
                const xmlSnippet = `
                  <mvc:View
                    xmlns:mvc="sap.ui.core.mvc"
                    xmlns:core="sap.ui.core"
                    >
                    <mvc:content>
                      <core:⇶
                    </mvc:content>
                  </mvc:View>`;

                testSuggestionsScenario({
                  model: ui5Model,
                  xmlText: xmlSnippet,
                  providers: {
                    elementName: [classesSuggestions],
                  },
                  assertion: (suggestions) => {
                    assertSuggestionProperties(suggestions, "content");
                    const suggestionNames = map(suggestions, (_) =>
                      ui5NodeToFQN(_.ui5Node)
                    );
                    expect(suggestionNames).to.deep.equalInAnyOrder([
                      "sap.ui.core.ComponentContainer",
                      "sap.ui.core.HTML",
                      "sap.ui.core.Icon",
                      "sap.ui.core.InvisibleText",
                      "sap.ui.core.LocalBusyIndicator",
                      "sap.ui.core.ScrollBar",
                    ]);
                    expect(suggestionNames).to.not.include(
                      "sap.ui.core.mvc.View"
                    );
                  },
                });
              });
            }
          );
        });
      });
    });

    context("none applicable scenarios", () => {
      it("will offer no suggestions which are abstract classes", () => {
        const xmlSnippet = `
            <mvc:View
              xmlns:mvc="sap.ui.core.mvc"
              xmlns="sap.m">
               <mvc:layoutData>
                <ComboBox⇶
              </mvc:layoutData>
            </mvc:View>`;

        testSuggestionsScenario({
          model: ui5Model,
          xmlText: xmlSnippet,
          providers: {
            elementName: [classesSuggestions],
          },
          assertion: (suggestions) => {
            expect(ui5Model.classes).to.have.property("sap.m.ComboBoxBase");
            expect(ui5Model.classes["sap.m.ComboBoxBase"].abstract).to.be.true;
            assertSuggestionProperties(suggestions, "layoutData");
            const suggestionNames = map(suggestions, (_) =>
              ui5NodeToFQN(_.ui5Node)
            );
            expect(suggestionNames).to.not.include("sap.m.ComboBoxBase");
          },
        });
      });

      it("will offer no suggestions in an aggregation with cardinality 0..1 which is already 'full'", () => {
        const xmlSnippet = `
            <mvc:View
              xmlns:mvc="sap.ui.core.mvc"
              xmlns="sap.m">
              <mvc:layoutData>
                <⇶
                <ToolbarLayoutData>
                </ToolbarLayoutData>
              </mvc:layoutData>
            </mvc:View>`;

        testSuggestionsScenario({
          model: ui5Model,
          xmlText: xmlSnippet,
          providers: {
            elementName: [classesSuggestions],
          },
          assertion: (suggestions) => {
            expect(suggestions).to.be.empty;
          },
        });
      });

      it("will offer no suggestions when under a tag with an empty name", () => {
        const xmlSnippet = `
            <mvc:View
              xmlns:mvc="sap.ui.core.mvc"
              xmlns="sap.m">
              <>
                <⇶
              </>
            </mvc:View>`;

        testSuggestionsScenario({
          model: ui5Model,
          xmlText: xmlSnippet,
          providers: {
            elementName: [classesSuggestions],
          },
          assertion: (suggestions) => {
            expect(suggestions).to.be.empty;
          },
        });
      });

      it("will offer no suggestions when under a tag with only namespace", () => {
        const xmlSnippet = `
            <mvc:View
              xmlns:mvc="sap.ui.core.mvc"
              xmlns="sap.m">
              <mvc:>
                <⇶
              </mvc:>
            </mvc:View>`;

        testSuggestionsScenario({
          model: ui5Model,
          xmlText: xmlSnippet,
          providers: {
            elementName: [classesSuggestions],
          },
          assertion: (suggestions) => {
            expect(suggestions).to.be.empty;
          },
        });
      });

      it("will offer no suggestion when the parent tag is not a recognized class or aggreation", () => {
        const xmlSnippet = `
            <mvc:View
              xmlns:mvc="sap.ui.core.mvc"
              xmlns="sap.m">
              <_ActionSheet>
                <⇶
              </_ActionSheet>
            </mvc:View>`;

        testSuggestionsScenario({
          model: ui5Model,
          xmlText: xmlSnippet,
          providers: {
            elementName: [classesSuggestions],
          },
          assertion: (suggestions) => {
            expect(suggestions).to.be.empty;
          },
        });
      });

      it("will offer no suggestions inside an explicit aggregation when the matching UI5 class in unrecognized", () => {
        const xmlSnippet = `
            <mvc:ViewTypo
              xmlns:mvc="sap.ui.core.mvc"
              xmlns="sap.m">
              <mvc:layoutData>
                <⇶
              </mvc:layoutData>
            </mvc:ViewTypo>`;

        testSuggestionsScenario({
          model: ui5Model,
          xmlText: xmlSnippet,
          providers: {
            elementName: [classesSuggestions],
          },
          assertion: (suggestions) => {
            expect(suggestions).to.be.empty;
          },
        });
      });

      it("will offer no suggestions inside an explicit aggregation when the aggregation name is not recognized", () => {
        const xmlSnippet = `
            <mvc:View
              xmlns:mvc="sap.ui.core.mvc"
              xmlns="sap.m">
              <mvc:layoutDataTypo>
                <⇶
              </mvc:layoutDataTypo>
            </mvc:View>`;

        testSuggestionsScenario({
          model: ui5Model,
          xmlText: xmlSnippet,
          providers: {
            elementName: [classesSuggestions],
          },
          assertion: (suggestions) => {
            expect(suggestions).to.be.empty;
          },
        });
      });

      it("will offer no suggestions inside an explicit aggregation when the aggregation namespace is not recognized", () => {
        const xmlSnippet = `
            <mvc:View
              xmlns:mvc="sap.ui.core.mvc"
              xmlns="sap.m">
              <mvcTypo:layoutData>
                <⇶
              </mvcTypo:layoutData>
            </mvc:View>`;

        testSuggestionsScenario({
          model: ui5Model,
          xmlText: xmlSnippet,
          providers: {
            elementName: [classesSuggestions],
          },
          assertion: (suggestions) => {
            expect(suggestions).to.be.empty;
          },
        });
      });

      it("will offer no suggestions inside an explicit aggregation when the prefix has an invalid URI", () => {
        const xmlSnippet = `
            <mvc:View
              xmlns:mvc="sap.ui.core.mvc">
              <mvc:layoutData>
                <mvcTypo:⇶
              </mvc:layoutData>
            </mvc:View>`;

        testSuggestionsScenario({
          model: ui5Model,
          xmlText: xmlSnippet,
          providers: {
            elementName: [classesSuggestions],
          },
          assertion: (suggestions) => {
            expect(suggestions).to.be.empty;
          },
        });
      });

      it("will offer no suggestions inside an explicit aggregation when the aggregation type is not a UI5Class or UI5Interface", () => {
        const clonedModel = cloneDeep(ui5Model);
        const viewClass = clonedModel.classes["sap.ui.core.mvc.View"];
        const contentAggregation = find(
          viewClass.aggregations,
          (_) => _.name === "content"
        ) as UI5Aggregation;
        expect(contentAggregation).to.exist;
        const contentWithInvalidType = clone(contentAggregation);
        contentWithInvalidType.type = undefined;
        viewClass.aggregations = [contentWithInvalidType];

        const xmlSnippet = `
            <mvc:View
              xmlns:mvc="sap.ui.core.mvc"
              xmlns="sap.m">
              <mvc:content>
                <⇶
              </mvc:content>
            </mvc:View>`;

        testSuggestionsScenario({
          model: clonedModel,
          xmlText: xmlSnippet,
          providers: {
            elementName: [classesSuggestions],
          },
          assertion: (suggestions) => {
            expect(suggestions).to.be.empty;
          },
        });
      });

      it("will offer no suggestions inside a default (implicit) aggregation when the matching UI5 class in unrecognized", () => {
        const xmlSnippet = `
            <mvc:ViewTypo
              xmlns:mvc="sap.ui.core.mvc"
              xmlns="sap.m">
                <⇶
            </mvc:ViewTypo>`;

        testSuggestionsScenario({
          model: ui5Model,
          xmlText: xmlSnippet,
          providers: {
            elementName: [classesSuggestions],
          },
          assertion: (suggestions) => {
            expect(suggestions).to.be.empty;
          },
        });
      });

      it("will offer no suggestions inside a default (implicit) aggregation when the aggregation type is not a UI5Class or UI5Interface", () => {
        const clonedModel = cloneDeep(ui5Model);
        const carouselClass = clonedModel.classes["sap.m.Carousel"];
        const pagesAggregation = find(
          carouselClass.aggregations,
          (_) => _.name === "pages"
        ) as UI5Aggregation;
        // TODO: can we do supply better type signatures for chai.expect?
        expect(pagesAggregation).to.exist;
        expect(carouselClass.defaultAggregation?.name).to.equal(
          pagesAggregation?.name
        );
        const pagesWithInvalidType = clone(pagesAggregation);
        pagesWithInvalidType.type = undefined;
        carouselClass.aggregations = [pagesWithInvalidType];
        carouselClass.defaultAggregation = pagesWithInvalidType;

        const xmlSnippet = `
            <Carousel
              xmlns="sap.m">
                <⇶
            </Carousel>`;

        testSuggestionsScenario({
          model: clonedModel,
          xmlText: xmlSnippet,
          providers: {
            elementName: [classesSuggestions],
          },
          assertion: (suggestions) => {
            expect(suggestions).to.be.empty;
          },
        });
      });
    });
  });
});