From ae493aaee760f3299e82d7f808808fda076eaf4b Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Tue, 1 Oct 2024 18:37:26 +0100 Subject: [PATCH 1/6] feat: Add MoreMenu component and related functionality for enhanced UI interactions --- Common/UI/Components/Card/Card.tsx | 45 ++++++++++--------- .../Components/Card/CardButtons/MoreButton.ts | 15 +++++++ .../ModelDetail/CardModelDetail.tsx | 4 +- .../Components/ModelTable/BaseModelTable.tsx | 36 ++++++++++++++- Common/UI/Components/MoreMenu/Divider.tsx | 12 +++++ Common/UI/Components/MoreMenu/MoreMenu.tsx | 41 +++++++++++++++++ .../UI/Components/MoreMenu/MoreMenuItem.tsx | 24 ++++++++++ .../src/Components/Monitor/MonitorTable.tsx | 3 ++ 8 files changed, 156 insertions(+), 24 deletions(-) create mode 100644 Common/UI/Components/Card/CardButtons/MoreButton.ts create mode 100644 Common/UI/Components/MoreMenu/Divider.tsx create mode 100644 Common/UI/Components/MoreMenu/MoreMenu.tsx create mode 100644 Common/UI/Components/MoreMenu/MoreMenuItem.tsx diff --git a/Common/UI/Components/Card/Card.tsx b/Common/UI/Components/Card/Card.tsx index d28a993bcfc..16619ba18f2 100644 --- a/Common/UI/Components/Card/Card.tsx +++ b/Common/UI/Components/Card/Card.tsx @@ -17,7 +17,7 @@ export interface CardButtonSchema { export interface ComponentProps { title?: string | ReactElement; description?: string | ReactElement; - buttons?: undefined | Array; + buttons?: undefined | Array; children?: undefined | Array | ReactElement; className?: string | undefined; bodyClassName?: string | undefined; @@ -57,34 +57,39 @@ const Card: FunctionComponent = (
{props.rightElement} - {props.buttons?.map((button: CardButtonSchema, i: number) => { + {props.buttons?.map((button: CardButtonSchema | ReactElement, i: number) => { return (
0 ? { - marginLeft: "10px", - } + marginLeft: "10px", + } : {} } key={i} > -
); })} diff --git a/Common/UI/Components/Card/CardButtons/MoreButton.ts b/Common/UI/Components/Card/CardButtons/MoreButton.ts new file mode 100644 index 00000000000..34d0ec4b720 --- /dev/null +++ b/Common/UI/Components/Card/CardButtons/MoreButton.ts @@ -0,0 +1,15 @@ +import { ButtonStyleType } from "../../Button/Button"; +import { CardButtonSchema } from "../Card"; +import IconProp from "Common/Types/Icon/IconProp"; + +type GetButtonFunctionType = () => CardButtonSchema; + +export const getMoreButton: GetButtonFunctionType = (): CardButtonSchema => { + return { + title: "", + buttonStyle: ButtonStyleType.ICON, + onClick: () => {}, + disabled: false, + icon: IconProp.More, + }; +}; diff --git a/Common/UI/Components/ModelDetail/CardModelDetail.tsx b/Common/UI/Components/ModelDetail/CardModelDetail.tsx index b2463f024bc..34c1408304f 100644 --- a/Common/UI/Components/ModelDetail/CardModelDetail.tsx +++ b/Common/UI/Components/ModelDetail/CardModelDetail.tsx @@ -39,7 +39,7 @@ const CardModelDetail: ( ) => ReactElement = ( props: ComponentProps, ): ReactElement => { - const [cardButtons, setCardButtons] = useState>([]); + const [cardButtons, setCardButtons] = useState>([]); const [showModel, setShowModal] = useState(false); const [item, setItem] = useState(null); const [refresher, setRefresher] = useState(false); @@ -65,7 +65,7 @@ const CardModelDetail: ( ), ) || User.isMasterAdmin(); - let cardButtons: Array = []; + let cardButtons: Array = []; if (props.isEditable && hasPermissionToEdit) { cardButtons.push({ diff --git a/Common/UI/Components/ModelTable/BaseModelTable.tsx b/Common/UI/Components/ModelTable/BaseModelTable.tsx index feb41ae3ab4..d645a857374 100644 --- a/Common/UI/Components/ModelTable/BaseModelTable.tsx +++ b/Common/UI/Components/ModelTable/BaseModelTable.tsx @@ -84,6 +84,8 @@ import React, { useEffect, useState, } from "react"; +import MoreMenu from "../MoreMenu/MoreMenu"; +import MoreMenuItem from "../MoreMenu/MoreMenuItem"; export enum ShowAs { Table, @@ -91,6 +93,11 @@ export enum ShowAs { OrderedStatesList, } + +export interface SaveFilterProps { + tableId: string; +} + export interface BaseTableCallbacks< TBaseModel extends BaseModel | AnalyticsBaseModel, > { @@ -207,6 +214,8 @@ export interface BaseTableProps< onShowFormType?: (formType: ModalType) => void; initialFilterData?: FilterData | undefined; + + saveFilterProps?: SaveFilterProps | undefined; } export interface ComponentProps< @@ -254,7 +263,7 @@ const BaseModelTable: ( Array> >([]); - const [cardButtons, setCardButtons] = useState>([]); + const [cardButtons, setCardButtons] = useState>([]); const [actionButtonSchema, setActionButtonSchema] = useState< Array> @@ -784,9 +793,25 @@ const BaseModelTable: ( return selectFields; }; + + const getSaveFilterDropdown: GetReactElementFunction = (): ReactElement => { + if(!props.saveFilterProps){ + return <> + } + + if(props.saveFilterProps && props.saveFilterProps.tableId){ + return ( + { + }}> + ) + } + + return <> + } + const setHeaderButtons: VoidFunction = (): void => { // add header buttons. - let headerbuttons: Array = []; + let headerbuttons: Array = []; if (props.cardProps?.buttons && props.cardProps?.buttons.length > 0) { headerbuttons = [...props.cardProps.buttons]; @@ -838,6 +863,7 @@ const BaseModelTable: ( }); } + if (showFilterButton) { headerbuttons.push({ title: "", @@ -854,6 +880,10 @@ const BaseModelTable: ( }); } + if(props.saveFilterProps){ + headerbuttons.push(getSaveFilterDropdown()); + } + setCardButtons(headerbuttons); }; @@ -1611,6 +1641,8 @@ const BaseModelTable: ( ); }; + + const getCardComponent: GetReactElementFunction = (): ReactElement => { if (showAs === ShowAs.Table || showAs === ShowAs.List) { return ( diff --git a/Common/UI/Components/MoreMenu/Divider.tsx b/Common/UI/Components/MoreMenu/Divider.tsx new file mode 100644 index 00000000000..fdaddad4cad --- /dev/null +++ b/Common/UI/Components/MoreMenu/Divider.tsx @@ -0,0 +1,12 @@ +import React, { FunctionComponent, ReactElement } from "react"; + +const MoreMenuItem: FunctionComponent = ( + +): ReactElement => { + return ( +
+
+ ); +}; + +export default MoreMenuItem; diff --git a/Common/UI/Components/MoreMenu/MoreMenu.tsx b/Common/UI/Components/MoreMenu/MoreMenu.tsx new file mode 100644 index 00000000000..452dfc8cbdb --- /dev/null +++ b/Common/UI/Components/MoreMenu/MoreMenu.tsx @@ -0,0 +1,41 @@ +import React, { FunctionComponent, ReactElement, useEffect, useState } from "react"; +import Button, { ButtonStyleType } from "../Button/Button"; +import IconProp from "../../../Types/Icon/IconProp"; +import useComponentOutsideClick from "../../Types/UseComponentOutsideClick"; + +export interface ComponentProps { + children: Array | ReactElement; +} + +const MoreMenu: FunctionComponent = ( + props: ComponentProps, +): ReactElement => { + + const { ref, isComponentVisible, setIsComponentVisible } = + useComponentOutsideClick(false); + + + + const [isDropdownVisible, setDropdownVisible] = useState(false); + + useEffect(() => { + setDropdownVisible(isComponentVisible); + }, [isComponentVisible]); + + return ( +
+
+
+ + {isComponentVisible &&
+ {props.children} +
} +
+ + ); +}; + +export default MoreMenu; diff --git a/Common/UI/Components/MoreMenu/MoreMenuItem.tsx b/Common/UI/Components/MoreMenu/MoreMenuItem.tsx new file mode 100644 index 00000000000..9babc158e17 --- /dev/null +++ b/Common/UI/Components/MoreMenu/MoreMenuItem.tsx @@ -0,0 +1,24 @@ +import React, { FunctionComponent, ReactElement } from "react"; +import IconProp from "../../../Types/Icon/IconProp"; +import Icon from "../Icon/Icon"; + +export interface ComponentProps { + icon?: IconProp | undefined; + text: string; + onClick: () => void; +} + +const MoreMenuItem: FunctionComponent = ( + props: ComponentProps, +): ReactElement => { + return ( + { + props.onClick(); + }}> + {props.icon && } + {props.text} + + ); +}; + +export default MoreMenuItem; diff --git a/Dashboard/src/Components/Monitor/MonitorTable.tsx b/Dashboard/src/Components/Monitor/MonitorTable.tsx index 19961a053a3..0b171344b5c 100644 --- a/Dashboard/src/Components/Monitor/MonitorTable.tsx +++ b/Dashboard/src/Components/Monitor/MonitorTable.tsx @@ -56,6 +56,9 @@ const MonitorsTable: FunctionComponent = ( modelType={Monitor} name="Monitors" id="Monitors-table" + saveFilterProps={{ + tableId: "monitor-table", + }} bulkActions={{ buttons: [ { From 7c941da7dfb7ac02b22382791de98df78a4d39f2 Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Wed, 2 Oct 2024 15:07:19 +0100 Subject: [PATCH 2/6] add table view table --- Common/Models/DatabaseModels/Index.ts | 3 + Common/Models/DatabaseModels/TableView.ts | 455 ++++++++++++++++++ .../1727877987596-MigrationName.ts | 22 + Common/Types/Permission.ts | 36 ++ 4 files changed, 516 insertions(+) create mode 100644 Common/Models/DatabaseModels/TableView.ts create mode 100644 Common/Server/Infrastructure/Postgres/SchemaMigrations/1727877987596-MigrationName.ts diff --git a/Common/Models/DatabaseModels/Index.ts b/Common/Models/DatabaseModels/Index.ts index 61cce3cb809..a7c7fb3bbc6 100644 --- a/Common/Models/DatabaseModels/Index.ts +++ b/Common/Models/DatabaseModels/Index.ts @@ -142,6 +142,7 @@ import ScheduledMaintenanceTemplateOwnerTeam from "./ScheduledMaintenanceTemplat import ScheduledMaintenanceTemplateOwnerUser from "./ScheduledMaintenanceTemplateOwnerUser"; import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel"; +import TableView from "./TableView"; const AllModelTypes: Array<{ new (): BaseModel; @@ -305,6 +306,8 @@ const AllModelTypes: Array<{ TelemetryIngestionKey, TelemetryException, + + TableView ]; const modelTypeMap: { [key: string]: { new (): BaseModel } } = {}; diff --git a/Common/Models/DatabaseModels/TableView.ts b/Common/Models/DatabaseModels/TableView.ts new file mode 100644 index 00000000000..53407976196 --- /dev/null +++ b/Common/Models/DatabaseModels/TableView.ts @@ -0,0 +1,455 @@ +import Project from "./Project"; +import User from "./User"; +import Route from "../../Types/API/Route"; +import { PlanType } from "../../Types/Billing/SubscriptionPlan"; +import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl"; +import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl"; +import TableBillingAccessControl from "../../Types/Database/AccessControl/TableBillingAccessControl"; +import ColumnLength from "../../Types/Database/ColumnLength"; +import ColumnType from "../../Types/Database/ColumnType"; +import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint"; +import EnableDocumentation from "../../Types/Database/EnableDocumentation"; +import EnableWorkflow from "../../Types/Database/EnableWorkflow"; +import TableColumn from "../../Types/Database/TableColumn"; +import TableColumnType from "../../Types/Database/TableColumnType"; +import TableMetadata from "../../Types/Database/TableMetadata"; +import TenantColumn from "../../Types/Database/TenantColumn"; +import IconProp from "../../Types/Icon/IconProp"; +import ObjectID from "../../Types/ObjectID"; +import Permission from "../../Types/Permission"; +import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm"; +import Query from "../../Types/BaseDatabase/Query"; +import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel"; +import Sort from "../../Types/BaseDatabase/Sort"; + +@TableBillingAccessControl({ + create: PlanType.Growth, + read: PlanType.Free, + update: PlanType.Growth, + delete: PlanType.Growth, +}) +@EnableDocumentation() +@TenantColumn("projectId") +@TableAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.CreateTableView, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadTableView, + ], + delete: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.DeleteTableView, + ], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.EditTableView, + ], +}) +@EnableWorkflow({ + create: true, + delete: true, + update: true, + read: true, +}) +@CrudApiEndpoint(new Route("/table-view")) +@TableMetadata({ + tableName: "TableView", + singularName: "Table View", + pluralName: "Table Views", + icon: IconProp.TableCells, + tableDescription: + "Table View is view settings for a table in a project. It contains columns, filters, and other settings.", +}) +@Entity({ + name: "TableView", +}) +export default class TableView extends BaseModel { + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.CreateTableView, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadTableView, + ], + update: [], + }) + @TableColumn({ + manyToOneRelationColumn: "projectId", + type: TableColumnType.Entity, + modelType: Project, + title: "Project", + description: "Relation to Project Resource in which this object belongs", + }) + @ManyToOne( + () => { + return Project; + }, + { + eager: false, + nullable: true, + onDelete: "CASCADE", + orphanedRowAction: "nullify", + }, + ) + @JoinColumn({ name: "projectId" }) + public project?: Project = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.CreateTableView, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadTableView, + ], + update: [], + }) + @Index() + @TableColumn({ + type: TableColumnType.ObjectID, + required: true, + canReadOnRelationQuery: true, + title: "Project ID", + description: "ID of your OneUptime Project in which this object belongs", + }) + @Column({ + type: ColumnType.ObjectID, + nullable: false, + transformer: ObjectID.getDatabaseTransformer(), + }) + public projectId?: ObjectID = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.CreateTableView, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadTableView, + ], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.EditTableView, + ], + }) + @TableColumn({ + required: true, + type: TableColumnType.ShortText, + canReadOnRelationQuery: true, + title: "Name", + description: "Any friendly name of this object", + }) + @Column({ + nullable: false, + type: ColumnType.ShortText, + length: ColumnLength.ShortText, + }) + public name?: string = undefined; + + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.CreateTableView, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadTableView, + ], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.EditTableView, + ], + }) + @TableColumn({ + required: true, + type: TableColumnType.ShortText, + canReadOnRelationQuery: true, + title: "Table ID", + description: "ID of the table this view is for", + }) + @Column({ + nullable: false, + type: ColumnType.ShortText, + length: ColumnLength.ShortText, + }) + public tableId?: string = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.CreateTableView, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadTableView, + ], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.EditTableView, + ], + }) + @TableColumn({ + required: false, + type: TableColumnType.LongText, + title: "Description", + description: "Friendly description that will help you remember", + }) + @Column({ + nullable: true, + type: ColumnType.LongText, + length: ColumnLength.LongText, + }) + public description?: string = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.CreateTableView, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadTableView, + ], + update: [], + }) + @TableColumn({ + manyToOneRelationColumn: "createdByUserId", + type: TableColumnType.Entity, + modelType: User, + title: "Created by User", + description: + "Relation to User who created this object (if this object was created by a User)", + }) + @ManyToOne( + () => { + return User; + }, + { + eager: false, + nullable: true, + onDelete: "SET NULL", + orphanedRowAction: "nullify", + }, + ) + @JoinColumn({ name: "createdByUserId" }) + public createdByUser?: User = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.CreateTableView, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadTableView, + ], + update: [], + }) + @TableColumn({ + type: TableColumnType.ObjectID, + title: "Created by User ID", + description: + "User ID who created this object (if this object was created by a User)", + }) + @Column({ + type: ColumnType.ObjectID, + nullable: true, + transformer: ObjectID.getDatabaseTransformer(), + }) + public createdByUserId?: ObjectID = undefined; + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadTableView, + ], + update: [], + }) + @TableColumn({ + manyToOneRelationColumn: "deletedByUserId", + type: TableColumnType.Entity, + title: "Deleted by User", + description: + "Relation to User who deleted this object (if this object was deleted by a User)", + }) + @ManyToOne( + () => { + return User; + }, + { + cascade: false, + eager: false, + nullable: true, + onDelete: "SET NULL", + orphanedRowAction: "nullify", + }, + ) + @JoinColumn({ name: "deletedByUserId" }) + public deletedByUser?: User = undefined; + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadTableView, + ], + update: [], + }) + @TableColumn({ + type: TableColumnType.ObjectID, + title: "Deleted by User ID", + description: + "User ID who deleted this object (if this object was deleted by a User)", + }) + @Column({ + type: ColumnType.ObjectID, + nullable: true, + transformer: ObjectID.getDatabaseTransformer(), + }) + public deletedByUserId?: ObjectID = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.CreateTableView, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadTableView, + ], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.EditTableView, + ], + }) + @TableColumn({ + title: "Filters", + required: true, + unique: false, + type: TableColumnType.JSON, + canReadOnRelationQuery: true, + description: "Filters for this table view", + }) + @Column({ + type: ColumnType.JSON, + unique: false, + nullable: false, + }) + public filters?: Query = undefined; + + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.CreateTableView, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadTableView, + ], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.EditTableView, + ], + }) + @TableColumn({ + title: "Sort", + required: true, + unique: false, + type: TableColumnType.JSON, + canReadOnRelationQuery: true, + description: "Sort for this table view", + }) + @Column({ + type: ColumnType.JSON, + unique: false, + nullable: false, + }) + public sort?: Sort = undefined; + + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.CreateTableView, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadTableView, + ], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.EditTableView, + ], + }) + @TableColumn({ + title: "Items on Page", + required: true, + unique: false, + type: TableColumnType.Number, + canReadOnRelationQuery: true, + description: "Items on page", + }) + @Column({ + type: ColumnType.Number, + unique: false, + nullable: false, + default: 10 + }) + public itemsOnPage?: number = undefined; + +} diff --git a/Common/Server/Infrastructure/Postgres/SchemaMigrations/1727877987596-MigrationName.ts b/Common/Server/Infrastructure/Postgres/SchemaMigrations/1727877987596-MigrationName.ts new file mode 100644 index 00000000000..b00e7d1c495 --- /dev/null +++ b/Common/Server/Infrastructure/Postgres/SchemaMigrations/1727877987596-MigrationName.ts @@ -0,0 +1,22 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class MigrationName1727877987596 implements MigrationInterface { + public name = 'MigrationName1727877987596' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "TableView" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "projectId" uuid NOT NULL, "name" character varying(100) NOT NULL, "tableId" character varying(100) NOT NULL, "description" character varying(500), "createdByUserId" uuid, "deletedByUserId" uuid, "filters" jsonb NOT NULL, "sort" jsonb NOT NULL, "itemsOnPage" integer NOT NULL DEFAULT '10', CONSTRAINT "PK_1e17a8834a65403cc87c4ead0cc" PRIMARY KEY ("_id"))`); + await queryRunner.query(`CREATE INDEX "IDX_a8691d4e023d907b5c73a43e42" ON "TableView" ("projectId") `); + await queryRunner.query(`ALTER TABLE "TableView" ADD CONSTRAINT "FK_a8691d4e023d907b5c73a43e424" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "TableView" ADD CONSTRAINT "FK_2f2b7d1e199910951a0b5933d92" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "TableView" ADD CONSTRAINT "FK_b36d769f3d3d6fe4f9c35984551" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "TableView" DROP CONSTRAINT "FK_b36d769f3d3d6fe4f9c35984551"`); + await queryRunner.query(`ALTER TABLE "TableView" DROP CONSTRAINT "FK_2f2b7d1e199910951a0b5933d92"`); + await queryRunner.query(`ALTER TABLE "TableView" DROP CONSTRAINT "FK_a8691d4e023d907b5c73a43e424"`); + await queryRunner.query(`DROP INDEX "public"."IDX_a8691d4e023d907b5c73a43e42"`); + await queryRunner.query(`DROP TABLE "TableView"`); + } + +} diff --git a/Common/Types/Permission.ts b/Common/Types/Permission.ts index d1b649c112a..016db6c154d 100644 --- a/Common/Types/Permission.ts +++ b/Common/Types/Permission.ts @@ -534,6 +534,11 @@ enum Permission { DeleteServiceCopilotCodeRepository = "DeleteServiceCopilotCodeRepository", EditServiceCopilotCodeRepository = "EditServiceCopilotCodeRepository", ReadServiceCopilotCodeRepository = "ReadServiceCopilotCodeRepository", + + CreateTableView = "CreateTableView", + DeleteTableView = "DeleteTableView", + EditTableView = "EditTableView", + ReadTableView = "ReadTableView", } export class PermissionHelper { @@ -806,6 +811,37 @@ export class PermissionHelper { isAccessControlPermission: false, }, + // Table view permissions + + { + permission: Permission.CreateTableView, + title: "Create Table View", + description: "This permission can create table views of this project.", + isAssignableToTenant: true, + isAccessControlPermission: false, + }, + { + permission: Permission.DeleteTableView, + title: "Delete Table View", + description: "This permission can delete table views of this project.", + isAssignableToTenant: true, + isAccessControlPermission: false, + }, + { + permission: Permission.EditTableView, + title: "Edit Table View", + description: "This permission can edit table views of this project.", + isAssignableToTenant: true, + isAccessControlPermission: false, + }, + { + permission: Permission.ReadTableView, + title: "Read Table View", + description: "This permission can read table views of this project.", + isAssignableToTenant: true, + isAccessControlPermission: false, + }, + { permission: Permission.CreateProjectLabel, title: "Create Label", From c833bcb37ab7313baf9e50c6f4fe694a81dc2bc7 Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Wed, 2 Oct 2024 15:13:37 +0100 Subject: [PATCH 3/6] add table filters model --- App/FeatureSet/BaseAPI/Index.ts | 12 +++ Common/Models/DatabaseModels/Index.ts | 2 +- Common/Models/DatabaseModels/TableView.ts | 6 +- .../1727877987596-MigrationName.ts | 49 ++++++++---- Common/Server/Services/Index.ts | 4 + Common/Server/Services/TableViewService.ts | 10 +++ Common/UI/Components/Card/Card.tsx | 74 ++++++++++--------- .../ModelDetail/CardModelDetail.tsx | 4 +- .../Components/ModelTable/BaseModelTable.tsx | 30 ++++---- Common/UI/Components/MoreMenu/Divider.tsx | 9 +-- Common/UI/Components/MoreMenu/MoreMenu.tsx | 65 +++++++++------- .../UI/Components/MoreMenu/MoreMenuItem.tsx | 33 ++++++--- 12 files changed, 179 insertions(+), 119 deletions(-) create mode 100644 Common/Server/Services/TableViewService.ts diff --git a/App/FeatureSet/BaseAPI/Index.ts b/App/FeatureSet/BaseAPI/Index.ts index 20eb5d10157..402dc1f951d 100644 --- a/App/FeatureSet/BaseAPI/Index.ts +++ b/App/FeatureSet/BaseAPI/Index.ts @@ -55,6 +55,9 @@ import IncidentInternalNoteService, { import IncidentNoteTemplateService, { Service as IncidentNoteTemplateServiceType, } from "Common/Server/Services/IncidentNoteTemplateService"; +import TableViewService, { + Service as TableViewServiceType, +} from "Common/Server/Services/TableViewService"; import IncidentOwnerTeamService, { Service as IncidentOwnerTeamServiceType, } from "Common/Server/Services/IncidentOwnerTeamService"; @@ -443,6 +446,7 @@ import ScheduledMaintenanceTemplateOwnerTeamService, { import ScheduledMaintenanceTemplateOwnerUserService, { Service as ScheduledMaintenanceTemplateOwnerUserServiceType, } from "Common/Server/Services/ScheduledMaintenanceTemplateOwnerUserService"; +import TableView from "Common/Models/DatabaseModels/TableView"; const BaseAPIFeatureSet: FeatureSet = { init: async (): Promise => { @@ -776,6 +780,14 @@ const BaseAPIFeatureSet: FeatureSet = { ).getRouter(), ); + app.use( + `/${APP_NAME.toLocaleLowerCase()}`, + new BaseAPI( + TableView, + TableViewService, + ).getRouter(), + ); + app.use( `/${APP_NAME.toLocaleLowerCase()}`, new BaseAPI( diff --git a/Common/Models/DatabaseModels/Index.ts b/Common/Models/DatabaseModels/Index.ts index a7c7fb3bbc6..9b69d354009 100644 --- a/Common/Models/DatabaseModels/Index.ts +++ b/Common/Models/DatabaseModels/Index.ts @@ -307,7 +307,7 @@ const AllModelTypes: Array<{ TelemetryException, - TableView + TableView, ]; const modelTypeMap: { [key: string]: { new (): BaseModel } } = {}; diff --git a/Common/Models/DatabaseModels/TableView.ts b/Common/Models/DatabaseModels/TableView.ts index 53407976196..f60b29cb80b 100644 --- a/Common/Models/DatabaseModels/TableView.ts +++ b/Common/Models/DatabaseModels/TableView.ts @@ -168,7 +168,6 @@ export default class TableView extends BaseModel { }) public name?: string = undefined; - @ColumnAccessControl({ create: [ Permission.ProjectOwner, @@ -383,7 +382,6 @@ export default class TableView extends BaseModel { }) public filters?: Query = undefined; - @ColumnAccessControl({ create: [ Permission.ProjectOwner, @@ -417,7 +415,6 @@ export default class TableView extends BaseModel { }) public sort?: Sort = undefined; - @ColumnAccessControl({ create: [ Permission.ProjectOwner, @@ -448,8 +445,7 @@ export default class TableView extends BaseModel { type: ColumnType.Number, unique: false, nullable: false, - default: 10 + default: 10, }) public itemsOnPage?: number = undefined; - } diff --git a/Common/Server/Infrastructure/Postgres/SchemaMigrations/1727877987596-MigrationName.ts b/Common/Server/Infrastructure/Postgres/SchemaMigrations/1727877987596-MigrationName.ts index b00e7d1c495..f062ce79a89 100644 --- a/Common/Server/Infrastructure/Postgres/SchemaMigrations/1727877987596-MigrationName.ts +++ b/Common/Server/Infrastructure/Postgres/SchemaMigrations/1727877987596-MigrationName.ts @@ -1,22 +1,39 @@ import { MigrationInterface, QueryRunner } from "typeorm"; export class MigrationName1727877987596 implements MigrationInterface { - public name = 'MigrationName1727877987596' + public name = "MigrationName1727877987596"; - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`CREATE TABLE "TableView" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "projectId" uuid NOT NULL, "name" character varying(100) NOT NULL, "tableId" character varying(100) NOT NULL, "description" character varying(500), "createdByUserId" uuid, "deletedByUserId" uuid, "filters" jsonb NOT NULL, "sort" jsonb NOT NULL, "itemsOnPage" integer NOT NULL DEFAULT '10', CONSTRAINT "PK_1e17a8834a65403cc87c4ead0cc" PRIMARY KEY ("_id"))`); - await queryRunner.query(`CREATE INDEX "IDX_a8691d4e023d907b5c73a43e42" ON "TableView" ("projectId") `); - await queryRunner.query(`ALTER TABLE "TableView" ADD CONSTRAINT "FK_a8691d4e023d907b5c73a43e424" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "TableView" ADD CONSTRAINT "FK_2f2b7d1e199910951a0b5933d92" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "TableView" ADD CONSTRAINT "FK_b36d769f3d3d6fe4f9c35984551" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "TableView" DROP CONSTRAINT "FK_b36d769f3d3d6fe4f9c35984551"`); - await queryRunner.query(`ALTER TABLE "TableView" DROP CONSTRAINT "FK_2f2b7d1e199910951a0b5933d92"`); - await queryRunner.query(`ALTER TABLE "TableView" DROP CONSTRAINT "FK_a8691d4e023d907b5c73a43e424"`); - await queryRunner.query(`DROP INDEX "public"."IDX_a8691d4e023d907b5c73a43e42"`); - await queryRunner.query(`DROP TABLE "TableView"`); - } + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "TableView" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "projectId" uuid NOT NULL, "name" character varying(100) NOT NULL, "tableId" character varying(100) NOT NULL, "description" character varying(500), "createdByUserId" uuid, "deletedByUserId" uuid, "filters" jsonb NOT NULL, "sort" jsonb NOT NULL, "itemsOnPage" integer NOT NULL DEFAULT '10', CONSTRAINT "PK_1e17a8834a65403cc87c4ead0cc" PRIMARY KEY ("_id"))`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_a8691d4e023d907b5c73a43e42" ON "TableView" ("projectId") `, + ); + await queryRunner.query( + `ALTER TABLE "TableView" ADD CONSTRAINT "FK_a8691d4e023d907b5c73a43e424" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "TableView" ADD CONSTRAINT "FK_2f2b7d1e199910951a0b5933d92" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "TableView" ADD CONSTRAINT "FK_b36d769f3d3d6fe4f9c35984551" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + } + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "TableView" DROP CONSTRAINT "FK_b36d769f3d3d6fe4f9c35984551"`, + ); + await queryRunner.query( + `ALTER TABLE "TableView" DROP CONSTRAINT "FK_2f2b7d1e199910951a0b5933d92"`, + ); + await queryRunner.query( + `ALTER TABLE "TableView" DROP CONSTRAINT "FK_a8691d4e023d907b5c73a43e424"`, + ); + await queryRunner.query( + `DROP INDEX "public"."IDX_a8691d4e023d907b5c73a43e42"`, + ); + await queryRunner.query(`DROP TABLE "TableView"`); + } } diff --git a/Common/Server/Services/Index.ts b/Common/Server/Services/Index.ts index e745e4ce016..20d983fdcc7 100644 --- a/Common/Server/Services/Index.ts +++ b/Common/Server/Services/Index.ts @@ -137,6 +137,8 @@ import ScheduledMaintenanceTemplateService from "./ScheduledMaintenanceTemplateS import ScheduledMaintenanceTemplateOwnerTeamService from "./ScheduledMaintenanceTemplateOwnerTeamService"; import ScheduledMaintenanceTemplateOwnerUserService from "./ScheduledMaintenanceTemplateOwnerUserService"; +import TableViewService from "./TableViewService"; + const services: Array = [ AcmeCertificateService, PromoCodeService, @@ -285,6 +287,8 @@ const services: Array = [ ScheduledMaintenanceTemplateService, ScheduledMaintenanceTemplateOwnerTeamService, ScheduledMaintenanceTemplateOwnerUserService, + + TableViewService, ]; export const AnalyticsServices: Array< diff --git a/Common/Server/Services/TableViewService.ts b/Common/Server/Services/TableViewService.ts new file mode 100644 index 00000000000..7748afa8ab9 --- /dev/null +++ b/Common/Server/Services/TableViewService.ts @@ -0,0 +1,10 @@ +import DatabaseService from "./DatabaseService"; +import TableView from "Common/Models/DatabaseModels/TableView"; + +export class Service extends DatabaseService { + public constructor() { + super(TableView); + } +} + +export default new Service(); diff --git a/Common/UI/Components/Card/Card.tsx b/Common/UI/Components/Card/Card.tsx index 16619ba18f2..d4cb2888e96 100644 --- a/Common/UI/Components/Card/Card.tsx +++ b/Common/UI/Components/Card/Card.tsx @@ -57,42 +57,46 @@ const Card: FunctionComponent = (
{props.rightElement} - {props.buttons?.map((button: CardButtonSchema | ReactElement, i: number) => { - return ( -
0 - ? { - marginLeft: "10px", - } - : {} - } - key={i} - > - - {React.isValidElement(button) ? button : null} - {React.isValidElement(button) ? null : ( -
- ); - })} + className={(button as CardButtonSchema).className} + onClick={() => { + if ((button as CardButtonSchema).onClick) { + (button as CardButtonSchema).onClick(); + } + }} + disabled={(button as CardButtonSchema).disabled} + icon={(button as CardButtonSchema).icon} + shortcutKey={ + (button as CardButtonSchema).shortcutKey + } + dataTestId="card-button" + isLoading={(button as CardButtonSchema).isLoading} + /> + )} +
+ ); + }, + )} diff --git a/Common/UI/Components/ModelDetail/CardModelDetail.tsx b/Common/UI/Components/ModelDetail/CardModelDetail.tsx index 34c1408304f..4a82dd61978 100644 --- a/Common/UI/Components/ModelDetail/CardModelDetail.tsx +++ b/Common/UI/Components/ModelDetail/CardModelDetail.tsx @@ -39,7 +39,9 @@ const CardModelDetail: ( ) => ReactElement = ( props: ComponentProps, ): ReactElement => { - const [cardButtons, setCardButtons] = useState>([]); + const [cardButtons, setCardButtons] = useState< + Array + >([]); const [showModel, setShowModal] = useState(false); const [item, setItem] = useState(null); const [refresher, setRefresher] = useState(false); diff --git a/Common/UI/Components/ModelTable/BaseModelTable.tsx b/Common/UI/Components/ModelTable/BaseModelTable.tsx index d645a857374..851313263db 100644 --- a/Common/UI/Components/ModelTable/BaseModelTable.tsx +++ b/Common/UI/Components/ModelTable/BaseModelTable.tsx @@ -93,7 +93,6 @@ export enum ShowAs { OrderedStatesList, } - export interface SaveFilterProps { tableId: string; } @@ -263,7 +262,9 @@ const BaseModelTable: ( Array> >([]); - const [cardButtons, setCardButtons] = useState>([]); + const [cardButtons, setCardButtons] = useState< + Array + >([]); const [actionButtonSchema, setActionButtonSchema] = useState< Array> @@ -793,21 +794,21 @@ const BaseModelTable: ( return selectFields; }; - const getSaveFilterDropdown: GetReactElementFunction = (): ReactElement => { - if(!props.saveFilterProps){ - return <> + if (!props.saveFilterProps) { + return <>; } - if(props.saveFilterProps && props.saveFilterProps.tableId){ - return ( - { - }}> - ) + if (props.saveFilterProps && props.saveFilterProps.tableId) { + return ( + + {}}> + + ); } - return <> - } + return <>; + }; const setHeaderButtons: VoidFunction = (): void => { // add header buttons. @@ -863,7 +864,6 @@ const BaseModelTable: ( }); } - if (showFilterButton) { headerbuttons.push({ title: "", @@ -880,7 +880,7 @@ const BaseModelTable: ( }); } - if(props.saveFilterProps){ + if (props.saveFilterProps) { headerbuttons.push(getSaveFilterDropdown()); } @@ -1641,8 +1641,6 @@ const BaseModelTable: ( ); }; - - const getCardComponent: GetReactElementFunction = (): ReactElement => { if (showAs === ShowAs.Table || showAs === ShowAs.List) { return ( diff --git a/Common/UI/Components/MoreMenu/Divider.tsx b/Common/UI/Components/MoreMenu/Divider.tsx index fdaddad4cad..fc884f5540b 100644 --- a/Common/UI/Components/MoreMenu/Divider.tsx +++ b/Common/UI/Components/MoreMenu/Divider.tsx @@ -1,12 +1,7 @@ import React, { FunctionComponent, ReactElement } from "react"; -const MoreMenuItem: FunctionComponent = ( - -): ReactElement => { - return ( -
-
- ); +const MoreMenuItem: FunctionComponent = (): ReactElement => { + return
; }; export default MoreMenuItem; diff --git a/Common/UI/Components/MoreMenu/MoreMenu.tsx b/Common/UI/Components/MoreMenu/MoreMenu.tsx index 452dfc8cbdb..5d56b1d4bf9 100644 --- a/Common/UI/Components/MoreMenu/MoreMenu.tsx +++ b/Common/UI/Components/MoreMenu/MoreMenu.tsx @@ -1,41 +1,54 @@ -import React, { FunctionComponent, ReactElement, useEffect, useState } from "react"; +import React, { + FunctionComponent, + ReactElement, + useEffect, + useState, +} from "react"; import Button, { ButtonStyleType } from "../Button/Button"; import IconProp from "../../../Types/Icon/IconProp"; import useComponentOutsideClick from "../../Types/UseComponentOutsideClick"; export interface ComponentProps { - children: Array | ReactElement; + children: Array | ReactElement; } const MoreMenu: FunctionComponent = ( - props: ComponentProps, + props: ComponentProps, ): ReactElement => { - - const { ref, isComponentVisible, setIsComponentVisible } = + const { ref, isComponentVisible, setIsComponentVisible } = useComponentOutsideClick(false); - - - const [isDropdownVisible, setDropdownVisible] = useState(false); - - useEffect(() => { - setDropdownVisible(isComponentVisible); - }, [isComponentVisible]); - - return ( -
-
-
- - {isComponentVisible &&
- {props.children} -
} + const [isDropdownVisible, setDropdownVisible] = useState(false); + + useEffect(() => { + setDropdownVisible(isComponentVisible); + }, [isComponentVisible]); + + return ( +
+
+
+ + {isComponentVisible && ( +
+ {props.children}
- - ); + )} +
+ ); }; export default MoreMenu; diff --git a/Common/UI/Components/MoreMenu/MoreMenuItem.tsx b/Common/UI/Components/MoreMenu/MoreMenuItem.tsx index 9babc158e17..6ea07caa6cf 100644 --- a/Common/UI/Components/MoreMenu/MoreMenuItem.tsx +++ b/Common/UI/Components/MoreMenu/MoreMenuItem.tsx @@ -3,22 +3,31 @@ import IconProp from "../../../Types/Icon/IconProp"; import Icon from "../Icon/Icon"; export interface ComponentProps { - icon?: IconProp | undefined; - text: string; - onClick: () => void; + icon?: IconProp | undefined; + text: string; + onClick: () => void; } const MoreMenuItem: FunctionComponent = ( - props: ComponentProps, + props: ComponentProps, ): ReactElement => { - return ( - { - props.onClick(); - }}> - {props.icon && } - {props.text} - - ); + return ( + { + props.onClick(); + }} + > + {props.icon && ( + + )} + {props.text} + + ); }; export default MoreMenuItem; From fd15e66f838915de20ba1d81d68ba06842cc4b25 Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Wed, 2 Oct 2024 19:26:40 +0100 Subject: [PATCH 4/6] add table view save settings. --- Common/Models/DatabaseModels/TableView.ts | 2 +- Common/UI/Components/ModelTable/TableView.tsx | 294 ++++++++++++++++++ Common/UI/Components/MoreMenu/Divider.tsx | 4 +- .../UI/Components/MoreMenu/MoreMenuItem.tsx | 6 +- .../Components/MoreMenu/MoreMenuSection.tsx | 22 ++ 5 files changed, 324 insertions(+), 4 deletions(-) create mode 100644 Common/UI/Components/ModelTable/TableView.tsx create mode 100644 Common/UI/Components/MoreMenu/MoreMenuSection.tsx diff --git a/Common/Models/DatabaseModels/TableView.ts b/Common/Models/DatabaseModels/TableView.ts index f60b29cb80b..4618d8468bd 100644 --- a/Common/Models/DatabaseModels/TableView.ts +++ b/Common/Models/DatabaseModels/TableView.ts @@ -380,7 +380,7 @@ export default class TableView extends BaseModel { unique: false, nullable: false, }) - public filters?: Query = undefined; + public query?: Query = undefined; @ColumnAccessControl({ create: [ diff --git a/Common/UI/Components/ModelTable/TableView.tsx b/Common/UI/Components/ModelTable/TableView.tsx new file mode 100644 index 00000000000..3f5137581d3 --- /dev/null +++ b/Common/UI/Components/ModelTable/TableView.tsx @@ -0,0 +1,294 @@ +import React, { + FunctionComponent, + ReactElement, + useEffect, + useState, +} from "react"; +import TableView from "../../../Models/DatabaseModels/TableView"; +import ObjectID from "../../../Types/ObjectID"; +import MoreMenu from "../MoreMenu/MoreMenu"; +import MoreMenuItem from "../MoreMenu/MoreMenuItem"; +import ListResult from "../../Utils/BaseDatabase/ListResult"; +import ModelAPI from "../../Utils/ModelAPI/ModelAPI"; +import { LIMIT_PER_PROJECT } from "../../../Types/Database/LimitMax"; +import SortOrder from "../../../Types/BaseDatabase/SortOrder"; +import API from "../../../Utils/API"; +import MoreMenuSection from "../MoreMenu/MoreMenuSection"; +import Button, { ButtonStyleType } from "../Button/Button"; +import IconProp from "../../../Types/Icon/IconProp"; +import { BarLoader } from "react-spinners"; +import ConfirmModal from "../Modal/ConfirmModal"; +import ModelFormModal from "../ModelFormModal/ModelFormModal"; +import { FormType } from "../Forms/ModelForm"; +import FormFieldSchemaType from "../Forms/Types/FormFieldSchemaType"; +import { PromiseVoidFunction } from "../../../Types/FunctionTypes"; +import { GetReactElementFunction } from "../../Types/FunctionTypes"; + +export interface ComponentProps { + tableId: string; + onViewChange: (tableView: TableView | null) => void; + currentTableView: TableView | null; + projectId: ObjectID; + userId: ObjectID; +} + +const TableViewElement: FunctionComponent = ( + props: ComponentProps, +): ReactElement => { + const [error, setError] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [allTableViews, setAllTableViews] = useState>([]); + const [tableViewToDelete, setTableViewToDelete] = useState< + TableView | undefined + >(undefined); + const [tableViewToEdit, setTableViewToEdit] = useState( + undefined, + ); + const [showCreateNewViewModal, setShowCreateNewViewModel] = + useState(false); + + // load all the filters for this user and for this project. + const fetchTableViews: PromiseVoidFunction = async (): Promise => { + try { + setError(""); + setIsLoading(true); + + const tableViews: ListResult = await ModelAPI.getList({ + modelType: TableView, + query: { + projectId: props.projectId, + createdByUserId: props.userId, + tableId: props.tableId, + }, + limit: LIMIT_PER_PROJECT, + skip: 0, + select: { + sort: true, + itemsOnPage: true, + query: true, + name: true, + }, + sort: { + name: SortOrder.Ascending, + }, + }); + + setAllTableViews(tableViews.data); + } catch (err) { + setError(API.getFriendlyErrorMessage(err as Error)); + } + + setIsLoading(false); + }; + + type DeleteTableViewFunction = (tableView: TableView) => Promise; + + const deleteTableView: DeleteTableViewFunction = async ( + tableView: TableView, + ): Promise => { + const tableViewId: ObjectID = tableView.id!; + + try { + setError(""); + setIsLoading(true); + + await ModelAPI.deleteItem({ + modelType: TableView, + id: tableViewId, + }); + + await fetchTableViews(); + } catch (err) { + setError(API.getFriendlyErrorMessage(err as Error)); + } + + setIsLoading(false); + }; + + useEffect(() => { + fetchTableViews().catch((err: Error) => { + setError(API.getFriendlyErrorMessage(err as Error)); + }); + }, []); + + type GetRightElementForTableViewMenuItemFunction = ( + item: TableView, + ) => React.JSX.Element; + + const getRightElementForTableViewMenuItem: GetRightElementForTableViewMenuItemFunction = + (item: TableView): ReactElement => { + return ( +
+
+ ); + }; + + type GetViewItemsFunction = () => Array; + + const getViewItems: GetViewItemsFunction = (): Array => { + return allTableViews.map((item: TableView, index: number) => { + return ( + { + props.onViewChange && props.onViewChange(item); + }} + /> + ); + }); + }; + + const getMenuContents: GetReactElementFunction = (): ReactElement => { + if (isLoading) { + return ; + } + + return ( + <> + {allTableViews.length > 0 ? ( + + {getViewItems()} + + ) : ( + <> + )} + {}}> + + ); + }; + + if (error) { + return ( + { + return setError(""); + }} + /> + ); + } + + if (tableViewToEdit) { + return ( + + modelType={TableView} + modelIdToEdit={tableViewToEdit.id!} + name="Edit View" + title="Edit View" + description="You can rename this table view to any name you like." + onClose={() => { + setTableViewToEdit(undefined); + }} + submitButtonText="Save Changes" + onSuccess={async () => { + setTableViewToEdit(undefined); + await fetchTableViews(); + }} + formProps={{ + name: "Edit View", + modelType: TableView, + id: "edit-table-view", + fields: [ + { + field: { + name: true, + }, + fieldType: FormFieldSchemaType.Text, + placeholder: "Name of the view", + description: "Please enter the new name of the view", + title: "Name", + required: true, + }, + ], + formType: FormType.Update, + }} + /> + ); + } + + if (tableViewToDelete) { + return ( + { + await deleteTableView(tableViewToDelete); + setTableViewToDelete(undefined); + }} + onClose={() => { + setTableViewToDelete(undefined); + }} + submitButtonText={`Delete`} + submitButtonType={ButtonStyleType.DANGER} + /> + ); + } + + if (showCreateNewViewModal) { + return ( + + modelType={TableView} + name="Save New View" + title="Save New View" + description="You can save the current table settings as a new view." + onClose={() => { + setShowCreateNewViewModel(false); + }} + submitButtonText="Save Changes" + onBeforeCreate={(tableView: TableView) => { + tableView.query = props.currentTableView?.query || {}; + tableView.itemsOnPage = props.currentTableView?.itemsOnPage || 10; + tableView.sort = props.currentTableView?.sort || {}; + return Promise.resolve(tableView); + }} + onSuccess={async () => { + setShowCreateNewViewModel(false); + await fetchTableViews(); + }} + formProps={{ + name: "Save New View", + modelType: TableView, + id: "save-table-view", + fields: [ + { + field: { + name: true, + }, + fieldType: FormFieldSchemaType.Text, + placeholder: "Name of the view", + description: "Please enter the new name of the view", + title: "Name", + required: true, + }, + ], + formType: FormType.Create, + }} + /> + ); + } + + return {getMenuContents()}; +}; + +export default TableViewElement; diff --git a/Common/UI/Components/MoreMenu/Divider.tsx b/Common/UI/Components/MoreMenu/Divider.tsx index fc884f5540b..853b08a97cb 100644 --- a/Common/UI/Components/MoreMenu/Divider.tsx +++ b/Common/UI/Components/MoreMenu/Divider.tsx @@ -1,7 +1,7 @@ import React, { FunctionComponent, ReactElement } from "react"; -const MoreMenuItem: FunctionComponent = (): ReactElement => { +const MoreMenuDivider: FunctionComponent = (): ReactElement => { return
; }; -export default MoreMenuItem; +export default MoreMenuDivider; diff --git a/Common/UI/Components/MoreMenu/MoreMenuItem.tsx b/Common/UI/Components/MoreMenu/MoreMenuItem.tsx index 6ea07caa6cf..fea7172a607 100644 --- a/Common/UI/Components/MoreMenu/MoreMenuItem.tsx +++ b/Common/UI/Components/MoreMenu/MoreMenuItem.tsx @@ -6,6 +6,7 @@ export interface ComponentProps { icon?: IconProp | undefined; text: string; onClick: () => void; + rightElement?: Array | ReactElement | undefined; } const MoreMenuItem: FunctionComponent = ( @@ -25,7 +26,10 @@ const MoreMenuItem: FunctionComponent = ( className="mr-3 h-5 w-5 text-gray-400 group-hover:text-gray-500" /> )} - {props.text} +
+
{props.text}
+
{props.rightElement}
+
); }; diff --git a/Common/UI/Components/MoreMenu/MoreMenuSection.tsx b/Common/UI/Components/MoreMenu/MoreMenuSection.tsx new file mode 100644 index 00000000000..4f0d4612a8a --- /dev/null +++ b/Common/UI/Components/MoreMenu/MoreMenuSection.tsx @@ -0,0 +1,22 @@ +import React, { FunctionComponent, ReactElement } from "react"; +import MoreMenuDivider from "./Divider"; +export interface ComponentProps { + title: string; + children: Array | ReactElement; +} + +const MoreMenuSection: FunctionComponent = ( + props: ComponentProps, +): ReactElement => { + return ( +
+
+ {props.title.toLocaleUpperCase()} +
+ {props.children} + +
+ ); +}; + +export default MoreMenuSection; From bc0e8e323ff60f8f625bb02e205902bc2b13498b Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Wed, 2 Oct 2024 19:49:22 +0100 Subject: [PATCH 5/6] integrate table view filters with base model table --- Common/Models/DatabaseModels/TableView.ts | 5 +- .../Components/ModelTable/BaseModelTable.tsx | 48 +++++++++++++++++-- Common/UI/Components/ModelTable/TableView.tsx | 32 +++++++++++-- 3 files changed, 73 insertions(+), 12 deletions(-) diff --git a/Common/Models/DatabaseModels/TableView.ts b/Common/Models/DatabaseModels/TableView.ts index 4618d8468bd..48c338e332c 100644 --- a/Common/Models/DatabaseModels/TableView.ts +++ b/Common/Models/DatabaseModels/TableView.ts @@ -21,6 +21,7 @@ import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm"; import Query from "../../Types/BaseDatabase/Query"; import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel"; import Sort from "../../Types/BaseDatabase/Sort"; +import AnalyticsBaseModel from "../AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel"; @TableBillingAccessControl({ create: PlanType.Growth, @@ -380,7 +381,7 @@ export default class TableView extends BaseModel { unique: false, nullable: false, }) - public query?: Query = undefined; + public query?: Query = undefined; @ColumnAccessControl({ create: [ @@ -413,7 +414,7 @@ export default class TableView extends BaseModel { unique: false, nullable: false, }) - public sort?: Sort = undefined; + public sort?: Sort = undefined; @ColumnAccessControl({ create: [ diff --git a/Common/UI/Components/ModelTable/BaseModelTable.tsx b/Common/UI/Components/ModelTable/BaseModelTable.tsx index 851313263db..e5e4a20bf74 100644 --- a/Common/UI/Components/ModelTable/BaseModelTable.tsx +++ b/Common/UI/Components/ModelTable/BaseModelTable.tsx @@ -84,8 +84,8 @@ import React, { useEffect, useState, } from "react"; -import MoreMenu from "../MoreMenu/MoreMenu"; -import MoreMenuItem from "../MoreMenu/MoreMenuItem"; +import TableViewElement from "./TableView"; +import TableView from "../../../Models/DatabaseModels/TableView"; export enum ShowAs { Table, @@ -800,10 +800,48 @@ const BaseModelTable: ( } if (props.saveFilterProps && props.saveFilterProps.tableId) { + const currentTableView: TableView = new TableView(); + currentTableView.query = (query || {}) as any; + currentTableView.itemsOnPage = itemsOnPage || 10; + + if (sortBy && sortOrder) { + currentTableView.sort = { + [sortBy as string]: sortOrder, + }; + } else { + currentTableView.sort = {}; + } + return ( - - {}}> - + { + if (tableView) { + const sortBy: string | undefined = Object.keys( + tableView.sort || {}, + )[0]; + let sortOrder: SortOrder = SortOrder.Descending; + + if (sortBy && tableView.sort) { + sortOrder = + ((tableView.sort as any)[sortBy as any] as any) || + SortOrder.Descending; + } + + // then set query, sort and items on the page + setQuery(tableView.query || {}); + setItemsOnPage(tableView.itemsOnPage || 10); + setSortBy(sortBy as keyof TBaseModel); + setSortOrder(sortOrder); + } else { + setQuery({}); + setSortBy(null); + setSortOrder(SortOrder.Descending); + setItemsOnPage(10); + } + }} + /> ); } diff --git a/Common/UI/Components/ModelTable/TableView.tsx b/Common/UI/Components/ModelTable/TableView.tsx index 3f5137581d3..7d5eebf1394 100644 --- a/Common/UI/Components/ModelTable/TableView.tsx +++ b/Common/UI/Components/ModelTable/TableView.tsx @@ -23,13 +23,13 @@ import { FormType } from "../Forms/ModelForm"; import FormFieldSchemaType from "../Forms/Types/FormFieldSchemaType"; import { PromiseVoidFunction } from "../../../Types/FunctionTypes"; import { GetReactElementFunction } from "../../Types/FunctionTypes"; +import ProjectUtil from "../../Utils/Project"; +import User from "../../Utils/User"; export interface ComponentProps { tableId: string; onViewChange: (tableView: TableView | null) => void; currentTableView: TableView | null; - projectId: ObjectID; - userId: ObjectID; } const TableViewElement: FunctionComponent = ( @@ -47,6 +47,9 @@ const TableViewElement: FunctionComponent = ( const [showCreateNewViewModal, setShowCreateNewViewModel] = useState(false); + const [currentlySelectedView, setCurrentlySelectedView] = + useState(null); + // load all the filters for this user and for this project. const fetchTableViews: PromiseVoidFunction = async (): Promise => { try { @@ -56,8 +59,8 @@ const TableViewElement: FunctionComponent = ( const tableViews: ListResult = await ModelAPI.getList({ modelType: TableView, query: { - projectId: props.projectId, - createdByUserId: props.userId, + projectId: ProjectUtil.getCurrentProjectId()!, + createdByUserId: User.getUserId(), tableId: props.tableId, }, limit: LIMIT_PER_PROJECT, @@ -149,6 +152,7 @@ const TableViewElement: FunctionComponent = ( text={item.name || ""} onClick={() => { props.onViewChange && props.onViewChange(item); + setCurrentlySelectedView(item); }} /> ); @@ -169,7 +173,25 @@ const TableViewElement: FunctionComponent = ( ) : ( <> )} - {}}> + + {currentlySelectedView ? ( + { + setCurrentlySelectedView(null); + props.onViewChange && props.onViewChange(null); + }} + > + ) : ( + <> + )} + + { + setShowCreateNewViewModel(true); + }} + > ); }; From c789438c627f723a0564ddd6092a551d1f3db2c9 Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Wed, 2 Oct 2024 19:52:32 +0100 Subject: [PATCH 6/6] add migration script for table views --- ...nName.ts => 1727894983857-MigrationName.ts} | 18 +++++++++++++++--- .../Postgres/SchemaMigrations/Index.ts | 2 ++ 2 files changed, 17 insertions(+), 3 deletions(-) rename Common/Server/Infrastructure/Postgres/SchemaMigrations/{1727877987596-MigrationName.ts => 1727894983857-MigrationName.ts} (60%) diff --git a/Common/Server/Infrastructure/Postgres/SchemaMigrations/1727877987596-MigrationName.ts b/Common/Server/Infrastructure/Postgres/SchemaMigrations/1727894983857-MigrationName.ts similarity index 60% rename from Common/Server/Infrastructure/Postgres/SchemaMigrations/1727877987596-MigrationName.ts rename to Common/Server/Infrastructure/Postgres/SchemaMigrations/1727894983857-MigrationName.ts index f062ce79a89..62e14fa33d1 100644 --- a/Common/Server/Infrastructure/Postgres/SchemaMigrations/1727877987596-MigrationName.ts +++ b/Common/Server/Infrastructure/Postgres/SchemaMigrations/1727894983857-MigrationName.ts @@ -1,15 +1,21 @@ import { MigrationInterface, QueryRunner } from "typeorm"; -export class MigrationName1727877987596 implements MigrationInterface { - public name = "MigrationName1727877987596"; +export class MigrationName1727894983857 implements MigrationInterface { + public name = "MigrationName1727894983857"; public async up(queryRunner: QueryRunner): Promise { await queryRunner.query( - `CREATE TABLE "TableView" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "projectId" uuid NOT NULL, "name" character varying(100) NOT NULL, "tableId" character varying(100) NOT NULL, "description" character varying(500), "createdByUserId" uuid, "deletedByUserId" uuid, "filters" jsonb NOT NULL, "sort" jsonb NOT NULL, "itemsOnPage" integer NOT NULL DEFAULT '10', CONSTRAINT "PK_1e17a8834a65403cc87c4ead0cc" PRIMARY KEY ("_id"))`, + `CREATE TABLE "TableView" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "projectId" uuid NOT NULL, "name" character varying(100) NOT NULL, "tableId" character varying(100) NOT NULL, "description" character varying(500), "createdByUserId" uuid, "deletedByUserId" uuid, "query" jsonb NOT NULL, "sort" jsonb NOT NULL, "itemsOnPage" integer NOT NULL DEFAULT '10', CONSTRAINT "PK_1e17a8834a65403cc87c4ead0cc" PRIMARY KEY ("_id"))`, ); await queryRunner.query( `CREATE INDEX "IDX_a8691d4e023d907b5c73a43e42" ON "TableView" ("projectId") `, ); + await queryRunner.query( + `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`, + ); + await queryRunner.query( + `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`, + ); await queryRunner.query( `ALTER TABLE "TableView" ADD CONSTRAINT "FK_a8691d4e023d907b5c73a43e424" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`, ); @@ -31,6 +37,12 @@ export class MigrationName1727877987596 implements MigrationInterface { await queryRunner.query( `ALTER TABLE "TableView" DROP CONSTRAINT "FK_a8691d4e023d907b5c73a43e424"`, ); + await queryRunner.query( + `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`, + ); + await queryRunner.query( + `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`, + ); await queryRunner.query( `DROP INDEX "public"."IDX_a8691d4e023d907b5c73a43e42"`, ); diff --git a/Common/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts b/Common/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts index cba03af5249..43ca20569b7 100644 --- a/Common/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +++ b/Common/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts @@ -71,6 +71,7 @@ import { MigrationName1727193130193 } from "./1727193130193-MigrationName"; import { MigrationName1727193702212 } from "./1727193702212-MigrationName"; import { MigrationName1727194211048 } from "./1727194211048-MigrationName"; import { MigrationName1727194579925 } from "./1727194579925-MigrationName"; +import { MigrationName1727894983857 } from "./1727894983857-MigrationName"; export default [ InitialMigration, @@ -146,4 +147,5 @@ export default [ MigrationName1727193702212, MigrationName1727194211048, MigrationName1727194579925, + MigrationName1727894983857, ];