mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 08:42:13 +02:00
Compare commits
34 Commits
dropdown-l
...
8.0.5572
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f99b9680f | ||
|
|
b08c39037d | ||
|
|
f7cc3c00da | ||
|
|
ac4286935a | ||
|
|
90a0b2e4a8 | ||
|
|
9b22c48d27 | ||
|
|
9c9dad5da0 | ||
|
|
e986f74025 | ||
|
|
bb85c9f8c8 | ||
|
|
25ab1cdbf9 | ||
|
|
44b8a9ddc9 | ||
|
|
c388ff9550 | ||
|
|
321d1680e6 | ||
|
|
6c0e9f0fed | ||
|
|
99349ecb30 | ||
|
|
258bbbd9cf | ||
|
|
1094a07fc6 | ||
|
|
14a5671645 | ||
|
|
5a41c66953 | ||
|
|
af605fce4c | ||
|
|
f8ef6c69fe | ||
|
|
e1848f44f7 | ||
|
|
825bd39dda | ||
|
|
b99905dfe8 | ||
|
|
a4bf40a2c1 | ||
|
|
711998b048 | ||
|
|
132e044c07 | ||
|
|
8ecc307451 | ||
|
|
c85c29989f | ||
|
|
95726e0f21 | ||
|
|
adc15992e9 | ||
|
|
58d83a2a80 | ||
|
|
5461cd4502 | ||
|
|
478465a65b |
1
.github/workflows/test.telemetry.yaml
vendored
1
.github/workflows/test.telemetry.yaml
vendored
@@ -17,5 +17,6 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
- run: cd Common && npm install
|
||||
- run: cd Telemetry && npm install && npm run test
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
} from "react-router-dom";
|
||||
import UserView from "./Pages/Users/View/Index";
|
||||
import UserDelete from "./Pages/Users/View/Delete";
|
||||
import UserSettings from "./Pages/Users/View/Settings";
|
||||
import ProjectView from "./Pages/Projects/View/Index";
|
||||
import ProjectDelete from "./Pages/Projects/View/Delete";
|
||||
|
||||
@@ -71,6 +72,11 @@ const App: () => JSX.Element = () => {
|
||||
element={<UserView />}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteMap[PageMap.USER_SETTINGS]?.toString() || ""}
|
||||
element={<UserSettings />}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteMap[PageMap.USER_DELETE]?.toString() || ""}
|
||||
element={<UserDelete />}
|
||||
|
||||
94
AdminDashboard/src/Pages/Users/View/Settings.tsx
Normal file
94
AdminDashboard/src/Pages/Users/View/Settings.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import PageMap from "../../../Utils/PageMap";
|
||||
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
|
||||
import Route from "Common/Types/API/Route";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
import SideMenuComponent from "./SideMenu";
|
||||
import User from "Common/Models/DatabaseModels/User";
|
||||
import ModelPage from "Common/UI/Components/Page/ModelPage";
|
||||
import CardModelDetail from "Common/UI/Components/ModelDetail/CardModelDetail";
|
||||
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
|
||||
import FieldType from "Common/UI/Components/Types/FieldType";
|
||||
|
||||
const UserSettings: FunctionComponent = (): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
|
||||
|
||||
return (
|
||||
<ModelPage<User>
|
||||
modelId={modelId}
|
||||
modelNameField="email"
|
||||
modelType={User}
|
||||
title={"User"}
|
||||
breadcrumbLinks={[
|
||||
{
|
||||
title: "Admin Dashboard",
|
||||
to: RouteUtil.populateRouteParams(RouteMap[PageMap.HOME] as Route),
|
||||
},
|
||||
{
|
||||
title: "Users",
|
||||
to: RouteUtil.populateRouteParams(RouteMap[PageMap.USERS] as Route),
|
||||
},
|
||||
{
|
||||
title: "User",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.USER_VIEW] as Route,
|
||||
{
|
||||
modelId: modelId,
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Settings",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.USER_SETTINGS] as Route,
|
||||
{
|
||||
modelId: modelId,
|
||||
},
|
||||
),
|
||||
},
|
||||
]}
|
||||
sideMenu={<SideMenuComponent modelId={modelId} />}
|
||||
>
|
||||
<CardModelDetail<User>
|
||||
name="user-master-admin-settings"
|
||||
cardProps={{
|
||||
title: "Master Admin Access",
|
||||
description:
|
||||
"Grant or revoke master admin access for this user. Master admins can manage every project and workspace.",
|
||||
}}
|
||||
isEditable={true}
|
||||
editButtonText="Update Access"
|
||||
formFields={[
|
||||
{
|
||||
field: {
|
||||
isMasterAdmin: true,
|
||||
},
|
||||
title: "Master Admin",
|
||||
description:
|
||||
"Enable to give this user full access to the entire platform.",
|
||||
fieldType: FormFieldSchemaType.Toggle,
|
||||
required: true,
|
||||
},
|
||||
]}
|
||||
modelDetailProps={{
|
||||
modelType: User,
|
||||
id: "user-master-admin-settings-detail",
|
||||
fields: [
|
||||
{
|
||||
field: {
|
||||
isMasterAdmin: true,
|
||||
},
|
||||
title: "Master Admin",
|
||||
fieldType: FieldType.Boolean,
|
||||
placeholder: "No",
|
||||
},
|
||||
],
|
||||
modelId: modelId,
|
||||
}}
|
||||
/>
|
||||
</ModelPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserSettings;
|
||||
@@ -30,6 +30,18 @@ const SideMenuComponent: FunctionComponent<SideMenuProps> = (
|
||||
}}
|
||||
icon={IconProp.Info}
|
||||
/>
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: "Settings",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.USER_SETTINGS] as Route,
|
||||
{
|
||||
modelId: props.modelId,
|
||||
},
|
||||
),
|
||||
}}
|
||||
icon={IconProp.Settings}
|
||||
/>
|
||||
</SideMenuSection>
|
||||
|
||||
<SideMenuSection title="Advanced">
|
||||
|
||||
@@ -6,6 +6,7 @@ enum PageMap {
|
||||
|
||||
USERS = "USERS",
|
||||
USER_VIEW = "USER_VIEW",
|
||||
USER_SETTINGS = "USER_SETTINGS",
|
||||
USER_DELETE = "USER_DELETE",
|
||||
|
||||
PROJECTS = "PROJECTS",
|
||||
|
||||
@@ -18,6 +18,9 @@ const RouteMap: Dictionary<Route> = {
|
||||
|
||||
[PageMap.USERS]: new Route(`/admin/users`),
|
||||
[PageMap.USER_VIEW]: new Route(`/admin/users/${RouteParams.ModelID}`),
|
||||
[PageMap.USER_SETTINGS]: new Route(
|
||||
`/admin/users/${RouteParams.ModelID}/settings`,
|
||||
),
|
||||
[PageMap.USER_DELETE]: new Route(
|
||||
`/admin/users/${RouteParams.ModelID}/delete`,
|
||||
),
|
||||
|
||||
@@ -10,6 +10,7 @@ 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 ColorField from "../../Types/Database/ColorField";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
import TableMetadata from "../../Types/Database/TableMetadata";
|
||||
@@ -418,6 +419,7 @@ export default class AlertFeed extends BaseModel {
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@ColorField()
|
||||
@TableColumn({
|
||||
type: TableColumnType.Color,
|
||||
required: true,
|
||||
|
||||
@@ -12,6 +12,7 @@ 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 ColorField from "../../Types/Database/ColorField";
|
||||
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
@@ -378,6 +379,7 @@ export default class AlertSeverity extends BaseModel {
|
||||
Permission.EditAlertSeverity,
|
||||
],
|
||||
})
|
||||
@ColorField()
|
||||
@TableColumn({
|
||||
title: "Color",
|
||||
required: true,
|
||||
|
||||
@@ -12,6 +12,7 @@ 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 ColorField from "../../Types/Database/ColorField";
|
||||
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
@@ -356,6 +357,7 @@ export default class AlertState extends BaseModel {
|
||||
Permission.EditAlertState,
|
||||
],
|
||||
})
|
||||
@ColorField()
|
||||
@TableColumn({
|
||||
title: "Color",
|
||||
required: true,
|
||||
|
||||
@@ -15,6 +15,7 @@ import TableColumn, {
|
||||
getTableColumns,
|
||||
} from "../../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../../Types/Database/TableColumnType";
|
||||
import { getFirstColorFieldColumn } from "../../../Types/Database/ColorField";
|
||||
import Dictionary from "../../../Types/Dictionary";
|
||||
import Email from "../../../Types/Email";
|
||||
import BadDataException from "../../../Types/Exception/BadDataException";
|
||||
@@ -203,6 +204,10 @@ export default class DatabaseBaseModel extends BaseEntity {
|
||||
return new Columns(Object.keys(getTableColumns(this)));
|
||||
}
|
||||
|
||||
public getFirstColorColumn(): string | null {
|
||||
return getFirstColorFieldColumn(this);
|
||||
}
|
||||
|
||||
public canQueryMultiTenant(): boolean {
|
||||
return Boolean(this.isMultiTenantRequestAllowed);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ 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 ColorField from "../../Types/Database/ColorField";
|
||||
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
@@ -378,6 +379,7 @@ export default class IncidentSeverity extends BaseModel {
|
||||
Permission.EditIncidentSeverity,
|
||||
],
|
||||
})
|
||||
@ColorField()
|
||||
@TableColumn({
|
||||
title: "Color",
|
||||
required: true,
|
||||
|
||||
@@ -12,6 +12,7 @@ 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 ColorField from "../../Types/Database/ColorField";
|
||||
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
@@ -380,6 +381,7 @@ export default class IncidentState extends BaseModel {
|
||||
Permission.EditIncidentState,
|
||||
],
|
||||
})
|
||||
@ColorField()
|
||||
@TableColumn({
|
||||
title: "Color",
|
||||
required: true,
|
||||
|
||||
@@ -118,6 +118,7 @@ import StatusPageHistoryChartBarColorRule from "./StatusPageHistoryChartBarColor
|
||||
import StatusPageOwnerTeam from "./StatusPageOwnerTeam";
|
||||
import StatusPageOwnerUser from "./StatusPageOwnerUser";
|
||||
import StatusPagePrivateUser from "./StatusPagePrivateUser";
|
||||
import StatusPagePrivateUserSession from "./StatusPagePrivateUserSession";
|
||||
import StatusPageResource from "./StatusPageResource";
|
||||
import StatusPageSCIM from "./StatusPageSCIM";
|
||||
import StatusPageSSO from "./StatusPageSso";
|
||||
@@ -130,6 +131,7 @@ import TeamComplianceSetting from "./TeamComplianceSetting";
|
||||
import TelemetryService from "./TelemetryService";
|
||||
import UsageBilling from "./TelemetryUsageBilling";
|
||||
import User from "./User";
|
||||
import UserSession from "./UserSession";
|
||||
import UserCall from "./UserCall";
|
||||
// Notification Methods
|
||||
import UserEmail from "./UserEmail";
|
||||
@@ -266,6 +268,7 @@ const AllModelTypes: Array<{
|
||||
StatusPageFooterLink,
|
||||
StatusPageHeaderLink,
|
||||
StatusPagePrivateUser,
|
||||
StatusPagePrivateUserSession,
|
||||
StatusPageHistoryChartBarColorRule,
|
||||
|
||||
ScheduledMaintenanceState,
|
||||
@@ -375,6 +378,7 @@ const AllModelTypes: Array<{
|
||||
ProbeOwnerTeam,
|
||||
ProbeOwnerUser,
|
||||
|
||||
UserSession,
|
||||
UserTotpAuth,
|
||||
UserWebAuthn,
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ 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 ColorField from "../../Types/Database/ColorField";
|
||||
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
@@ -365,6 +366,7 @@ export default class Label extends AccessControlModel {
|
||||
Permission.EditProjectLabel,
|
||||
],
|
||||
})
|
||||
@ColorField()
|
||||
@TableColumn({
|
||||
title: "Color",
|
||||
required: true,
|
||||
|
||||
@@ -12,6 +12,7 @@ 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 ColorField from "../../Types/Database/ColorField";
|
||||
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
@@ -379,6 +380,7 @@ export default class MonitorStatus extends BaseModel {
|
||||
Permission.EditProjectMonitorStatus,
|
||||
],
|
||||
})
|
||||
@ColorField()
|
||||
@TableColumn({
|
||||
title: "Color",
|
||||
required: true,
|
||||
|
||||
@@ -10,6 +10,7 @@ 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 ColorField from "../../Types/Database/ColorField";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
import TableMetadata from "../../Types/Database/TableMetadata";
|
||||
@@ -422,6 +423,7 @@ export default class ScheduledMaintenanceFeed extends BaseModel {
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@ColorField()
|
||||
@TableColumn({
|
||||
type: TableColumnType.Color,
|
||||
required: true,
|
||||
|
||||
@@ -12,6 +12,7 @@ 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 ColorField from "../../Types/Database/ColorField";
|
||||
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
@@ -380,6 +381,7 @@ export default class ScheduledMaintenanceState extends BaseModel {
|
||||
Permission.EditScheduledMaintenanceState,
|
||||
],
|
||||
})
|
||||
@ColorField()
|
||||
@TableColumn({
|
||||
title: "Color",
|
||||
required: true,
|
||||
|
||||
@@ -14,6 +14,7 @@ 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 ColorField from "../../Types/Database/ColorField";
|
||||
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
@@ -448,6 +449,7 @@ export default class ServiceCatalog extends BaseModel {
|
||||
Permission.EditServiceCatalog,
|
||||
],
|
||||
})
|
||||
@ColorField()
|
||||
@TableColumn({
|
||||
type: TableColumnType.Color,
|
||||
title: "Service Color",
|
||||
|
||||
413
Common/Models/DatabaseModels/StatusPagePrivateUserSession.ts
Normal file
413
Common/Models/DatabaseModels/StatusPagePrivateUserSession.ts
Normal file
@@ -0,0 +1,413 @@
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
import Project from "./Project";
|
||||
import StatusPage from "./StatusPage";
|
||||
import StatusPagePrivateUser from "./StatusPagePrivateUser";
|
||||
import Route from "../../Types/API/Route";
|
||||
import { PlanType } from "../../Types/Billing/SubscriptionPlan";
|
||||
import AllowAccessIfSubscriptionIsUnpaid from "../../Types/Database/AccessControl/AllowAccessIfSubscriptionIsUnpaid";
|
||||
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
|
||||
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
|
||||
import TableBillingAccessControl from "../../Types/Database/AccessControl/TableBillingAccessControl";
|
||||
import CanAccessIfCanReadOn from "../../Types/Database/CanAccessIfCanReadOn";
|
||||
import ColumnLength from "../../Types/Database/ColumnLength";
|
||||
import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
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 HashedString from "../../Types/HashedString";
|
||||
import IconProp from "../../Types/Icon/IconProp";
|
||||
import { JSONObject } from "../../Types/JSON";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
|
||||
|
||||
@AllowAccessIfSubscriptionIsUnpaid()
|
||||
@TableBillingAccessControl({
|
||||
create: PlanType.Growth,
|
||||
read: PlanType.Growth,
|
||||
update: PlanType.Growth,
|
||||
delete: PlanType.Growth,
|
||||
})
|
||||
@CanAccessIfCanReadOn("statusPage")
|
||||
@TenantColumn("projectId")
|
||||
@TableAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
delete: [],
|
||||
update: [],
|
||||
})
|
||||
@CrudApiEndpoint(new Route("/status-page-private-user-session"))
|
||||
@Entity({
|
||||
name: "StatusPagePrivateUserSession",
|
||||
})
|
||||
@TableMetadata({
|
||||
tableName: "StatusPagePrivateUserSession",
|
||||
singularName: "Status Page Private User Session",
|
||||
pluralName: "Status Page Private User Sessions",
|
||||
icon: IconProp.Lock,
|
||||
tableDescription:
|
||||
"Stores status page private user sessions, refresh tokens, and device metadata for secure access control.",
|
||||
})
|
||||
export default class StatusPagePrivateUserSession extends BaseModel {
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "projectId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: Project,
|
||||
title: "Project",
|
||||
description: "Project that owns this private status page session.",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return Project;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "projectId" })
|
||||
public project?: Project = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "Project ID",
|
||||
description: "Project identifier for this session.",
|
||||
required: true,
|
||||
canReadOnRelationQuery: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: false,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public projectId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "statusPageId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: StatusPage,
|
||||
title: "Status Page",
|
||||
description: "Status page associated with this session.",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return StatusPage;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "statusPageId" })
|
||||
public statusPage?: StatusPage = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "Status Page ID",
|
||||
description: "Identifier for the status page.",
|
||||
required: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: false,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public statusPageId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "statusPagePrivateUserId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: StatusPagePrivateUser,
|
||||
title: "Status Page Private User",
|
||||
description: "Private user record associated with this session.",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return StatusPagePrivateUser;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: false,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "delete",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "statusPagePrivateUserId" })
|
||||
public statusPagePrivateUser?: StatusPagePrivateUser = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "Status Page Private User ID",
|
||||
description: "Identifier for the status page private user.",
|
||||
required: true,
|
||||
canReadOnRelationQuery: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: false,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public statusPagePrivateUserId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@Index({ unique: true })
|
||||
@TableColumn({
|
||||
type: TableColumnType.HashedString,
|
||||
title: "Refresh Token",
|
||||
description: "Hashed refresh token for the private user session.",
|
||||
required: true,
|
||||
hideColumnInDocumentation: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.HashedString,
|
||||
length: ColumnLength.HashedString,
|
||||
nullable: false,
|
||||
unique: true,
|
||||
transformer: HashedString.getDatabaseTransformer(),
|
||||
})
|
||||
public refreshToken?: HashedString = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Date,
|
||||
title: "Refresh Token Expires At",
|
||||
description: "Expiration timestamp for the refresh token.",
|
||||
required: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Date,
|
||||
nullable: false,
|
||||
})
|
||||
public refreshTokenExpiresAt?: Date = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Date,
|
||||
title: "Last Active At",
|
||||
description: "Last time this session was active.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Date,
|
||||
nullable: true,
|
||||
})
|
||||
public lastActiveAt?: Date = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Device Name",
|
||||
description: "Friendly name for the device used to access the status page.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public deviceName?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Device Type",
|
||||
description: "Type of device (desktop, mobile, etc).",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public deviceType?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Device OS",
|
||||
description: "Operating system reported for this session.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public deviceOS?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Browser",
|
||||
description: "Browser or client application used for the session.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public deviceBrowser?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "IP Address",
|
||||
description: "IP address recorded for this session.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public ipAddress?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.VeryLongText,
|
||||
title: "User Agent",
|
||||
description: "User agent string supplied by the client.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.VeryLongText,
|
||||
nullable: true,
|
||||
})
|
||||
public userAgent?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Boolean,
|
||||
title: "Is Revoked",
|
||||
description: "Indicates if the session has been revoked.",
|
||||
isDefaultValueColumn: true,
|
||||
defaultValue: false,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Boolean,
|
||||
nullable: false,
|
||||
default: false,
|
||||
})
|
||||
public isRevoked?: boolean = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Date,
|
||||
title: "Revoked At",
|
||||
description: "Timestamp when the session was revoked, if applicable.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Date,
|
||||
nullable: true,
|
||||
})
|
||||
public revokedAt?: Date = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Revoked Reason",
|
||||
description: "Reason provided for revoking this session.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public revokedReason?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.JSON,
|
||||
title: "Additional Info",
|
||||
description: "Flexible JSON payload for storing structured metadata.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.JSON,
|
||||
nullable: true,
|
||||
})
|
||||
public additionalInfo?: JSONObject = undefined;
|
||||
}
|
||||
@@ -12,6 +12,7 @@ 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 ColorField from "../../Types/Database/ColorField";
|
||||
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
@@ -505,6 +506,7 @@ export default class TelemetryService extends BaseModel {
|
||||
Permission.EditTelemetryService,
|
||||
],
|
||||
})
|
||||
@ColorField()
|
||||
@TableColumn({
|
||||
type: TableColumnType.Color,
|
||||
title: "Service Color",
|
||||
|
||||
318
Common/Models/DatabaseModels/UserSession.ts
Normal file
318
Common/Models/DatabaseModels/UserSession.ts
Normal file
@@ -0,0 +1,318 @@
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
import User from "./User";
|
||||
import Route from "../../Types/API/Route";
|
||||
import AllowAccessIfSubscriptionIsUnpaid from "../../Types/Database/AccessControl/AllowAccessIfSubscriptionIsUnpaid";
|
||||
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
|
||||
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
|
||||
import ColumnLength from "../../Types/Database/ColumnLength";
|
||||
import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
import CurrentUserCanAccessRecordBy from "../../Types/Database/CurrentUserCanAccessRecordBy";
|
||||
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
import TableMetadata from "../../Types/Database/TableMetadata";
|
||||
import HashedString from "../../Types/HashedString";
|
||||
import IconProp from "../../Types/Icon/IconProp";
|
||||
import { JSONObject } from "../../Types/JSON";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import Permission from "../../Types/Permission";
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
|
||||
|
||||
@EnableDocumentation({
|
||||
isMasterAdminApiDocs: true,
|
||||
})
|
||||
@AllowAccessIfSubscriptionIsUnpaid()
|
||||
@TableAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
delete: [Permission.CurrentUser],
|
||||
update: [Permission.CurrentUser],
|
||||
})
|
||||
@CrudApiEndpoint(new Route("/user-session"))
|
||||
@Entity({
|
||||
name: "UserSession",
|
||||
})
|
||||
@TableMetadata({
|
||||
tableName: "UserSession",
|
||||
singularName: "User Session",
|
||||
pluralName: "User Sessions",
|
||||
icon: IconProp.Lock,
|
||||
tableDescription:
|
||||
"Active user sessions with refresh tokens and device metadata for enhanced authentication security.",
|
||||
})
|
||||
@CurrentUserCanAccessRecordBy("userId")
|
||||
class UserSession extends BaseModel {
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "userId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: User,
|
||||
title: "User",
|
||||
description: "User account this session belongs to.",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return User;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: false,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "delete",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "userId" })
|
||||
public user?: User = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.CurrentUser],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: true,
|
||||
title: "User ID",
|
||||
description: "Identifier for the user that owns this session.",
|
||||
canReadOnRelationQuery: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: false,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public userId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@Index({ unique: true })
|
||||
@TableColumn({
|
||||
type: TableColumnType.HashedString,
|
||||
title: "Refresh Token",
|
||||
description: "Hashed refresh token for this session.",
|
||||
required: true,
|
||||
hideColumnInDocumentation: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.HashedString,
|
||||
length: ColumnLength.HashedString,
|
||||
nullable: false,
|
||||
unique: true,
|
||||
transformer: HashedString.getDatabaseTransformer(),
|
||||
})
|
||||
public refreshToken?: HashedString = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Date,
|
||||
title: "Refresh Token Expires At",
|
||||
description: "Expiration timestamp for the refresh token.",
|
||||
required: true,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Date,
|
||||
nullable: false,
|
||||
})
|
||||
public refreshTokenExpiresAt?: Date = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Date,
|
||||
title: "Last Active At",
|
||||
description: "Last time this session was used.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Date,
|
||||
nullable: true,
|
||||
})
|
||||
public lastActiveAt?: Date = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Device Name",
|
||||
description: "Friendly name for the device used to sign in.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public deviceName?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Device Type",
|
||||
description: "Type of device (e.g., desktop, mobile).",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public deviceType?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Device OS",
|
||||
description: "Operating system reported for this session.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public deviceOS?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Browser",
|
||||
description: "Browser or client application used for this session.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public deviceBrowser?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "IP Address",
|
||||
description: "IP address observed for this session.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public ipAddress?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.VeryLongText,
|
||||
title: "User Agent",
|
||||
description: "Complete user agent string supplied by the client.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.VeryLongText,
|
||||
nullable: true,
|
||||
})
|
||||
public userAgent?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Boolean,
|
||||
title: "Is Revoked",
|
||||
description: "Marks whether the session has been explicitly revoked.",
|
||||
isDefaultValueColumn: true,
|
||||
defaultValue: false,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Boolean,
|
||||
nullable: false,
|
||||
default: false,
|
||||
})
|
||||
public isRevoked?: boolean = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Date,
|
||||
title: "Revoked At",
|
||||
description: "Timestamp when the session was revoked, if applicable.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Date,
|
||||
nullable: true,
|
||||
})
|
||||
public revokedAt?: Date = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Revoked Reason",
|
||||
description: "Optional reason describing why the session was revoked.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public revokedReason?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.CurrentUser],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.JSON,
|
||||
title: "Additional Info",
|
||||
description:
|
||||
"Flexible JSON payload for storing structured session metadata.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.JSON,
|
||||
nullable: true,
|
||||
})
|
||||
public additionalInfo?: JSONObject = undefined;
|
||||
}
|
||||
|
||||
export default UserSession;
|
||||
@@ -0,0 +1,91 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1762890441920 implements MigrationInterface {
|
||||
public name = "MigrationName1762890441920";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "StatusPagePrivateUserSession" ("_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, "statusPageId" uuid NOT NULL, "statusPagePrivateUserId" uuid NOT NULL, "refreshToken" character varying(64) NOT NULL, "refreshTokenExpiresAt" TIMESTAMP WITH TIME ZONE NOT NULL, "lastActiveAt" TIMESTAMP WITH TIME ZONE, "deviceName" character varying(100), "deviceType" character varying(100), "deviceOS" character varying(100), "deviceBrowser" character varying(100), "ipAddress" character varying(100), "userAgent" text, "isRevoked" boolean NOT NULL DEFAULT false, "revokedAt" TIMESTAMP WITH TIME ZONE, "revokedReason" character varying(100), "additionalInfo" jsonb, CONSTRAINT "UQ_12ce827a16d121bf6719260b8a9" UNIQUE ("refreshToken"), CONSTRAINT "PK_cbace84fe4c9712b94e571dc133" PRIMARY KEY ("_id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_ac5f4c13d6bc9696cbfb8e5a79" ON "StatusPagePrivateUserSession" ("projectId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_7b8d9b6e068c045d56b47a484b" ON "StatusPagePrivateUserSession" ("statusPageId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_365d602943505272f8f651ff4e" ON "StatusPagePrivateUserSession" ("statusPagePrivateUserId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE UNIQUE INDEX "IDX_12ce827a16d121bf6719260b8a" ON "StatusPagePrivateUserSession" ("refreshToken") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "UserSession" ("_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, "userId" uuid NOT NULL, "refreshToken" character varying(64) NOT NULL, "refreshTokenExpiresAt" TIMESTAMP WITH TIME ZONE NOT NULL, "lastActiveAt" TIMESTAMP WITH TIME ZONE, "deviceName" character varying(100), "deviceType" character varying(100), "deviceOS" character varying(100), "deviceBrowser" character varying(100), "ipAddress" character varying(100), "userAgent" text, "isRevoked" boolean NOT NULL DEFAULT false, "revokedAt" TIMESTAMP WITH TIME ZONE, "revokedReason" character varying(100), "additionalInfo" jsonb, CONSTRAINT "UQ_d66bd8342b0005c7192bdb17efc" UNIQUE ("refreshToken"), CONSTRAINT "PK_9dcd180f25755bab5fcebcbeb14" PRIMARY KEY ("_id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_7353eaf92987aeaf38c2590e94" ON "UserSession" ("userId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE UNIQUE INDEX "IDX_d66bd8342b0005c7192bdb17ef" ON "UserSession" ("refreshToken") `,
|
||||
);
|
||||
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 "StatusPagePrivateUserSession" ADD CONSTRAINT "FK_ac5f4c13d6bc9696cbfb8e5a794" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPagePrivateUserSession" ADD CONSTRAINT "FK_7b8d9b6e068c045d56b47a484be" FOREIGN KEY ("statusPageId") REFERENCES "StatusPage"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPagePrivateUserSession" ADD CONSTRAINT "FK_365d602943505272f8f651ff4e8" FOREIGN KEY ("statusPagePrivateUserId") REFERENCES "StatusPagePrivateUser"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserSession" ADD CONSTRAINT "FK_7353eaf92987aeaf38c2590e943" FOREIGN KEY ("userId") REFERENCES "User"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "UserSession" DROP CONSTRAINT "FK_7353eaf92987aeaf38c2590e943"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPagePrivateUserSession" DROP CONSTRAINT "FK_365d602943505272f8f651ff4e8"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPagePrivateUserSession" DROP CONSTRAINT "FK_7b8d9b6e068c045d56b47a484be"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPagePrivateUserSession" DROP CONSTRAINT "FK_ac5f4c13d6bc9696cbfb8e5a794"`,
|
||||
);
|
||||
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_d66bd8342b0005c7192bdb17ef"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_7353eaf92987aeaf38c2590e94"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "UserSession"`);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_12ce827a16d121bf6719260b8a"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_365d602943505272f8f651ff4e"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_7b8d9b6e068c045d56b47a484b"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_ac5f4c13d6bc9696cbfb8e5a79"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "StatusPagePrivateUserSession"`);
|
||||
}
|
||||
}
|
||||
@@ -181,6 +181,7 @@ import { MigrationName1761232578396 } from "./1761232578396-MigrationName";
|
||||
import { MigrationName1761834523183 } from "./1761834523183-MigrationName";
|
||||
import { MigrationName1762181014879 } from "./1762181014879-MigrationName";
|
||||
import { MigrationName1762554602716 } from "./1762554602716-MigrationName";
|
||||
import { MigrationName1762890441920 } from "./1762890441920-MigrationName";
|
||||
|
||||
export default [
|
||||
InitialMigration,
|
||||
@@ -366,4 +367,5 @@ export default [
|
||||
MigrationName1761834523183,
|
||||
MigrationName1762181014879,
|
||||
MigrationName1762554602716,
|
||||
MigrationName1762890441920,
|
||||
];
|
||||
|
||||
@@ -106,6 +106,7 @@ import StatusPageHistoryChartBarColorRuleService from "./StatusPageHistoryChartB
|
||||
import StatusPageOwnerTeamService from "./StatusPageOwnerTeamService";
|
||||
import StatusPageOwnerUserService from "./StatusPageOwnerUserService";
|
||||
import StatusPagePrivateUserService from "./StatusPagePrivateUserService";
|
||||
import StatusPagePrivateUserSessionService from "./StatusPagePrivateUserSessionService";
|
||||
import StatusPageResourceService from "./StatusPageResourceService";
|
||||
// Status Page
|
||||
import StatusPageService from "./StatusPageService";
|
||||
@@ -125,6 +126,7 @@ import UserNotificationSettingService from "./UserNotificationSettingService";
|
||||
import UserOnCallLogService from "./UserOnCallLogService";
|
||||
import UserOnCallLogTimelineService from "./UserOnCallLogTimelineService";
|
||||
import UserService from "./UserService";
|
||||
import UserSessionService from "./UserSessionService";
|
||||
import UserTotpAuthService from "./UserTotpAuthService";
|
||||
import UserWebAuthnService from "./UserWebAuthnService";
|
||||
import UserSmsService from "./UserSmsService";
|
||||
@@ -266,6 +268,7 @@ const services: Array<BaseService> = [
|
||||
StatusPageOwnerTeamService,
|
||||
StatusPageOwnerUserService,
|
||||
StatusPagePrivateUserService,
|
||||
StatusPagePrivateUserSessionService,
|
||||
StatusPageResourceService,
|
||||
StatusPageService,
|
||||
StatusPageSsoService,
|
||||
@@ -278,6 +281,7 @@ const services: Array<BaseService> = [
|
||||
TeamService,
|
||||
|
||||
UserService,
|
||||
UserSessionService,
|
||||
UserCallService,
|
||||
UserEmailService,
|
||||
UserNotificationRuleService,
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import DatabaseService from "./DatabaseService";
|
||||
import Model from "../../Models/DatabaseModels/StatusPagePrivateUserSession";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
super(Model);
|
||||
}
|
||||
}
|
||||
|
||||
export default new Service();
|
||||
@@ -345,7 +345,12 @@ export class TeamMemberService extends DatabaseService<TeamMember> {
|
||||
},
|
||||
});
|
||||
|
||||
if (membersInTeam.toNumber() <= 1) {
|
||||
// Skip the one-member guard when SCIM manages membership for the project.
|
||||
const isSCIMEnabled: boolean = await this.isSCIMEnabled(
|
||||
member.projectId!,
|
||||
);
|
||||
|
||||
if (!isSCIMEnabled && membersInTeam.toNumber() <= 1) {
|
||||
throw new BadDataException(
|
||||
Errors.TeamMemberService.ONE_MEMBER_REQUIRED,
|
||||
);
|
||||
|
||||
10
Common/Server/Services/UserSessionService.ts
Normal file
10
Common/Server/Services/UserSessionService.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import DatabaseService from "./DatabaseService";
|
||||
import Model from "../../Models/DatabaseModels/UserSession";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
super(Model);
|
||||
}
|
||||
}
|
||||
|
||||
export default new Service();
|
||||
@@ -2,6 +2,7 @@ import Pill, { PillSize } from "../../../UI/Components/Pill/Pill";
|
||||
import "@testing-library/jest-dom/extend-expect";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import Color from "../../../Types/Color";
|
||||
import IconProp from "../../../Types/Icon/IconProp";
|
||||
import * as React from "react";
|
||||
import { describe, expect, test } from "@jest/globals";
|
||||
|
||||
@@ -46,4 +47,11 @@ describe("<Pill />", () => {
|
||||
render(<Pill text="Love" color={color} size={PillSize.ExtraLarge} />);
|
||||
expect(screen.getByTestId("pill")).toHaveStyle("backgroundColor: #786598");
|
||||
});
|
||||
test("renders icon when provided", () => {
|
||||
const color: Color = new Color("#807149");
|
||||
const { container } = render(
|
||||
<Pill text="Love" color={color} icon={IconProp.Label} />,
|
||||
);
|
||||
expect(container.querySelector('[role="icon"]')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,9 +19,9 @@ export default class Color extends DatabaseProperty {
|
||||
this._color = v;
|
||||
}
|
||||
|
||||
public constructor(color: string) {
|
||||
public constructor(color: string | Color) {
|
||||
super();
|
||||
this.color = color;
|
||||
this.color = color.toString();
|
||||
}
|
||||
|
||||
public override toString(): string {
|
||||
|
||||
64
Common/Types/Database/ColorField.ts
Normal file
64
Common/Types/Database/ColorField.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type BaseModel from "../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
||||
import { ReflectionMetadataType } from "../Reflection";
|
||||
import "reflect-metadata";
|
||||
|
||||
const colorFieldSymbol: symbol = Symbol("ColorField");
|
||||
|
||||
type ColorFieldColumnsFunction = <T extends BaseModel>(
|
||||
target: T,
|
||||
) => Array<string>;
|
||||
|
||||
type FirstColorFieldColumnFunction = <T extends BaseModel>(
|
||||
target: T,
|
||||
) => string | null;
|
||||
|
||||
type IsColorFieldColumnFunction = <T extends BaseModel>(
|
||||
target: T,
|
||||
propertyKey: string,
|
||||
) => boolean;
|
||||
|
||||
const ColorField: () => ReflectionMetadataType = () => {
|
||||
return Reflect.metadata(colorFieldSymbol, true);
|
||||
};
|
||||
|
||||
export const isColorFieldColumn: IsColorFieldColumnFunction = <
|
||||
T extends BaseModel,
|
||||
>(
|
||||
target: T,
|
||||
propertyKey: string,
|
||||
): boolean => {
|
||||
return Boolean(Reflect.getMetadata(colorFieldSymbol, target, propertyKey));
|
||||
};
|
||||
|
||||
export const getColorFieldColumns: ColorFieldColumnsFunction = <
|
||||
T extends BaseModel,
|
||||
>(
|
||||
target: T,
|
||||
): Array<string> => {
|
||||
const columns: Array<string> = [];
|
||||
const keys: Array<string> = Object.keys(target);
|
||||
|
||||
for (const key of keys) {
|
||||
if (isColorFieldColumn(target, key)) {
|
||||
columns.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
return columns;
|
||||
};
|
||||
|
||||
export const getFirstColorFieldColumn: FirstColorFieldColumnFunction = <
|
||||
T extends BaseModel,
|
||||
>(
|
||||
target: T,
|
||||
): string | null => {
|
||||
const columns: Array<string> = getColorFieldColumns(target);
|
||||
|
||||
if (columns.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return columns[0] as string;
|
||||
};
|
||||
|
||||
export default ColorField;
|
||||
@@ -98,6 +98,7 @@ enum IconProp {
|
||||
False = "False",
|
||||
Text = "Text",
|
||||
Circle = "Circle",
|
||||
EmptyCircle = "EmptyCircle",
|
||||
Webhook = "Webhook",
|
||||
SendMessage = "SendMessage",
|
||||
ExternalLink = "ExternalLink",
|
||||
|
||||
@@ -6,13 +6,32 @@ import React, {
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import Select, { ControlProps, GroupBase, OptionProps } from "react-select";
|
||||
import Color from "../../../Types/Color";
|
||||
import Label from "../../../Models/DatabaseModels/Label";
|
||||
import Select, {
|
||||
ControlProps,
|
||||
CSSObjectWithLabel,
|
||||
FormatOptionLabelMeta,
|
||||
GroupBase,
|
||||
OptionProps,
|
||||
} from "react-select";
|
||||
|
||||
export type DropdownValue = string | number | boolean;
|
||||
|
||||
export type DropdownOptionLabel =
|
||||
| Label
|
||||
| {
|
||||
id?: string;
|
||||
name: string;
|
||||
color?: Color;
|
||||
};
|
||||
|
||||
export interface DropdownOption {
|
||||
value: DropdownValue;
|
||||
label: string;
|
||||
description?: string;
|
||||
labels?: Array<DropdownOptionLabel>;
|
||||
color?: Color;
|
||||
}
|
||||
|
||||
export interface ComponentProps {
|
||||
@@ -111,6 +130,333 @@ const Dropdown: FunctionComponent<ComponentProps> = (
|
||||
|
||||
const firstUpdate: React.MutableRefObject<boolean> = useRef(true);
|
||||
|
||||
interface NormalizedDropdownLabel {
|
||||
id?: string;
|
||||
name: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
const normalizeLabelColor: (
|
||||
color?: Color | string | null,
|
||||
) => string | undefined = (
|
||||
color?: Color | string | null,
|
||||
): string | undefined => {
|
||||
if (!color) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (color instanceof Color) {
|
||||
return color.toString();
|
||||
}
|
||||
|
||||
if (typeof color === "string" && color.trim().length > 0) {
|
||||
return color;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const normalizeDropdownLabel: (
|
||||
label: DropdownOptionLabel,
|
||||
) => NormalizedDropdownLabel | null = (
|
||||
label: DropdownOptionLabel,
|
||||
): NormalizedDropdownLabel | null => {
|
||||
if (!label) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const getValueFromModel: (
|
||||
columnName: string,
|
||||
) => string | Color | null | undefined = (
|
||||
columnName: string,
|
||||
): string | Color | null | undefined => {
|
||||
if (
|
||||
typeof (label as Label).getColumnValue === "function" &&
|
||||
typeof (label as Label).getTableColumnMetadata === "function"
|
||||
) {
|
||||
return (label as Label).getColumnValue(columnName) as
|
||||
| string
|
||||
| Color
|
||||
| null
|
||||
| undefined;
|
||||
}
|
||||
|
||||
return (label as any)?.[columnName] as string | Color | null | undefined;
|
||||
};
|
||||
|
||||
const labelName: string | undefined = (() => {
|
||||
const valueFromGetter: string | null | undefined = getValueFromModel(
|
||||
"name",
|
||||
) as string | undefined | null;
|
||||
|
||||
if (valueFromGetter && valueFromGetter.trim().length > 0) {
|
||||
return valueFromGetter;
|
||||
}
|
||||
|
||||
const fallbackName: string | undefined = (label as any)?.name;
|
||||
if (fallbackName && fallbackName.trim().length > 0) {
|
||||
return fallbackName;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
})();
|
||||
|
||||
if (!labelName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rawColor: Color | string | null | undefined = getValueFromModel(
|
||||
"color",
|
||||
) as Color | string | null | undefined;
|
||||
const color: string | undefined =
|
||||
normalizeLabelColor(rawColor) ||
|
||||
normalizeLabelColor((label as any)?.color);
|
||||
|
||||
const idValue: string | undefined = (() => {
|
||||
if (typeof (label as Label).id !== "undefined") {
|
||||
const idFromGetter: ObjectID | null | undefined = (label as Label).id;
|
||||
if (idFromGetter) {
|
||||
return idFromGetter.toString();
|
||||
}
|
||||
}
|
||||
|
||||
const fallbackId: string | undefined =
|
||||
(label as any)?._id || (label as any)?.id;
|
||||
|
||||
if (fallbackId) {
|
||||
return fallbackId;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
})();
|
||||
|
||||
const normalized: NormalizedDropdownLabel = {
|
||||
name: labelName,
|
||||
};
|
||||
|
||||
if (idValue) {
|
||||
normalized.id = idValue;
|
||||
}
|
||||
|
||||
if (color) {
|
||||
normalized.color = color;
|
||||
}
|
||||
|
||||
return normalized;
|
||||
};
|
||||
|
||||
const normalizeLabelCollection: (
|
||||
labels?: Array<DropdownOptionLabel>,
|
||||
) => Array<NormalizedDropdownLabel> = (
|
||||
labels?: Array<DropdownOptionLabel>,
|
||||
): Array<NormalizedDropdownLabel> => {
|
||||
if (!labels || labels.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return labels
|
||||
.map((label: DropdownOptionLabel) => {
|
||||
return normalizeDropdownLabel(label);
|
||||
})
|
||||
.filter(
|
||||
(
|
||||
label: NormalizedDropdownLabel | null,
|
||||
): label is NormalizedDropdownLabel => {
|
||||
return label !== null;
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const renderOptionColorIndicator: (
|
||||
color?: Color | string,
|
||||
) => ReactElement | null = (color?: Color | string): ReactElement | null => {
|
||||
const normalizedColor: string | undefined = color
|
||||
? new Color(color).toString()
|
||||
: undefined;
|
||||
|
||||
if (!normalizedColor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="h-2.5 w-2.5 flex-none rounded-full border border-gray-200"
|
||||
style={{
|
||||
backgroundColor: normalizedColor,
|
||||
}}
|
||||
title={normalizedColor}
|
||||
></span>
|
||||
);
|
||||
};
|
||||
|
||||
const getLabelStyle: (color?: string) => {
|
||||
backgroundColor: string;
|
||||
color: string;
|
||||
} = (color?: string): { backgroundColor: string; color: string } => {
|
||||
if (!color) {
|
||||
return {
|
||||
backgroundColor: "#e5e7eb", // gray-200
|
||||
color: "#374151", // gray-700
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedColor: Color = Color.fromString(color);
|
||||
return {
|
||||
backgroundColor: parsedColor.toString(),
|
||||
color: Color.shouldUseDarkText(parsedColor)
|
||||
? "#111827" // gray-900
|
||||
: "#f9fafb", // gray-50
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
backgroundColor: color,
|
||||
color: "#111827",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const defaultSelectedLabelAccentColor: string = "#6366f1"; // indigo-500
|
||||
|
||||
const resolveSelectedLabelColor: (color?: string) => string = (
|
||||
color?: string,
|
||||
): string => {
|
||||
if (!color) {
|
||||
return defaultSelectedLabelAccentColor;
|
||||
}
|
||||
|
||||
try {
|
||||
return Color.fromString(color).toString();
|
||||
} catch {
|
||||
return defaultSelectedLabelAccentColor;
|
||||
}
|
||||
};
|
||||
|
||||
const renderAssociatedLabels: (
|
||||
labels: Array<NormalizedDropdownLabel>,
|
||||
context: FormatOptionLabelMeta<DropdownOption>["context"],
|
||||
hiddenLabelCount: number,
|
||||
) => ReactElement | null = (
|
||||
labels: Array<NormalizedDropdownLabel>,
|
||||
context: FormatOptionLabelMeta<DropdownOption>["context"],
|
||||
hiddenLabelCount: number,
|
||||
): ReactElement | null => {
|
||||
if (labels.length === 0 && hiddenLabelCount === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (context === "value") {
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
{labels.map((label: NormalizedDropdownLabel, index: number) => {
|
||||
const accentColor: string = resolveSelectedLabelColor(label.color);
|
||||
|
||||
return (
|
||||
<span
|
||||
key={`${label.id || label.name}-selected-${index}`}
|
||||
className="inline-flex items-center gap-1 rounded-full border border-gray-200 bg-white px-2 py-0.5 text-xs font-medium text-gray-600"
|
||||
style={{
|
||||
borderColor: accentColor,
|
||||
}}
|
||||
title={label.name}
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="h-1.5 w-1.5 rounded-full"
|
||||
style={{
|
||||
backgroundColor: accentColor,
|
||||
}}
|
||||
></span>
|
||||
{label.name}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
{hiddenLabelCount > 0 ? (
|
||||
<span className="inline-flex items-center rounded-full border border-gray-200 bg-white px-2 py-0.5 text-xs font-medium text-gray-500">
|
||||
+{hiddenLabelCount}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{labels.map((label: NormalizedDropdownLabel, index: number) => {
|
||||
const { backgroundColor, color } = getLabelStyle(label.color);
|
||||
|
||||
return (
|
||||
<span
|
||||
key={`${label.id || label.name}-menu-${index}`}
|
||||
className="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium shadow-sm"
|
||||
style={{ backgroundColor, color }}
|
||||
>
|
||||
{label.name}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
{hiddenLabelCount > 0 ? (
|
||||
<span className="inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-600">
|
||||
+{hiddenLabelCount}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const formatDropdownOptionLabel: (
|
||||
option: DropdownOption,
|
||||
meta: FormatOptionLabelMeta<DropdownOption>,
|
||||
) => ReactElement = (
|
||||
option: DropdownOption,
|
||||
meta: FormatOptionLabelMeta<DropdownOption>,
|
||||
): ReactElement => {
|
||||
const normalizedLabels: Array<NormalizedDropdownLabel> =
|
||||
normalizeLabelCollection(option.labels);
|
||||
|
||||
const maxVisibleLabels: number = meta.context === "menu" ? 4 : 2;
|
||||
const visibleLabels: Array<NormalizedDropdownLabel> =
|
||||
normalizedLabels.slice(0, maxVisibleLabels);
|
||||
const hiddenLabelCount: number = Math.max(
|
||||
normalizedLabels.length - visibleLabels.length,
|
||||
0,
|
||||
);
|
||||
|
||||
if (meta.context === "value") {
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{renderOptionColorIndicator(option.color)}
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
{option.label}
|
||||
</span>
|
||||
</div>
|
||||
{renderAssociatedLabels(
|
||||
visibleLabels,
|
||||
meta.context,
|
||||
hiddenLabelCount,
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
{renderOptionColorIndicator(option.color)}
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
{option.label}
|
||||
</span>
|
||||
</div>
|
||||
{option.description ? (
|
||||
<span className="text-xs text-gray-500">{option.description}</span>
|
||||
) : null}
|
||||
{renderAssociatedLabels(visibleLabels, meta.context, hiddenLabelCount)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (firstUpdate.current && props.initialValue) {
|
||||
firstUpdate.current = false;
|
||||
@@ -138,6 +484,9 @@ const Dropdown: FunctionComponent<ComponentProps> = (
|
||||
}}
|
||||
>
|
||||
<Select
|
||||
classNamePrefix="ou-select"
|
||||
unstyled={false}
|
||||
formatOptionLabel={formatDropdownOptionLabel}
|
||||
onBlur={() => {
|
||||
props.onBlur?.();
|
||||
}}
|
||||
@@ -150,25 +499,169 @@ const Dropdown: FunctionComponent<ComponentProps> = (
|
||||
}}
|
||||
classNames={{
|
||||
control: (
|
||||
state: ControlProps<any, boolean, GroupBase<any>>,
|
||||
state: ControlProps<DropdownOption, boolean, GroupBase<any>>,
|
||||
): string => {
|
||||
return state.isFocused
|
||||
? "!border-indigo-500"
|
||||
: "border-Gray500-300";
|
||||
const classes: Array<string> = [
|
||||
"!min-h-[40px] !rounded-lg !border !bg-white !shadow-sm !transition-all !duration-150",
|
||||
state.isFocused
|
||||
? "!border-indigo-400 !ring-2 !ring-indigo-100"
|
||||
: "!border-gray-300 hover:!border-indigo-300",
|
||||
state.isDisabled
|
||||
? "!bg-gray-100 !text-gray-400"
|
||||
: "!cursor-pointer",
|
||||
];
|
||||
|
||||
if (props.error) {
|
||||
classes.push("!border-red-400 !ring-2 !ring-red-100");
|
||||
}
|
||||
|
||||
return classes.join(" ");
|
||||
},
|
||||
valueContainer: () => {
|
||||
return "!gap-2 !px-2";
|
||||
},
|
||||
placeholder: () => {
|
||||
return "text-sm text-gray-400";
|
||||
},
|
||||
input: () => {
|
||||
return "text-sm text-gray-900";
|
||||
},
|
||||
singleValue: () => {
|
||||
return "text-sm text-gray-900 font-medium";
|
||||
},
|
||||
indicatorsContainer: () => {
|
||||
return "!gap-1 !px-1";
|
||||
},
|
||||
dropdownIndicator: () => {
|
||||
return "text-gray-500 transition-colors duration-150 hover:text-indigo-400";
|
||||
},
|
||||
clearIndicator: () => {
|
||||
return "text-gray-400 transition-colors duration-150 hover:text-red-500";
|
||||
},
|
||||
menu: () => {
|
||||
return "!mt-2 !rounded-xl !border !border-gray-100 !bg-white !shadow-xl";
|
||||
},
|
||||
menuList: () => {
|
||||
return "!py-2";
|
||||
},
|
||||
option: (
|
||||
state: OptionProps<any, boolean, GroupBase<any>>,
|
||||
state: OptionProps<DropdownOption, boolean, GroupBase<any>>,
|
||||
): string => {
|
||||
if (state.isDisabled) {
|
||||
return "bg-gray-100";
|
||||
return "px-3 py-2 text-sm text-gray-300 cursor-not-allowed";
|
||||
}
|
||||
|
||||
if (state.isSelected) {
|
||||
return "!bg-indigo-500";
|
||||
return "px-3 py-2 text-sm bg-indigo-200 text-indigo-900";
|
||||
}
|
||||
|
||||
if (state.isFocused) {
|
||||
return "!bg-indigo-100";
|
||||
return "px-3 py-2 text-sm bg-indigo-100 text-indigo-700";
|
||||
}
|
||||
return "";
|
||||
|
||||
return "px-3 py-2 text-sm text-gray-700";
|
||||
},
|
||||
noOptionsMessage: () => {
|
||||
return "px-3 py-2 text-sm text-gray-500";
|
||||
},
|
||||
multiValue: () => {
|
||||
return "flex items-center gap-2 rounded-lg border border-indigo-100 bg-indigo-50 px-2 py-1";
|
||||
},
|
||||
multiValueLabel: () => {
|
||||
return "flex flex-wrap items-center gap-2 text-sm font-medium text-indigo-900";
|
||||
},
|
||||
multiValueRemove: () => {
|
||||
return "text-indigo-400 hover:text-indigo-600 transition-colors duration-150";
|
||||
},
|
||||
}}
|
||||
styles={{
|
||||
dropdownIndicator: (
|
||||
provided: CSSObjectWithLabel,
|
||||
): CSSObjectWithLabel => {
|
||||
return {
|
||||
...provided,
|
||||
padding: 8,
|
||||
};
|
||||
},
|
||||
clearIndicator: (
|
||||
provided: CSSObjectWithLabel,
|
||||
): CSSObjectWithLabel => {
|
||||
return {
|
||||
...provided,
|
||||
padding: 8,
|
||||
};
|
||||
},
|
||||
indicatorSeparator: (): CSSObjectWithLabel => {
|
||||
return {
|
||||
display: "none",
|
||||
} as CSSObjectWithLabel;
|
||||
},
|
||||
option: (
|
||||
provided: CSSObjectWithLabel,
|
||||
state: OptionProps<
|
||||
DropdownOption,
|
||||
boolean,
|
||||
GroupBase<DropdownOption>
|
||||
>,
|
||||
): CSSObjectWithLabel => {
|
||||
if (state.isSelected) {
|
||||
return {
|
||||
...provided,
|
||||
backgroundColor: "#c7d2fe", // indigo-200
|
||||
color: "#1e1b4b", // indigo-900
|
||||
};
|
||||
}
|
||||
|
||||
if (state.isFocused) {
|
||||
return {
|
||||
...provided,
|
||||
backgroundColor: "#e0e7ff", // indigo-100
|
||||
color: "#312e81", // indigo-800
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...provided,
|
||||
color: "#374151", // gray-700
|
||||
};
|
||||
},
|
||||
multiValue: (provided: CSSObjectWithLabel): CSSObjectWithLabel => {
|
||||
return {
|
||||
...provided,
|
||||
backgroundColor: "#eef2ff", // indigo-50
|
||||
borderRadius: 8,
|
||||
border: "1px solid #c7d2fe", // indigo-200
|
||||
paddingLeft: 4,
|
||||
paddingRight: 4,
|
||||
};
|
||||
},
|
||||
multiValueLabel: (
|
||||
provided: CSSObjectWithLabel,
|
||||
): CSSObjectWithLabel => {
|
||||
return {
|
||||
...provided,
|
||||
color: "#312e81", // indigo-800
|
||||
fontSize: "0.875rem",
|
||||
fontWeight: 500,
|
||||
};
|
||||
},
|
||||
multiValueRemove: (
|
||||
provided: CSSObjectWithLabel,
|
||||
): CSSObjectWithLabel => {
|
||||
return {
|
||||
...provided,
|
||||
color: "#6366f1", // indigo-500
|
||||
":hover": {
|
||||
color: "#4f46e5", // indigo-600
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
};
|
||||
},
|
||||
menuPortal: (base: CSSObjectWithLabel): CSSObjectWithLabel => {
|
||||
return {
|
||||
...base,
|
||||
zIndex: 50,
|
||||
};
|
||||
},
|
||||
}}
|
||||
isClearable={true}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
CategoryCheckboxOption,
|
||||
CheckboxCategory,
|
||||
} from "../CategoryCheckbox/CategoryCheckboxTypes";
|
||||
import type { DropdownOption } from "../Dropdown/Dropdown";
|
||||
import Loader, { LoaderType } from "../Loader/Loader";
|
||||
import Pill, { PillSize } from "../Pill/Pill";
|
||||
import { FormErrors, FormProps, FormSummaryConfig } from "./BasicForm";
|
||||
@@ -24,6 +25,7 @@ import AnalyticsBaseModel from "../../../Models/AnalyticsModels/AnalyticsBaseMod
|
||||
import AccessControlModel from "../../../Models/DatabaseModels/DatabaseBaseModel/AccessControlModel";
|
||||
import BaseModel from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
||||
import FileModel from "../../../Models/DatabaseModels/DatabaseBaseModel/FileModel";
|
||||
import Label from "../../../Models/DatabaseModels/Label";
|
||||
import URL from "../../../Types/API/URL";
|
||||
import { ColumnAccessControl } from "../../../Types/BaseDatabase/AccessControl";
|
||||
import { Black, VeryLightGray } from "../../../Types/BrandColors";
|
||||
@@ -412,17 +414,26 @@ const ModelForm: <TBaseModel extends BaseModel>(
|
||||
[field.dropdownModal.valueField]: true,
|
||||
} as any;
|
||||
|
||||
let hasAccessControlColumn: boolean = false;
|
||||
let colorColumnName: string | null = null;
|
||||
let shouldSelectColorColumn: boolean = false;
|
||||
|
||||
colorColumnName = tempModel.getFirstColorColumn();
|
||||
|
||||
if (colorColumnName) {
|
||||
select[colorColumnName] = true;
|
||||
shouldSelectColorColumn = true;
|
||||
}
|
||||
|
||||
const accessControlColumnName: string | null =
|
||||
tempModel.getAccessControlColumn();
|
||||
|
||||
// also select labels, so they can select resources by labels. This is useful for resources like monitors, etc.
|
||||
if (tempModel.getAccessControlColumn()) {
|
||||
select[tempModel.getAccessControlColumn()!] = {
|
||||
if (accessControlColumnName) {
|
||||
select[accessControlColumnName] = {
|
||||
_id: true,
|
||||
name: true,
|
||||
color: true,
|
||||
} as any;
|
||||
|
||||
hasAccessControlColumn = true;
|
||||
}
|
||||
|
||||
const listResult: ListResult<BaseModel> =
|
||||
@@ -436,22 +447,51 @@ const ModelForm: <TBaseModel extends BaseModel>(
|
||||
});
|
||||
|
||||
if (listResult.data && listResult.data.length > 0) {
|
||||
field.dropdownOptions = listResult.data.map((item: BaseModel) => {
|
||||
if (!field.dropdownModal) {
|
||||
throw new BadDataException("Dropdown Modal value mot found");
|
||||
}
|
||||
const dropdownOptions: Array<DropdownOption> = listResult.data.map(
|
||||
(item: BaseModel) => {
|
||||
if (!field.dropdownModal) {
|
||||
throw new BadDataException("Dropdown Modal value mot found");
|
||||
}
|
||||
|
||||
return {
|
||||
label: (item as any)[
|
||||
field.dropdownModal?.labelField
|
||||
].toString(),
|
||||
value: (item as any)[
|
||||
field.dropdownModal?.valueField
|
||||
].toString(),
|
||||
};
|
||||
});
|
||||
const option: DropdownOption = {
|
||||
label: (item as any)[
|
||||
field.dropdownModal?.labelField
|
||||
].toString(),
|
||||
value: (item as any)[
|
||||
field.dropdownModal?.valueField
|
||||
].toString(),
|
||||
};
|
||||
|
||||
if (hasAccessControlColumn) {
|
||||
if (colorColumnName && shouldSelectColorColumn) {
|
||||
const color: Color = item.getColumnValue(
|
||||
colorColumnName,
|
||||
) as Color;
|
||||
if (color) {
|
||||
option.color = color;
|
||||
}
|
||||
}
|
||||
|
||||
if (accessControlColumnName) {
|
||||
const labelsForItem: Array<AccessControlModel> = (
|
||||
((item as any)[
|
||||
accessControlColumnName
|
||||
] as Array<AccessControlModel>) || []
|
||||
).filter((label: AccessControlModel | null) => {
|
||||
return Boolean(label);
|
||||
}) as Array<AccessControlModel>;
|
||||
|
||||
if (labelsForItem.length > 0) {
|
||||
option.labels = labelsForItem as Array<Label>;
|
||||
}
|
||||
}
|
||||
|
||||
return option;
|
||||
},
|
||||
);
|
||||
|
||||
field.dropdownOptions = dropdownOptions;
|
||||
|
||||
if (accessControlColumnName) {
|
||||
const categories: Array<CheckboxCategory> = [];
|
||||
|
||||
// populate categories.
|
||||
@@ -459,8 +499,7 @@ const ModelForm: <TBaseModel extends BaseModel>(
|
||||
let localLabels: Array<AccessControlModel> = [];
|
||||
|
||||
for (const item of listResult.data) {
|
||||
const accessControlColumn: string | null =
|
||||
tempModel.getAccessControlColumn()!;
|
||||
const accessControlColumn: string = accessControlColumnName;
|
||||
const labels: Array<AccessControlModel> =
|
||||
((item as any)[
|
||||
accessControlColumn
|
||||
@@ -516,8 +555,7 @@ const ModelForm: <TBaseModel extends BaseModel>(
|
||||
const options: Array<CategoryCheckboxOption> = [];
|
||||
|
||||
for (const item of listResult.data) {
|
||||
const accessControlColumn: string =
|
||||
tempModel.getAccessControlColumn()!;
|
||||
const accessControlColumn: string = accessControlColumnName;
|
||||
const labels: Array<AccessControlModel> =
|
||||
((item as any)[
|
||||
accessControlColumn
|
||||
@@ -554,9 +592,8 @@ const ModelForm: <TBaseModel extends BaseModel>(
|
||||
options: options,
|
||||
},
|
||||
accessControlColumnTitle:
|
||||
tempModel.getTableColumnMetadata(
|
||||
tempModel.getAccessControlColumn()!,
|
||||
).title || "",
|
||||
tempModel.getTableColumnMetadata(accessControlColumnName)
|
||||
.title || "",
|
||||
};
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1102,6 +1102,10 @@ const Icon: FunctionComponent<ComponentProps> = ({
|
||||
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
|
||||
/>,
|
||||
);
|
||||
} else if (icon === IconProp.EmptyCircle) {
|
||||
return getSvgWrapper(<circle cx="12" cy="12" r="7.5" />, {
|
||||
strokeWidth: "2.25",
|
||||
});
|
||||
} else if (icon === IconProp.Circle) {
|
||||
return getSvgWrapper(
|
||||
<path
|
||||
|
||||
50
Common/UI/Components/Label/Label.tsx
Normal file
50
Common/UI/Components/Label/Label.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import React, { CSSProperties, FunctionComponent, ReactElement } from "react";
|
||||
import LabelModel from "../../../Models/DatabaseModels/Label";
|
||||
import Pill, { ComponentProps as PillProps, PillSize } from "../Pill/Pill";
|
||||
import { Black } from "../../../Types/BrandColors";
|
||||
import Color from "../../../Types/Color";
|
||||
import IconProp from "../../../Types/Icon/IconProp";
|
||||
|
||||
export interface ComponentProps {
|
||||
label: LabelModel;
|
||||
size?: PillSize | undefined;
|
||||
style?: CSSProperties;
|
||||
isMinimal?: boolean | undefined;
|
||||
}
|
||||
|
||||
const LabelElement: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
): ReactElement => {
|
||||
const { label } = props;
|
||||
|
||||
const resolveColor: Color = (() => {
|
||||
if (!label.color) {
|
||||
return Black;
|
||||
}
|
||||
|
||||
if (typeof label.color === "string") {
|
||||
return Color.fromString(label.color);
|
||||
}
|
||||
|
||||
return label.color;
|
||||
})();
|
||||
|
||||
const text: string = label.name || label.slug || "";
|
||||
|
||||
const pillProps: PillProps = {
|
||||
color: resolveColor,
|
||||
text,
|
||||
size: props.size,
|
||||
isMinimal: props.isMinimal,
|
||||
tooltip: label.description || undefined,
|
||||
icon: IconProp.EmptyCircle,
|
||||
};
|
||||
|
||||
if (props.style) {
|
||||
pillProps.style = props.style;
|
||||
}
|
||||
|
||||
return <Pill {...pillProps} />;
|
||||
};
|
||||
|
||||
export default LabelElement;
|
||||
@@ -1,25 +1,29 @@
|
||||
import LabelElement from "./Label";
|
||||
import TableColumnListComponent from "Common/UI/Components/TableColumnList/TableColumnListComponent";
|
||||
import Label from "Common/Models/DatabaseModels/Label";
|
||||
import TableColumnListComponent from "../TableColumnList/TableColumnListComponent";
|
||||
import LabelModel from "../../../Models/DatabaseModels/Label";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
|
||||
export interface ComponentProps {
|
||||
labels: Array<Label>;
|
||||
labels: Array<LabelModel>;
|
||||
}
|
||||
|
||||
const LabelsElement: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
): ReactElement => {
|
||||
return (
|
||||
// {/** >4 because 3 labels are shown by default and then the more text is shown */}
|
||||
<TableColumnListComponent
|
||||
items={props.labels}
|
||||
moreText={props.labels.length > 4 ? "more labels" : "more label"}
|
||||
className={props.labels.length > 0 ? "-mb-1 -mt-1" : ""}
|
||||
getEachElement={(label: Label) => {
|
||||
getEachElement={(label: LabelModel) => {
|
||||
return (
|
||||
<div className={props.labels.length > 0 ? "my-2" : ""}>
|
||||
<LabelElement label={label} />
|
||||
<LabelElement
|
||||
label={label}
|
||||
style={{
|
||||
marginRight: "5px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
@@ -38,6 +38,7 @@ import { ListDetailProps } from "../List/ListRow";
|
||||
import ConfirmModal from "../Modal/ConfirmModal";
|
||||
import { ModalWidth } from "../Modal/Modal";
|
||||
import Filter from "../ModelFilter/Filter";
|
||||
import { DropdownOption, DropdownOptionLabel } from "../Dropdown/Dropdown";
|
||||
import OrderedStatesList from "../OrderedStatesList/OrderedStatesList";
|
||||
import Pill from "../Pill/Pill";
|
||||
import Table from "../Table/Table";
|
||||
@@ -51,6 +52,7 @@ import AnalyticsBaseModel, {
|
||||
import BaseModel, {
|
||||
DatabaseBaseModelType,
|
||||
} from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
||||
import AccessControlModel from "../../../Models/DatabaseModels/DatabaseBaseModel/AccessControlModel";
|
||||
import Route from "../../../Types/API/Route";
|
||||
import URL from "../../../Types/API/URL";
|
||||
import { ColumnAccessControl } from "../../../Types/BaseDatabase/AccessControl";
|
||||
@@ -64,6 +66,7 @@ import { Yellow } from "../../../Types/BrandColors";
|
||||
import { LIMIT_PER_PROJECT } from "../../../Types/Database/LimitMax";
|
||||
import Dictionary from "../../../Types/Dictionary";
|
||||
import BadDataException from "../../../Types/Exception/BadDataException";
|
||||
import Color from "../../../Types/Color";
|
||||
import {
|
||||
ErrorFunction,
|
||||
PromiseVoidFunction,
|
||||
@@ -641,30 +644,155 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
|
||||
|
||||
const query: Query<TBaseModel> = filter.filterQuery || {};
|
||||
|
||||
let colorColumnName: string | null = null;
|
||||
let accessControlColumnName: string | null = null;
|
||||
|
||||
if (
|
||||
filter.filterEntityType &&
|
||||
filter.filterEntityType.prototype instanceof BaseModel
|
||||
) {
|
||||
const filterModel: BaseModel =
|
||||
new (filter.filterEntityType as DatabaseBaseModelType)();
|
||||
colorColumnName = filterModel.getFirstColorColumn();
|
||||
accessControlColumnName = filterModel.getAccessControlColumn();
|
||||
}
|
||||
|
||||
const select: Select<TBaseModel> = {
|
||||
[filter.filterDropdownField.label]: true,
|
||||
[filter.filterDropdownField.value]: true,
|
||||
} as Select<TBaseModel>;
|
||||
|
||||
if (colorColumnName) {
|
||||
(select as Dictionary<boolean>)[colorColumnName] = true;
|
||||
}
|
||||
|
||||
if (accessControlColumnName) {
|
||||
(select as Dictionary<JSONObject>)[accessControlColumnName] = {
|
||||
_id: true,
|
||||
name: true,
|
||||
color: true,
|
||||
} as JSONObject;
|
||||
}
|
||||
|
||||
const listResult: ListResult<TBaseModel> =
|
||||
await props.callbacks.getList({
|
||||
modelType: filter.filterEntityType,
|
||||
query: query,
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
select: {
|
||||
[filter.filterDropdownField.label]: true,
|
||||
[filter.filterDropdownField.value]: true,
|
||||
} as any,
|
||||
select: select,
|
||||
sort: {},
|
||||
});
|
||||
|
||||
filter.filterDropdownOptions = [];
|
||||
|
||||
for (const item of listResult.data) {
|
||||
filter.filterDropdownOptions.push({
|
||||
const option: DropdownOption = {
|
||||
value: item.getColumnValue(
|
||||
filter.filterDropdownField.value,
|
||||
) as string,
|
||||
label: item.getColumnValue(
|
||||
filter.filterDropdownField.label,
|
||||
) as string,
|
||||
});
|
||||
};
|
||||
|
||||
if (colorColumnName) {
|
||||
const colorValue: Color | string | null = item.getColumnValue(
|
||||
colorColumnName,
|
||||
) as Color | string | null;
|
||||
|
||||
if (colorValue instanceof Color) {
|
||||
option.color = colorValue;
|
||||
} else if (
|
||||
typeof colorValue === "string" &&
|
||||
colorValue.trim().length > 0
|
||||
) {
|
||||
try {
|
||||
option.color = new Color(colorValue);
|
||||
} catch {
|
||||
// ignore invalid colors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (accessControlColumnName) {
|
||||
const accessControlValue:
|
||||
| AccessControlModel
|
||||
| Array<AccessControlModel>
|
||||
| null =
|
||||
(item.getColumnValue(accessControlColumnName) as
|
||||
| AccessControlModel
|
||||
| Array<AccessControlModel>
|
||||
| null) || null;
|
||||
|
||||
const accessControlItems: Array<AccessControlModel> =
|
||||
Array.isArray(accessControlValue)
|
||||
? accessControlValue
|
||||
: accessControlValue
|
||||
? [accessControlValue]
|
||||
: [];
|
||||
|
||||
type SimplifiedDropdownLabel = {
|
||||
id?: string;
|
||||
name: string;
|
||||
color?: Color;
|
||||
};
|
||||
|
||||
const dropdownLabels: Array<SimplifiedDropdownLabel> =
|
||||
accessControlItems
|
||||
.map((label: AccessControlModel | null) => {
|
||||
if (!label) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const labelNameRaw: string | null = label.getColumnValue(
|
||||
"name",
|
||||
) as string | null;
|
||||
|
||||
if (!labelNameRaw) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const labelName: string = labelNameRaw.toString().trim();
|
||||
|
||||
if (!labelName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const labelColorValue: Color | null = label.getColumnValue(
|
||||
"color",
|
||||
) as Color | null;
|
||||
|
||||
const normalizedLabel: SimplifiedDropdownLabel = {
|
||||
name: labelName,
|
||||
};
|
||||
|
||||
const labelId: ObjectID | null = label.id;
|
||||
|
||||
if (labelId) {
|
||||
normalizedLabel.id = labelId.toString();
|
||||
}
|
||||
|
||||
if (labelColorValue) {
|
||||
normalizedLabel.color = labelColorValue;
|
||||
}
|
||||
|
||||
return normalizedLabel;
|
||||
})
|
||||
.filter(
|
||||
(
|
||||
label: SimplifiedDropdownLabel | null,
|
||||
): label is SimplifiedDropdownLabel => {
|
||||
return label !== null;
|
||||
},
|
||||
);
|
||||
|
||||
if (dropdownLabels.length > 0) {
|
||||
option.labels = dropdownLabels as Array<DropdownOptionLabel>;
|
||||
}
|
||||
}
|
||||
|
||||
filter.filterDropdownOptions.push(option);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,9 @@ import Analytics from "../../Utils/Analytics";
|
||||
import Breadcrumbs from "../Breadcrumbs/Breadcrumbs";
|
||||
import ErrorMessage from "../ErrorMessage/ErrorMessage";
|
||||
import PageLoader from "../Loader/PageLoader";
|
||||
import Pill from "../Pill/Pill";
|
||||
import LabelElement from "../Label/Label";
|
||||
import Link from "../../../Types/Link";
|
||||
import Label from "../../../Models/DatabaseModels/Label";
|
||||
import Color from "../../../Types/Color";
|
||||
import { Black } from "../../../Types/BrandColors";
|
||||
import LabelModel from "../../../Models/DatabaseModels/Label";
|
||||
import React, { FunctionComponent, ReactElement, useEffect } from "react";
|
||||
|
||||
export interface ComponentProps {
|
||||
@@ -17,7 +15,7 @@ export interface ComponentProps {
|
||||
className?: string | undefined;
|
||||
isLoading?: boolean | undefined;
|
||||
error?: string | undefined;
|
||||
labels?: Array<Label> | undefined;
|
||||
labels?: Array<LabelModel> | undefined;
|
||||
}
|
||||
|
||||
const Page: FunctionComponent<ComponentProps> = (
|
||||
@@ -71,33 +69,19 @@ const Page: FunctionComponent<ComponentProps> = (
|
||||
</span>
|
||||
<div className="flex flex-wrap items-center gap-2 justify-end">
|
||||
{props.labels
|
||||
.filter((label: Label | null) => {
|
||||
.filter((label: LabelModel | null) => {
|
||||
return Boolean(label && (label.name || label.slug));
|
||||
})
|
||||
.map((label: Label, index: number) => {
|
||||
const resolveColor: Color = (() => {
|
||||
if (!label.color) {
|
||||
return Black;
|
||||
}
|
||||
|
||||
if (typeof label.color === "string") {
|
||||
return Color.fromString(label.color);
|
||||
}
|
||||
|
||||
return label.color;
|
||||
})();
|
||||
|
||||
.map((label: LabelModel, index: number) => {
|
||||
return (
|
||||
<Pill
|
||||
<LabelElement
|
||||
key={
|
||||
label.id?.toString() ||
|
||||
label._id ||
|
||||
label.slug ||
|
||||
`${label.name || "label"}-${index}`
|
||||
}
|
||||
color={resolveColor}
|
||||
text={label.name || label.slug || "Label"}
|
||||
isMinimal={false}
|
||||
label={label}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Black } from "../../../Types/BrandColors";
|
||||
import Color from "../../../Types/Color";
|
||||
import Icon, { SizeProp, ThickProp } from "../Icon/Icon";
|
||||
import IconProp from "../../../Types/Icon/IconProp";
|
||||
import React, { CSSProperties, FunctionComponent, ReactElement } from "react";
|
||||
import Tooltip from "../Tooltip/Tooltip";
|
||||
import { GetReactElementFunction } from "../../Types/FunctionTypes";
|
||||
@@ -18,6 +20,7 @@ export interface ComponentProps {
|
||||
style?: CSSProperties;
|
||||
isMinimal?: boolean | undefined;
|
||||
tooltip?: string | undefined;
|
||||
icon?: IconProp | undefined;
|
||||
}
|
||||
|
||||
const Pill: FunctionComponent<ComponentProps> = (
|
||||
@@ -47,7 +50,7 @@ const Pill: FunctionComponent<ComponentProps> = (
|
||||
return (
|
||||
<span
|
||||
data-testid="pill"
|
||||
className="rounded-full p-1 pl-3 pr-3"
|
||||
className="inline-flex items-center rounded-full p-1 pl-3 pr-3"
|
||||
style={{
|
||||
// https://stackoverflow.com/questions/3942878/how-to-decide-font-color-in-white-or-black-depending-on-background-color
|
||||
|
||||
@@ -63,8 +66,15 @@ const Pill: FunctionComponent<ComponentProps> = (
|
||||
...props.style,
|
||||
}}
|
||||
>
|
||||
{" "}
|
||||
{props.text}{" "}
|
||||
{props.icon ? (
|
||||
<Icon
|
||||
icon={props.icon}
|
||||
size={SizeProp.Small}
|
||||
thick={ThickProp.Thick}
|
||||
className="mr-2"
|
||||
/>
|
||||
) : null}
|
||||
{props.text}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import NumberUtil from "../../Utils/Number";
|
||||
import { DropdownOption } from "../Components/Dropdown/Dropdown";
|
||||
import BaseModel from "../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
||||
import Color from "../../Types/Color";
|
||||
|
||||
type Enum<E> = Record<keyof E, number | string> & { [k: number]: string };
|
||||
|
||||
@@ -52,10 +53,27 @@ export default class DropdownUtil {
|
||||
valueField: string;
|
||||
}): Array<DropdownOption> {
|
||||
return data.array.map((item: TBaseModel) => {
|
||||
return {
|
||||
const option: DropdownOption = {
|
||||
label: item.getColumnValue(data.labelField) as string,
|
||||
value: item.getColumnValue(data.valueField) as string,
|
||||
};
|
||||
|
||||
const colorColumnName: string | null =
|
||||
typeof item.getFirstColorColumn === "function"
|
||||
? item.getFirstColorColumn()
|
||||
: null;
|
||||
|
||||
if (colorColumnName) {
|
||||
const color: Color | null = item.getColumnValue(
|
||||
colorColumnName,
|
||||
) as Color | null;
|
||||
|
||||
if (color) {
|
||||
option.color = color;
|
||||
}
|
||||
}
|
||||
|
||||
return option;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LabelsElement from "../Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import AlertElement from "./Alert";
|
||||
import { Black } from "Common/Types/BrandColors";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LabelsElement from "../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import MonitorsElement from "../../Components/Monitor/Monitors";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import IncidentElement from "./Incident";
|
||||
|
||||
@@ -9,7 +9,7 @@ import SortOrder from "Common/Types/BaseDatabase/SortOrder";
|
||||
import ListResult from "Common/Types/BaseDatabase/ListResult";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import ComponentLoader from "Common/UI/Components/ComponentLoader/ComponentLoader";
|
||||
import LabelsElement from "./Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
import Exception from "Common/Types/Exception/Exception";
|
||||
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { Black } from "Common/Types/BrandColors";
|
||||
import Pill from "Common/UI/Components/Pill/Pill";
|
||||
import Label from "Common/Models/DatabaseModels/Label";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
|
||||
export interface ComponentProps {
|
||||
label: Label;
|
||||
}
|
||||
|
||||
const LabelElement: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
): ReactElement => {
|
||||
return (
|
||||
<Pill
|
||||
color={props.label.color || Black}
|
||||
text={props.label.name || ""}
|
||||
style={{
|
||||
marginRight: "5px",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default LabelElement;
|
||||
@@ -1,4 +1,4 @@
|
||||
import LabelsElement from "../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import MonitorTypeUtil from "../../Utils/MonitorType";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import LabelsElement from "../Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import MonitorsElement from "../Monitor/Monitors";
|
||||
import StatusPagesElement from "../StatusPage/StatusPagesElement";
|
||||
import Route from "Common/Types/API/Route";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LabelsElement from "../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
|
||||
import ModelTable from "Common/UI/Components/ModelTable/ModelTable";
|
||||
|
||||
@@ -16,7 +16,7 @@ import IncidentSeverityElement from "../../IncidentSeverity/IncidentSeverityElem
|
||||
import IncidentStateElement from "../../IncidentState/IncidentStateElement";
|
||||
import ScheduledMaintenanceStateElement from "../../ScheduledMaintenanceState/ScheduledMaintenanceStateElement";
|
||||
import MonitorStatusElement from "../../MonitorStatus/MonitorStatusElement";
|
||||
import LabelElement from "../../Label/Label";
|
||||
import LabelElement from "Common/UI/Components/Label/Label";
|
||||
import MonitorElement from "../../Monitor/Monitor";
|
||||
import { GetReactElementFunction } from "Common/UI/Types/FunctionTypes";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LabelsElement from "../../../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import PageComponentProps from "../../../PageComponentProps";
|
||||
import CodeRepositoryType from "Common/Types/CodeRepository/CodeRepositoryType";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Banner from "Common/UI/Components/Banner/Banner";
|
||||
import LabelsElement from "../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import PageMap from "../../Utils/PageMap";
|
||||
import RouteMap, { RouteUtil } from "../../Utils/RouteMap";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import ChangeAlertState from "../../../Components/Alert/ChangeState";
|
||||
import LabelsElement from "../../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import OnCallDutyPoliciesView from "../../../Components/OnCallPolicy/OnCallPolicies";
|
||||
import PageComponentProps from "../../PageComponentProps";
|
||||
import SortOrder from "Common/Types/BaseDatabase/SortOrder";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LabelsElement from "../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import PageMap from "../../Utils/PageMap";
|
||||
import RouteMap, { RouteUtil } from "../../Utils/RouteMap";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LabelsElement from "../../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import PageComponentProps from "../../PageComponentProps";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
|
||||
|
||||
@@ -45,6 +45,8 @@ import FetchMonitors from "../../Components/Monitor/FetchMonitors";
|
||||
import FetchIncidentSeverities from "../../Components/IncidentSeverity/FetchIncidentSeverity";
|
||||
import IncidentState from "Common/Models/DatabaseModels/IncidentState";
|
||||
import SortOrder from "Common/Types/BaseDatabase/SortOrder";
|
||||
import Color from "Common/Types/Color";
|
||||
import { DropdownOption } from "Common/UI/Components/Dropdown/Dropdown";
|
||||
|
||||
const IncidentCreate: FunctionComponent<
|
||||
PageComponentProps
|
||||
@@ -313,18 +315,24 @@ const IncidentCreate: FunctionComponent<
|
||||
select: {
|
||||
_id: true,
|
||||
name: true,
|
||||
color: true,
|
||||
},
|
||||
sort: {
|
||||
order: SortOrder.Ascending,
|
||||
},
|
||||
});
|
||||
|
||||
return incidentStates.data.map((state: IncidentState) => {
|
||||
return {
|
||||
label: state.name || "",
|
||||
value: state._id?.toString() || "",
|
||||
};
|
||||
});
|
||||
return incidentStates.data.map(
|
||||
(state: IncidentState): DropdownOption => {
|
||||
const option: DropdownOption = {
|
||||
label: state.name || "",
|
||||
value: state._id?.toString() || "",
|
||||
color: state.color as Color,
|
||||
};
|
||||
|
||||
return option;
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
// Silently fail and return empty array
|
||||
return [];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import ChangeIncidentState from "../../../Components/Incident/ChangeState";
|
||||
import LabelsElement from "../../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import MonitorsElement from "../../../Components/Monitor/Monitors";
|
||||
import OnCallDutyPoliciesView from "../../../Components/OnCallPolicy/OnCallPolicies";
|
||||
import SubscriberNotificationStatus from "../../../Components/StatusPageSubscribers/SubscriberNotificationStatus";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LabelsElement from "../../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import DisabledWarning from "../../../Components/Monitor/DisabledWarning";
|
||||
import IncomingMonitorLink from "../../../Components/Monitor/IncomingRequestMonitor/IncomingMonitorLink";
|
||||
import ServerMonitorDocumentation from "../../../Components/Monitor/ServerMonitor/Documentation";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LabelsElement from "../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import CurrentStatusElement from "../../Components/MonitorGroup/CurrentStatus";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import PageMap from "../../Utils/PageMap";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LabelsElement from "../../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import PageComponentProps from "../../PageComponentProps";
|
||||
import URL from "Common/Types/API/URL";
|
||||
import SortOrder from "Common/Types/BaseDatabase/SortOrder";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LabelsElement from "../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import PageComponentProps from "../PageComponentProps";
|
||||
import URL from "Common/Types/API/URL";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LabelsElement from "../../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import PageComponentProps from "../../PageComponentProps";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LabelsElement from "../../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import PageComponentProps from "../../PageComponentProps";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LabelsElement from "../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import PageComponentProps from "../PageComponentProps";
|
||||
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LabelsElement from "../../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import MonitorsElement from "../../../Components/Monitor/Monitors";
|
||||
import ChangeScheduledMaintenanceState from "../../../Components/ScheduledMaintenance/ChangeState";
|
||||
import StatusPagesElement from "../../../Components/StatusPage/StatusPagesElement";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LabelsElement from "../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import ServiceCatalogElement from "../../Components/ServiceCatalog/ServiceElement";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import PageComponentProps from "../PageComponentProps";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LabelsElement from "../../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import PageComponentProps from "../../PageComponentProps";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import TechStack from "Common/Types/ServiceCatalog/TechStack";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LabelsElement from "../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import PageMap from "../../Utils/PageMap";
|
||||
import RouteMap, { RouteUtil } from "../../Utils/RouteMap";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LabelsElement from "../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import MonitorsElement from "../../Components/Monitor/Monitors";
|
||||
import OnCallDutyPoliciesView from "../../Components/OnCallPolicy/OnCallPolicies";
|
||||
import TeamElement from "../../Components/Team/Team";
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import PageComponentProps from "../PageComponentProps";
|
||||
import Color from "Common/Types/Color";
|
||||
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
|
||||
import ModelTable from "Common/UI/Components/ModelTable/ModelTable";
|
||||
import Pill from "Common/UI/Components/Pill/Pill";
|
||||
import LabelElement from "Common/UI/Components/Label/Label";
|
||||
import FieldType from "Common/UI/Components/Types/FieldType";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import Label from "Common/Models/DatabaseModels/Label";
|
||||
@@ -92,12 +91,7 @@ const Labels: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
type: FieldType.Text,
|
||||
|
||||
getElement: (item: Label): ReactElement => {
|
||||
return (
|
||||
<Pill
|
||||
color={item["color"] as Color}
|
||||
text={item["name"] as string}
|
||||
/>
|
||||
);
|
||||
return <LabelElement label={item} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LabelsElement from "../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import UserElement from "../../Components/User/User";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import PageMap from "../../Utils/PageMap";
|
||||
|
||||
@@ -21,7 +21,7 @@ import React, {
|
||||
ReactElement,
|
||||
useState,
|
||||
} from "react";
|
||||
import LabelsElement from "../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import CardModelDetail from "Common/UI/Components/ModelDetail/CardModelDetail";
|
||||
import Project from "Common/Models/DatabaseModels/Project";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LabelsElement from "../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import MonitorsElement from "../../Components/Monitor/Monitors";
|
||||
import TeamElement from "../../Components/Team/Team";
|
||||
import UserElement from "../../Components/User/User";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LabelsElement from "../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import UserElement from "../../Components/User/User";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import PageMap from "../../Utils/PageMap";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LabelsElement from "../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import PageComponentProps from "../PageComponentProps";
|
||||
import URL from "Common/Types/API/URL";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LabelsElement from "../../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import PageComponentProps from "../../PageComponentProps";
|
||||
import StatusPagePreviewLink from "./StatusPagePreviewLink";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LabelsElement from "../../../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import PageComponentProps from "../../../PageComponentProps";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LabelsElement from "../../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import PageComponentProps from "../../PageComponentProps";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LabelsElement from "../../Components/Label/Labels";
|
||||
import LabelsElement from "Common/UI/Components/Label/Labels";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import PageComponentProps from "../PageComponentProps";
|
||||
import URL from "Common/Types/API/URL";
|
||||
|
||||
Reference in New Issue
Block a user