address feedback suggestions

This commit is contained in:
Scott Wilson
2024-09-11 12:40:27 -07:00
parent 16182a9d1d
commit ce9b66ef14
16 changed files with 147 additions and 112 deletions

View File

@@ -367,8 +367,8 @@ export const ORGANIZATIONS = {
offset: "The offset to start from. If you enter 10, it will start from the 10th identity membership.",
limit: "The number of identity memberships to return.",
orderBy: "The column to order identity memberships by.",
direction: "The direction identity memberships will be sorted in.",
textFilter: "The text string that identity membership names will be filtered by."
orderDirection: "The direction identity memberships will be sorted in.",
search: "The text string that identity membership names will be filtered by."
},
GET_PROJECTS: {
organizationId: "The ID of the organization to get projects from."
@@ -479,8 +479,8 @@ export const PROJECT_IDENTITIES = {
offset: "The offset to start from. If you enter 10, it will start from the 10th identity membership.",
limit: "The number of identity memberships to return.",
orderBy: "The column to order identity memberships by.",
direction: "The direction identity memberships will be sorted in.",
textFilter: "The text string that identity membership names will be filtered by."
orderDirection: "The direction identity memberships will be sorted in.",
search: "The text string that identity membership names will be filtered by."
},
GET_IDENTITY_MEMBERSHIP_BY_ID: {
identityId: "The ID of the identity to get the membership for.",

View File

@@ -40,17 +40,12 @@ export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => {
.default(OrgIdentityOrderBy.Name)
.describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.orderBy)
.optional(),
direction: z
orderDirection: z
.nativeEnum(OrderByDirection)
.default(OrderByDirection.ASC)
.describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.direction)
.describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.orderDirection)
.optional(),
textFilter: z
.string()
.trim()
.default("")
.describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.textFilter)
.optional()
search: z.string().trim().describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.search).optional()
}),
response: {
200: z.object({
@@ -80,8 +75,8 @@ export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => {
limit: req.query.limit,
offset: req.query.offset,
orderBy: req.query.orderBy,
direction: req.query.direction,
textFilter: req.query.textFilter
orderDirection: req.query.orderDirection,
search: req.query.search
});
return { identityMemberships, totalCount };

View File

@@ -235,17 +235,12 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
.default(ProjectIdentityOrderBy.Name)
.describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.orderBy)
.optional(),
direction: z
orderDirection: z
.nativeEnum(OrderByDirection)
.default(OrderByDirection.ASC)
.describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.direction)
.describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.orderDirection)
.optional(),
textFilter: z
.string()
.trim()
.default("")
.describe(PROJECT_IDENTITIES.LIST_IDENTITY_MEMBERSHIPS.textFilter)
.optional()
search: z.string().trim().describe(PROJECT_IDENTITIES.LIST_IDENTITY_MEMBERSHIPS.search).optional()
}),
response: {
200: z.object({
@@ -287,8 +282,8 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
limit: req.query.limit,
offset: req.query.offset,
orderBy: req.query.orderBy,
direction: req.query.direction,
textFilter: req.query.textFilter
orderDirection: req.query.orderDirection,
search: req.query.search
});
return { identityMemberships, totalCount };
}

View File

@@ -112,7 +112,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
projectId: string,
filter: { identityId?: string } & Pick<
TListProjectIdentityDTO,
"limit" | "offset" | "textFilter" | "orderBy" | "direction"
"limit" | "offset" | "search" | "orderBy" | "orderDirection"
> = {},
tx?: Knex
) => {
@@ -126,8 +126,8 @@ export const identityProjectDALFactory = (db: TDbClient) => {
void qb.where("identityId", filter.identityId);
}
if (filter.textFilter) {
void qb.whereILike(`${TableName.Identity}.name`, `%${filter.textFilter}%`);
if (filter.search) {
void qb.whereILike(`${TableName.Identity}.name`, `%${filter.search}%`);
}
})
.join(
@@ -173,7 +173,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
if (filter.orderBy) {
switch (filter.orderBy) {
case "name":
void query.orderBy(`${TableName.Identity}.${filter.orderBy}`, filter.direction);
void query.orderBy(`${TableName.Identity}.${filter.orderBy}`, filter.orderDirection);
break;
default:
// do nothing
@@ -238,7 +238,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
const getCountByProjectId = async (
projectId: string,
filter: { identityId?: string } & Pick<TListProjectIdentityDTO, "textFilter"> = {},
filter: { identityId?: string } & Pick<TListProjectIdentityDTO, "search"> = {},
tx?: Knex
) => {
try {
@@ -251,12 +251,13 @@ export const identityProjectDALFactory = (db: TDbClient) => {
void qb.where("identityId", filter.identityId);
}
if (filter.textFilter) {
void qb.whereILike(`${TableName.Identity}.name`, `%${filter.textFilter}%`);
if (filter.search) {
void qb.whereILike(`${TableName.Identity}.name`, `%${filter.search}%`);
}
});
})
.count();
return identities.length;
return Number(identities[0].count);
} catch (error) {
throw new DatabaseError({ error, name: "GetCountByProjectId" });
}

View File

@@ -272,8 +272,8 @@ export const identityProjectServiceFactory = ({
limit,
offset,
orderBy,
direction,
textFilter
orderDirection,
search
}: TListProjectIdentityDTO) => {
const { permission } = await permissionService.getProjectPermission(
actor,
@@ -288,11 +288,11 @@ export const identityProjectServiceFactory = ({
limit,
offset,
orderBy,
direction,
textFilter
orderDirection,
search
});
const totalCount = await identityProjectDAL.getCountByProjectId(projectId, { textFilter });
const totalCount = await identityProjectDAL.getCountByProjectId(projectId, { search });
return { identityMemberships, totalCount };
};

View File

@@ -44,8 +44,8 @@ export type TListProjectIdentityDTO = {
limit?: number;
offset?: number;
orderBy?: ProjectIdentityOrderBy;
direction?: OrderByDirection;
textFilter?: string;
orderDirection?: OrderByDirection;
search?: string;
} & TProjectPermission;
export type TGetProjectIdentityByIdentityIdDTO = {

View File

@@ -34,11 +34,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
limit,
offset = 0,
orderBy,
direction = OrderByDirection.ASC,
textFilter,
orderDirection = OrderByDirection.ASC,
search,
...filter
}: Partial<TIdentityOrgMemberships> &
Pick<TListOrgIdentitiesByOrgIdDTO, "offset" | "limit" | "orderBy" | "direction" | "textFilter">,
Pick<TListOrgIdentitiesByOrgIdDTO, "offset" | "limit" | "orderBy" | "orderDirection" | "search">,
tx?: Knex
) => {
try {
@@ -65,18 +65,18 @@ export const identityOrgDALFactory = (db: TDbClient) => {
if (orderBy) {
switch (orderBy) {
case "name":
void query.orderBy(`${TableName.Identity}.${orderBy}`, direction);
void query.orderBy(`${TableName.Identity}.${orderBy}`, orderDirection);
break;
case "role":
void query.orderBy(`${TableName.IdentityOrgMembership}.${orderBy}`, direction);
void query.orderBy(`${TableName.IdentityOrgMembership}.${orderBy}`, orderDirection);
break;
default:
// do nothing
}
}
if (textFilter?.length) {
void query.whereILike(`${TableName.Identity}.name`, `%${textFilter}%`);
if (search?.length) {
void query.whereILike(`${TableName.Identity}.name`, `%${search}%`);
}
const docs = await query;
@@ -117,21 +117,22 @@ export const identityOrgDALFactory = (db: TDbClient) => {
};
const countAllOrgIdentities = async (
{ textFilter, ...filter }: Partial<TIdentityOrgMemberships> & Pick<TListOrgIdentitiesByOrgIdDTO, "textFilter">,
{ search, ...filter }: Partial<TIdentityOrgMemberships> & Pick<TListOrgIdentitiesByOrgIdDTO, "search">,
tx?: Knex
) => {
try {
const query = (tx || db.replicaNode())(TableName.IdentityOrgMembership)
.where(filter)
.join(TableName.Identity, `${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`);
.join(TableName.Identity, `${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`)
.count();
if (textFilter?.length) {
void query.whereILike(`${TableName.Identity}.name`, `%${textFilter}%`);
if (search?.length) {
void query.whereILike(`${TableName.Identity}.name`, `%${search}%`);
}
const identities = await query;
return identities.length;
return Number(identities[0].count);
} catch (error) {
throw new DatabaseError({ error, name: "countAllOrgIdentities" });
}

View File

@@ -204,8 +204,8 @@ export const identityServiceFactory = ({
limit,
offset,
orderBy,
direction,
textFilter
orderDirection,
search
}: TListOrgIdentitiesByOrgIdDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
@@ -215,13 +215,13 @@ export const identityServiceFactory = ({
limit,
offset,
orderBy,
direction,
textFilter
orderDirection,
search
});
const totalCount = await identityOrgMembershipDAL.countAllOrgIdentities({
[`${TableName.IdentityOrgMembership}.orgId` as "orgId"]: orgId,
textFilter
search
});
return { identityMemberships, totalCount };

View File

@@ -34,8 +34,8 @@ export type TListOrgIdentitiesByOrgIdDTO = {
limit?: number;
offset?: number;
orderBy?: OrgIdentityOrderBy;
direction?: OrderByDirection;
textFilter?: string;
orderDirection?: OrderByDirection;
search?: string;
} & TOrgPermission;
export enum OrgIdentityOrderBy {

View File

@@ -0,0 +1,4 @@
export enum OrderByDirection {
ASC = "asc",
DESC = "desc"
}

View File

@@ -1,6 +1,7 @@
import { useMutation, useQuery, useQueryClient, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { TGroupOrgMembership } from "../groups/types";
import {
@@ -8,6 +9,7 @@ import {
Invoice,
License,
Organization,
OrgIdentityOrderBy,
OrgPlanTable,
PlanBillingInfo,
PmtMethod,
@@ -372,9 +374,9 @@ export const useGetIdentityMembershipOrgs = (
organizationId,
offset = 0,
limit = 100,
orderBy = "name",
direction = "asc",
textFilter = ""
orderBy = OrgIdentityOrderBy.Name,
orderDirection = OrderByDirection.ASC,
search = ""
}: TListOrgIdentitiesDTO,
options?: Omit<
UseQueryOptions<
@@ -390,8 +392,8 @@ export const useGetIdentityMembershipOrgs = (
offset: String(offset),
limit: String(limit),
orderBy: String(orderBy),
direction: String(direction),
textFilter: String(textFilter)
orderDirection: String(orderDirection),
search: String(search)
});
return useQuery({
queryKey: organizationKeys.getOrgIdentityMembershipsWithParams({
@@ -399,8 +401,8 @@ export const useGetIdentityMembershipOrgs = (
offset,
limit,
orderBy,
direction,
textFilter
orderDirection,
search
}),
queryFn: async () => {
const { data } = await apiRequest.get<TOrgIdentitiesList>(

View File

@@ -1,3 +1,4 @@
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { IdentityMembershipOrg } from "@app/hooks/api/identities/types";
export type Organization = {
@@ -109,12 +110,17 @@ export type TListOrgIdentitiesDTO = {
organizationId: string;
offset?: number;
limit?: number;
orderBy?: string;
direction?: string;
textFilter?: string;
orderBy?: OrgIdentityOrderBy;
orderDirection?: OrderByDirection;
search?: string;
};
export type TOrgIdentitiesList = {
identityMemberships: IdentityMembershipOrg[];
totalCount: number;
};
export enum OrgIdentityOrderBy {
Name = "name",
Role = "role"
}

View File

@@ -1,6 +1,7 @@
import { useMutation, useQuery, useQueryClient, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { CaStatus } from "../ca/enums";
import { TCertificateAuthority } from "../ca/types";
@@ -23,6 +24,7 @@ import {
DeleteEnvironmentDTO,
DeleteWorkspaceDTO,
NameWorkspaceSecretsDTO,
ProjectIdentityOrderBy,
RenameWorkspaceDTO,
TGetUpgradeProjectStatusDTO,
TListProjectIdentitiesDTO,
@@ -538,9 +540,9 @@ export const useGetWorkspaceIdentityMemberships = (
workspaceId,
offset = 0,
limit = 100,
orderBy = "name",
direction = "asc",
textFilter = ""
orderBy = ProjectIdentityOrderBy.Name,
orderDirection = OrderByDirection.ASC,
search = ""
}: TListProjectIdentitiesDTO,
options?: Omit<
UseQueryOptions<
@@ -558,16 +560,16 @@ export const useGetWorkspaceIdentityMemberships = (
offset,
limit,
orderBy,
direction,
textFilter
orderDirection,
search
}),
queryFn: async () => {
const params = new URLSearchParams({
offset: String(offset),
limit: String(limit),
orderBy: String(orderBy),
direction: String(direction),
textFilter: String(textFilter)
orderDirection: String(orderDirection),
search: String(search)
});
const { data } = await apiRequest.get<TProjectIdentitiesList>(

View File

@@ -1,3 +1,5 @@
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { TProjectRole } from "../roles/types";
export enum ProjectVersion {
@@ -146,7 +148,11 @@ export type TListProjectIdentitiesDTO = {
workspaceId: string;
offset?: number;
limit?: number;
orderBy?: string;
direction?: string;
textFilter?: string;
orderBy?: ProjectIdentityOrderBy;
orderDirection?: OrderByDirection;
search?: string;
};
export enum ProjectIdentityOrderBy {
Name = "name"
}

View File

@@ -36,6 +36,8 @@ import {
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useDebounce } from "@app/hooks";
import { useGetIdentityMembershipOrgs, useGetOrgRoles, useUpdateIdentity } from "@app/hooks/api";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { OrgIdentityOrderBy } from "@app/hooks/api/organization/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
@@ -55,10 +57,10 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
const { currentOrg } = useOrganization();
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(INIT_PER_PAGE);
const [direction, setDirection] = useState("asc");
const [orderBy, setOrderBy] = useState("name");
const [textFilter, setTextFilter] = useState("");
const debouncedTextFilter = useDebounce(textFilter);
const [orderDirection, setOrderDirection] = useState(OrderByDirection.ASC);
const [orderBy, setOrderBy] = useState(OrgIdentityOrderBy.Name);
const [search, setSearch] = useState("");
const debouncedSearch = useDebounce(search);
const organizationId = currentOrg?.id || "";
@@ -70,9 +72,9 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
organizationId,
offset,
limit: perPage,
direction,
orderDirection,
orderBy,
textFilter: debouncedTextFilter
search: debouncedSearch
},
{ keepPreviousData: true }
);
@@ -84,14 +86,16 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
const { data: roles } = useGetOrgRoles(organizationId);
const handleSort = (column: string) => {
const handleSort = (column: OrgIdentityOrderBy) => {
if (column === orderBy) {
setDirection((prev) => (prev === "asc" ? "desc" : "asc"));
setOrderDirection((prev) =>
prev === OrderByDirection.ASC ? OrderByDirection.DESC : OrderByDirection.ASC
);
return;
}
setOrderBy(column);
setDirection("asc");
setOrderDirection(OrderByDirection.ASC);
};
const handleChangeRole = async ({ identityId, role }: { identityId: string; role: string }) => {
@@ -122,8 +126,8 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
<div>
<Input
containerClassName="mb-4"
value={textFilter}
onChange={(e) => setTextFilter(e.target.value)}
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search identities by name..."
/>
@@ -136,12 +140,17 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
Name
<IconButton
variant="plain"
className={`ml-2 ${orderBy === "name" ? "" : "opacity-30"}`}
className={`ml-2 ${orderBy === OrgIdentityOrderBy.Name ? "" : "opacity-30"}`}
ariaLabel="sort"
onClick={() => handleSort("name")}
onClick={() => handleSort(OrgIdentityOrderBy.Name)}
>
<FontAwesomeIcon
icon={direction === "desc" && orderBy === "name" ? faArrowUp : faArrowDown}
icon={
orderDirection === OrderByDirection.DESC &&
orderBy === OrgIdentityOrderBy.Name
? faArrowUp
: faArrowDown
}
/>
</IconButton>
</div>
@@ -151,12 +160,17 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
Role
<IconButton
variant="plain"
className={`ml-2 ${orderBy === "role" ? "" : "opacity-30"}`}
className={`ml-2 ${orderBy === OrgIdentityOrderBy.Role ? "" : "opacity-30"}`}
ariaLabel="sort"
onClick={() => handleSort("role")}
onClick={() => handleSort(OrgIdentityOrderBy.Role)}
>
<FontAwesomeIcon
icon={direction === "desc" && orderBy === "role" ? faArrowUp : faArrowDown}
icon={
orderDirection === OrderByDirection.DESC &&
orderBy === OrgIdentityOrderBy.Role
? faArrowUp
: faArrowDown
}
/>
</IconButton>
</div>
@@ -275,7 +289,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
{!isLoading && data && data?.identityMemberships.length === 0 && (
<EmptyState
title={
debouncedTextFilter.trim().length > 0
debouncedSearch.trim().length > 0
? "No identities match search filter"
: "No identities have been created in this organization"
}

View File

@@ -46,8 +46,10 @@ import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@a
import { withProjectPermission } from "@app/hoc";
import { useDebounce } from "@app/hooks";
import { useDeleteIdentityFromWorkspace, useGetWorkspaceIdentityMemberships } from "@app/hooks/api";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { IdentityMembership } from "@app/hooks/api/identities/types";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { ProjectIdentityOrderBy } from "@app/hooks/api/workspace/types";
import { usePopUp } from "@app/hooks/usePopUp";
import { IdentityModal } from "./components/IdentityModal";
@@ -67,10 +69,10 @@ export const IdentityTab = withProjectPermission(
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(INIT_PER_PAGE);
const [direction, setDirection] = useState("asc");
const [orderBy, setOrderBy] = useState("name");
const [textFilter, setTextFilter] = useState("");
const debouncedTextFilter = useDebounce(textFilter);
const [orderDirection, setOrderDirection] = useState(OrderByDirection.ASC);
const [orderBy, setOrderBy] = useState(ProjectIdentityOrderBy.Name);
const [search, setSearch] = useState("");
const debouncedSearch = useDebounce(search);
const workspaceId = currentWorkspace?.id ?? "";
@@ -80,9 +82,9 @@ export const IdentityTab = withProjectPermission(
workspaceId: currentWorkspace?.id || "",
offset,
limit: perPage,
direction,
orderDirection,
orderBy,
textFilter: debouncedTextFilter
search: debouncedSearch
},
{ keepPreviousData: true }
);
@@ -125,14 +127,16 @@ export const IdentityTab = withProjectPermission(
if (data && data.totalCount < offset) setPage(1);
}, [data?.totalCount]);
const handleSort = (column: string) => {
const handleSort = (column: ProjectIdentityOrderBy) => {
if (column === orderBy) {
setDirection((prev) => (prev === "asc" ? "desc" : "asc"));
setOrderDirection((prev) =>
prev === OrderByDirection.ASC ? OrderByDirection.DESC : OrderByDirection.ASC
);
return;
}
setOrderBy(column);
setDirection("asc");
setOrderDirection(OrderByDirection.ASC);
};
return (
@@ -176,8 +180,8 @@ export const IdentityTab = withProjectPermission(
</div>
<Input
containerClassName="mb-4"
value={textFilter}
onChange={(e) => setTextFilter(e.target.value)}
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search identities by name..."
/>
@@ -190,13 +194,18 @@ export const IdentityTab = withProjectPermission(
Name
<IconButton
variant="plain"
className={`ml-2 ${orderBy === "name" ? "" : "opacity-30"}`}
className={`ml-2 ${
orderBy === ProjectIdentityOrderBy.Name ? "" : "opacity-30"
}`}
ariaLabel="sort"
onClick={() => handleSort("name")}
onClick={() => handleSort(ProjectIdentityOrderBy.Name)}
>
<FontAwesomeIcon
icon={
direction === "desc" && orderBy === "name" ? faArrowUp : faArrowDown
orderDirection === OrderByDirection.DESC &&
orderBy === ProjectIdentityOrderBy.Name
? faArrowUp
: faArrowDown
}
/>
</IconButton>
@@ -372,7 +381,7 @@ export const IdentityTab = withProjectPermission(
{!isLoading && data && data?.identityMemberships.length === 0 && (
<EmptyState
title={
debouncedTextFilter.trim().length > 0
debouncedSearch.trim().length > 0
? "No identities match search filter"
: "No identities have been added to this project"
}