mirror of
https://github.com/Infisical/infisical.git
synced 2025-08-24 20:43:19 +00:00
Compare commits
20 Commits
policy-del
...
misc/add-i
Author | SHA1 | Date | |
---|---|---|---|
|
2f29a513cc | ||
|
978a3e5828 | ||
|
27bf91e58f | ||
|
f2c3c76c60 | ||
|
85023916e4 | ||
|
02afd6a8e7 | ||
|
929eac4350 | ||
|
c6074dd69a | ||
|
a9b26755ba | ||
|
033e5d3f81 | ||
|
90634e1913 | ||
|
3c8ec7d7fb | ||
|
26a59286c5 | ||
|
392792bb1e | ||
|
48f40ff938 | ||
|
969896e431 | ||
|
fd85da5739 | ||
|
2caf6ff94b | ||
|
ed7d709a70 | ||
|
953cc3a850 |
@@ -0,0 +1,29 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.raw(`
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS ${TableName.IdentityAccessToken}_identityid_index
|
||||
ON ${TableName.IdentityAccessToken} ("identityId")
|
||||
`);
|
||||
|
||||
await knex.raw(`
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS ${TableName.IdentityAccessToken}_identityuaclientsecretid_index
|
||||
ON ${TableName.IdentityAccessToken} ("identityUAClientSecretId")
|
||||
`);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.raw(`
|
||||
DROP INDEX IF EXISTS ${TableName.IdentityAccessToken}_identityid_index
|
||||
`);
|
||||
|
||||
await knex.raw(`
|
||||
DROP INDEX IF EXISTS ${TableName.IdentityAccessToken}_identityuaclientsecretid_index
|
||||
`);
|
||||
}
|
||||
|
||||
const config = { transaction: false };
|
||||
|
||||
export { config };
|
@@ -131,7 +131,6 @@ export const auditLogQueueServiceFactory = async ({
|
||||
});
|
||||
|
||||
try {
|
||||
logger.info(`Streaming audit log [url=${url}] for org [orgId=${orgId}]`);
|
||||
const response = await request.post(
|
||||
url,
|
||||
{ ...providerSpecificPayload(url), ...auditLog },
|
||||
@@ -143,9 +142,6 @@ export const auditLogQueueServiceFactory = async ({
|
||||
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
|
||||
}
|
||||
);
|
||||
logger.info(
|
||||
`Successfully streamed audit log [url=${url}] for org [orgId=${orgId}] [response=${JSON.stringify(response.data)}]`
|
||||
);
|
||||
return response;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
@@ -237,7 +233,6 @@ export const auditLogQueueServiceFactory = async ({
|
||||
});
|
||||
|
||||
try {
|
||||
logger.info(`Streaming audit log [url=${url}] for org [orgId=${orgId}]`);
|
||||
const response = await request.post(
|
||||
url,
|
||||
{ ...providerSpecificPayload(url), ...auditLog },
|
||||
@@ -249,9 +244,6 @@ export const auditLogQueueServiceFactory = async ({
|
||||
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
|
||||
}
|
||||
);
|
||||
logger.info(
|
||||
`Successfully streamed audit log [url=${url}] for org [orgId=${orgId}] [response=${JSON.stringify(response.data)}]`
|
||||
);
|
||||
return response;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
|
@@ -1419,7 +1419,8 @@ export const registerRoutes = async (
|
||||
const identityAccessTokenService = identityAccessTokenServiceFactory({
|
||||
identityAccessTokenDAL,
|
||||
identityOrgMembershipDAL,
|
||||
accessTokenQueue
|
||||
accessTokenQueue,
|
||||
identityDAL
|
||||
});
|
||||
|
||||
const identityProjectService = identityProjectServiceFactory({
|
||||
|
@@ -17,77 +17,11 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
|
||||
const doc = await (tx || db.replicaNode())(TableName.IdentityAccessToken)
|
||||
.where(filter)
|
||||
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityAccessToken}.identityId`)
|
||||
.leftJoin(
|
||||
TableName.IdentityUaClientSecret,
|
||||
`${TableName.IdentityAccessToken}.identityUAClientSecretId`,
|
||||
`${TableName.IdentityUaClientSecret}.id`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.IdentityUniversalAuth,
|
||||
`${TableName.IdentityUaClientSecret}.identityUAId`,
|
||||
`${TableName.IdentityUniversalAuth}.id`
|
||||
)
|
||||
.leftJoin(TableName.IdentityGcpAuth, `${TableName.Identity}.id`, `${TableName.IdentityGcpAuth}.identityId`)
|
||||
.leftJoin(
|
||||
TableName.IdentityAliCloudAuth,
|
||||
`${TableName.Identity}.id`,
|
||||
`${TableName.IdentityAliCloudAuth}.identityId`
|
||||
)
|
||||
.leftJoin(TableName.IdentityAwsAuth, `${TableName.Identity}.id`, `${TableName.IdentityAwsAuth}.identityId`)
|
||||
.leftJoin(TableName.IdentityAzureAuth, `${TableName.Identity}.id`, `${TableName.IdentityAzureAuth}.identityId`)
|
||||
.leftJoin(TableName.IdentityLdapAuth, `${TableName.Identity}.id`, `${TableName.IdentityLdapAuth}.identityId`)
|
||||
.leftJoin(
|
||||
TableName.IdentityKubernetesAuth,
|
||||
`${TableName.Identity}.id`,
|
||||
`${TableName.IdentityKubernetesAuth}.identityId`
|
||||
)
|
||||
.leftJoin(TableName.IdentityOciAuth, `${TableName.Identity}.id`, `${TableName.IdentityOciAuth}.identityId`)
|
||||
.leftJoin(TableName.IdentityOidcAuth, `${TableName.Identity}.id`, `${TableName.IdentityOidcAuth}.identityId`)
|
||||
.leftJoin(TableName.IdentityTokenAuth, `${TableName.Identity}.id`, `${TableName.IdentityTokenAuth}.identityId`)
|
||||
.leftJoin(TableName.IdentityJwtAuth, `${TableName.Identity}.id`, `${TableName.IdentityJwtAuth}.identityId`)
|
||||
.leftJoin(
|
||||
TableName.IdentityTlsCertAuth,
|
||||
`${TableName.Identity}.id`,
|
||||
`${TableName.IdentityTlsCertAuth}.identityId`
|
||||
)
|
||||
.select(selectAllTableCols(TableName.IdentityAccessToken))
|
||||
.select(
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityUniversalAuth).as("accessTokenTrustedIpsUa"),
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityGcpAuth).as("accessTokenTrustedIpsGcp"),
|
||||
db
|
||||
.ref("accessTokenTrustedIps")
|
||||
.withSchema(TableName.IdentityAliCloudAuth)
|
||||
.as("accessTokenTrustedIpsAliCloud"),
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityAwsAuth).as("accessTokenTrustedIpsAws"),
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityAzureAuth).as("accessTokenTrustedIpsAzure"),
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityKubernetesAuth).as("accessTokenTrustedIpsK8s"),
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityOciAuth).as("accessTokenTrustedIpsOci"),
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityOidcAuth).as("accessTokenTrustedIpsOidc"),
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityTokenAuth).as("accessTokenTrustedIpsToken"),
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityJwtAuth).as("accessTokenTrustedIpsJwt"),
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityLdapAuth).as("accessTokenTrustedIpsLdap"),
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityTlsCertAuth).as("accessTokenTrustedIpsTlsCert"),
|
||||
db.ref("name").withSchema(TableName.Identity)
|
||||
)
|
||||
.select(db.ref("name").withSchema(TableName.Identity))
|
||||
.first();
|
||||
|
||||
if (!doc) return;
|
||||
|
||||
return {
|
||||
...doc,
|
||||
trustedIpsUniversalAuth: doc.accessTokenTrustedIpsUa,
|
||||
trustedIpsGcpAuth: doc.accessTokenTrustedIpsGcp,
|
||||
trustedIpsAliCloudAuth: doc.accessTokenTrustedIpsAliCloud,
|
||||
trustedIpsAwsAuth: doc.accessTokenTrustedIpsAws,
|
||||
trustedIpsAzureAuth: doc.accessTokenTrustedIpsAzure,
|
||||
trustedIpsKubernetesAuth: doc.accessTokenTrustedIpsK8s,
|
||||
trustedIpsOciAuth: doc.accessTokenTrustedIpsOci,
|
||||
trustedIpsOidcAuth: doc.accessTokenTrustedIpsOidc,
|
||||
trustedIpsAccessTokenAuth: doc.accessTokenTrustedIpsToken,
|
||||
trustedIpsAccessJwtAuth: doc.accessTokenTrustedIpsJwt,
|
||||
trustedIpsAccessLdapAuth: doc.accessTokenTrustedIpsLdap,
|
||||
trustedIpsAccessTlsCertAuth: doc.accessTokenTrustedIpsTlsCert
|
||||
};
|
||||
return doc;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "IdAccessTokenFindOne" });
|
||||
}
|
||||
|
@@ -7,12 +7,14 @@ import { checkIPAgainstBlocklist, TIp } from "@app/lib/ip";
|
||||
|
||||
import { TAccessTokenQueueServiceFactory } from "../access-token-queue/access-token-queue";
|
||||
import { AuthTokenType } from "../auth/auth-type";
|
||||
import { TIdentityDALFactory } from "../identity/identity-dal";
|
||||
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||
import { TIdentityAccessTokenDALFactory } from "./identity-access-token-dal";
|
||||
import { TIdentityAccessTokenJwtPayload, TRenewAccessTokenDTO } from "./identity-access-token-types";
|
||||
|
||||
type TIdentityAccessTokenServiceFactoryDep = {
|
||||
identityAccessTokenDAL: TIdentityAccessTokenDALFactory;
|
||||
identityDAL: Pick<TIdentityDALFactory, "getTrustedIpsByAuthMethod">;
|
||||
identityOrgMembershipDAL: TIdentityOrgDALFactory;
|
||||
accessTokenQueue: Pick<
|
||||
TAccessTokenQueueServiceFactory,
|
||||
@@ -25,7 +27,8 @@ export type TIdentityAccessTokenServiceFactory = ReturnType<typeof identityAcces
|
||||
export const identityAccessTokenServiceFactory = ({
|
||||
identityAccessTokenDAL,
|
||||
identityOrgMembershipDAL,
|
||||
accessTokenQueue
|
||||
accessTokenQueue,
|
||||
identityDAL
|
||||
}: TIdentityAccessTokenServiceFactoryDep) => {
|
||||
const validateAccessTokenExp = async (identityAccessToken: TIdentityAccessTokens) => {
|
||||
const {
|
||||
@@ -190,24 +193,11 @@ export const identityAccessTokenServiceFactory = ({
|
||||
message: "Failed to authorize revoked access token, access token is revoked"
|
||||
});
|
||||
|
||||
const trustedIpsMap: Record<IdentityAuthMethod, unknown> = {
|
||||
[IdentityAuthMethod.UNIVERSAL_AUTH]: identityAccessToken.trustedIpsUniversalAuth,
|
||||
[IdentityAuthMethod.GCP_AUTH]: identityAccessToken.trustedIpsGcpAuth,
|
||||
[IdentityAuthMethod.ALICLOUD_AUTH]: identityAccessToken.trustedIpsAliCloudAuth,
|
||||
[IdentityAuthMethod.AWS_AUTH]: identityAccessToken.trustedIpsAwsAuth,
|
||||
[IdentityAuthMethod.OCI_AUTH]: identityAccessToken.trustedIpsOciAuth,
|
||||
[IdentityAuthMethod.AZURE_AUTH]: identityAccessToken.trustedIpsAzureAuth,
|
||||
[IdentityAuthMethod.KUBERNETES_AUTH]: identityAccessToken.trustedIpsKubernetesAuth,
|
||||
[IdentityAuthMethod.OIDC_AUTH]: identityAccessToken.trustedIpsOidcAuth,
|
||||
[IdentityAuthMethod.TOKEN_AUTH]: identityAccessToken.trustedIpsAccessTokenAuth,
|
||||
[IdentityAuthMethod.JWT_AUTH]: identityAccessToken.trustedIpsAccessJwtAuth,
|
||||
[IdentityAuthMethod.LDAP_AUTH]: identityAccessToken.trustedIpsAccessLdapAuth,
|
||||
[IdentityAuthMethod.TLS_CERT_AUTH]: identityAccessToken.trustedIpsAccessTlsCertAuth
|
||||
};
|
||||
|
||||
const trustedIps = trustedIpsMap[identityAccessToken.authMethod as IdentityAuthMethod];
|
||||
|
||||
if (ipAddress) {
|
||||
const trustedIps = await identityDAL.getTrustedIpsByAuthMethod(
|
||||
identityAccessToken.identityId,
|
||||
identityAccessToken.authMethod as IdentityAuthMethod
|
||||
);
|
||||
if (ipAddress && trustedIps) {
|
||||
checkIPAgainstBlocklist({
|
||||
ipAddress,
|
||||
trustedIps: trustedIps as TIp[]
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TIdentities } from "@app/db/schemas";
|
||||
import { IdentityAuthMethod, TableName, TIdentities } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
@@ -8,6 +8,28 @@ export type TIdentityDALFactory = ReturnType<typeof identityDALFactory>;
|
||||
export const identityDALFactory = (db: TDbClient) => {
|
||||
const identityOrm = ormify(db, TableName.Identity);
|
||||
|
||||
const getTrustedIpsByAuthMethod = async (identityId: string, authMethod: IdentityAuthMethod) => {
|
||||
const authMethodToTableName = {
|
||||
[IdentityAuthMethod.TOKEN_AUTH]: TableName.IdentityTokenAuth,
|
||||
[IdentityAuthMethod.UNIVERSAL_AUTH]: TableName.IdentityUniversalAuth,
|
||||
[IdentityAuthMethod.KUBERNETES_AUTH]: TableName.IdentityKubernetesAuth,
|
||||
[IdentityAuthMethod.GCP_AUTH]: TableName.IdentityGcpAuth,
|
||||
[IdentityAuthMethod.ALICLOUD_AUTH]: TableName.IdentityAliCloudAuth,
|
||||
[IdentityAuthMethod.AWS_AUTH]: TableName.IdentityAwsAuth,
|
||||
[IdentityAuthMethod.AZURE_AUTH]: TableName.IdentityAzureAuth,
|
||||
[IdentityAuthMethod.TLS_CERT_AUTH]: TableName.IdentityTlsCertAuth,
|
||||
[IdentityAuthMethod.OCI_AUTH]: TableName.IdentityOciAuth,
|
||||
[IdentityAuthMethod.OIDC_AUTH]: TableName.IdentityOidcAuth,
|
||||
[IdentityAuthMethod.JWT_AUTH]: TableName.IdentityJwtAuth,
|
||||
[IdentityAuthMethod.LDAP_AUTH]: TableName.IdentityLdapAuth
|
||||
} as const;
|
||||
const tableName = authMethodToTableName[authMethod];
|
||||
if (!tableName) return;
|
||||
const data = await db(tableName).where({ identityId }).first();
|
||||
if (!data) return;
|
||||
return data.accessTokenTrustedIps;
|
||||
};
|
||||
|
||||
const getIdentitiesByFilter = async ({
|
||||
limit,
|
||||
offset,
|
||||
@@ -38,5 +60,5 @@ export const identityDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
return { ...identityOrm, getIdentitiesByFilter };
|
||||
return { ...identityOrm, getTrustedIpsByAuthMethod, getIdentitiesByFilter };
|
||||
};
|
||||
|
@@ -392,7 +392,12 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityOrgMembership}.identityId`)
|
||||
.where(`${TableName.IdentityOrgMembership}.orgId`, orgId)
|
||||
.leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`)
|
||||
.orderBy(`${TableName.Identity}.${orderBy}`, orderDirection)
|
||||
.orderBy(
|
||||
orderBy === OrgIdentityOrderBy.Role
|
||||
? `${TableName.IdentityOrgMembership}.${orderBy}`
|
||||
: `${TableName.Identity}.${orderBy}`,
|
||||
orderDirection
|
||||
)
|
||||
.select(`${TableName.IdentityOrgMembership}.id`)
|
||||
.select<{ id: string; total_count: string }>(
|
||||
db.raw(
|
||||
@@ -523,6 +528,23 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
|
||||
if (orderBy === OrgIdentityOrderBy.Name) {
|
||||
void query.orderBy("identityName", orderDirection);
|
||||
} else if (orderBy === OrgIdentityOrderBy.Role) {
|
||||
void query.orderByRaw(
|
||||
`
|
||||
CASE
|
||||
WHEN ??.role = ?
|
||||
THEN ??.slug
|
||||
ELSE ??.role
|
||||
END ?
|
||||
`,
|
||||
[
|
||||
TableName.IdentityOrgMembership,
|
||||
"custom",
|
||||
TableName.OrgRoles,
|
||||
TableName.IdentityOrgMembership,
|
||||
db.raw(orderDirection)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
const docs = await query;
|
||||
|
@@ -46,8 +46,8 @@ export type TListOrgIdentitiesByOrgIdDTO = {
|
||||
} & TOrgPermission;
|
||||
|
||||
export enum OrgIdentityOrderBy {
|
||||
Name = "name"
|
||||
// Role = "role"
|
||||
Name = "name",
|
||||
Role = "role"
|
||||
}
|
||||
|
||||
export type TSearchOrgIdentitiesByOrgIdDAL = {
|
||||
|
@@ -11,9 +11,9 @@ Fairly frequently, you might run into situations when you need to spend company
|
||||
|
||||
As a perk of working at Infisical, we cover some of your meal expenses.
|
||||
|
||||
HQ team members: meals and unlimited snacks are provided on-site at no cost.
|
||||
**HQ team members**: meals and unlimited snacks are provided **on-site** at no cost.
|
||||
|
||||
Remote team members: a food stipend is allocated based on location.
|
||||
**Remote team members**: a food stipend is allocated based on location.
|
||||
|
||||
# Trivial expenses
|
||||
|
||||
@@ -27,21 +27,28 @@ This means expenses that are:
|
||||
Please spend money in a way that you think is in the best interest of the company.
|
||||
</Note>
|
||||
|
||||
## Saving receipts
|
||||
|
||||
Make sure you keep copies for all receipts. If you expense something on a company card and cannot provide a receipt, this may be deducted from your pay.
|
||||
# Travel
|
||||
|
||||
You should default to using your company card in all cases - it has no transaction fees. If using your personal card is unavoidable, please reach out to Maidul to get it reimbursed manually.
|
||||
If you need to travel on Infisical’s behalf for in-person onboarding, meeting customers, and offsites, again please spend money in the best interests of the company.
|
||||
|
||||
## Training
|
||||
We do not pre-approve your travel expenses, and trust team members to make the right decisions here. Some guidance:
|
||||
|
||||
- Please find a flight ticket that is reasonably priced. We all travel economy by default – we cannot afford for folks to fly premium or business class. Feel free to upgrade using your personal money/airmiles if you’d like to.
|
||||
- Feel free to pay for the Uber/subway/bus to and from the airport with your Brex card.
|
||||
- For business travel, Infisical will cover reasonable expenses for breakfast, lunch, and dinner.
|
||||
- When traveling internationally, Infisical does not cover roaming charges for your phone. You can expense a reasonable eSIM, which usually is no more than $20.
|
||||
|
||||
<Note>
|
||||
Note that this only applies to business travel. It is not applicable for personal travel or day-to-day commuting.
|
||||
</Note>
|
||||
|
||||
For engineers, you’re welcome to take an approved Udemy course. Please reach out to Maidul. For the GTM team, you may buy a book a month if it’s relevant to your work.
|
||||
|
||||
# Equipment
|
||||
|
||||
Infisical is a remote first company so we understand the importance of having a comfortable work setup. To support this, we provide allowances for essential office equipment.
|
||||
|
||||
### Desk & Chair
|
||||
### 1. Desk & Chair
|
||||
|
||||
Most people already have a comfortable desk and chair, but if you need an upgrade, we offer the following allowances.
|
||||
While we're not yet able to provide the latest and greatest, we strive to be reasonable given the stage of our company.
|
||||
@@ -50,10 +57,10 @@ While we're not yet able to provide the latest and greatest, we strive to be rea
|
||||
|
||||
**Chair**: $150 USD
|
||||
|
||||
### Laptop
|
||||
### 2. Laptop
|
||||
Each team member will receive a company-issued Macbook Pro before they start their first day.
|
||||
|
||||
### Notes
|
||||
### 3. Notes
|
||||
|
||||
1. All equipment purchased using company allowances remains the property of Infisical.
|
||||
2. Keep all receipts for equipment purchases and submit them for reimbursement.
|
||||
@@ -65,6 +72,28 @@ This is because we don't yet have a formal HR department to handle such logistic
|
||||
For any equipment related questions, please reach out to Maidul.
|
||||
|
||||
|
||||
## Brex
|
||||
# Brex
|
||||
|
||||
We use Brex as our primary credit card provider. Don't have a company card yet? Reach out to Maidul.
|
||||
|
||||
### Budgets
|
||||
|
||||
You will generally have multiple budgets assigned to you. "General Company Expenses" primarily covers quick SaaS purchases (not food). Remote team members should have a "Lunch Stipend" budget that applies to food.
|
||||
|
||||
If your position involves a lot of travel, you may also have a "Travel" budget that applies to expenses related to business travel (e.g., you can not use it for transportation or food during personal travel).
|
||||
|
||||
### Saving receipts
|
||||
|
||||
Make sure you keep copies for all receipts. If you expense something on a company card and cannot provide a receipt, this may be deducted from your pay.
|
||||
|
||||
You should default to using your company card in all cases - it has no transaction fees. If using your personal card is unavoidable, please reach out to Maidul to get it reimbursed manually.
|
||||
|
||||
### Need a one-off budget increase?
|
||||
|
||||
You can do this directly within Brex - just request the amount and duration for the relevant budget in the app, and your hiring manager will automatically be notified for approval.
|
||||
|
||||
|
||||
# Training
|
||||
|
||||
For engineers, you’re welcome to take an approved Udemy course. Please reach out to Maidul. For the GTM team, you may buy a book a month if it’s relevant to your work.
|
||||
|
||||
|
BIN
docs/favicon.png
BIN
docs/favicon.png
Binary file not shown.
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 6.2 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 4.3 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 4.3 KiB |
@@ -1,4 +1,6 @@
|
||||
import { ReactNode } from "react";
|
||||
import { IconDefinition } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
type Props = {
|
||||
@@ -7,6 +9,7 @@ type Props = {
|
||||
className?: string;
|
||||
labelClassName?: string;
|
||||
truncate?: boolean;
|
||||
icon?: IconDefinition;
|
||||
};
|
||||
|
||||
export const GenericFieldLabel = ({
|
||||
@@ -14,11 +17,15 @@ export const GenericFieldLabel = ({
|
||||
children,
|
||||
className,
|
||||
labelClassName,
|
||||
truncate
|
||||
truncate,
|
||||
icon
|
||||
}: Props) => {
|
||||
return (
|
||||
<div className={twMerge("min-w-0", className)}>
|
||||
<p className={twMerge("text-xs font-medium text-mineshaft-400", labelClassName)}>{label}</p>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{icon && <FontAwesomeIcon icon={icon} className="text-mineshaft-400" size="sm" />}
|
||||
<p className={twMerge("text-xs font-medium text-mineshaft-400", labelClassName)}>{label}</p>
|
||||
</div>
|
||||
{children ? (
|
||||
<p className={twMerge("text-sm text-mineshaft-100", truncate && "truncate")}>{children}</p>
|
||||
) : (
|
||||
|
@@ -64,7 +64,7 @@ export const Pagination = ({
|
||||
<FontAwesomeIcon className="text-xs" icon={faCaretDown} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="min-w-fit">
|
||||
<DropdownMenuContent sideOffset={2} className="min-w-fit">
|
||||
{perPageList.map((perPageOption) => (
|
||||
<DropdownMenuItem
|
||||
key={`pagination-per-page-options-${perPageOption}`}
|
||||
|
@@ -81,7 +81,8 @@ export const useSearchIdentities = (dto: TSearchIdentitiesDTO) => {
|
||||
search
|
||||
});
|
||||
return data;
|
||||
}
|
||||
},
|
||||
placeholderData: (previousData) => previousData
|
||||
});
|
||||
};
|
||||
|
||||
|
@@ -154,6 +154,6 @@ export type TOrgIdentitiesList = {
|
||||
};
|
||||
|
||||
export enum OrgIdentityOrderBy {
|
||||
Name = "name"
|
||||
// Role = "role"
|
||||
Name = "name",
|
||||
Role = "role"
|
||||
}
|
||||
|
@@ -56,12 +56,12 @@ export const OrgGroupsSection = () => {
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex justify-between">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Groups</p>
|
||||
<OrgPermissionCan I={OrgPermissionGroupActions.Create} a={OrgPermissionSubjects.Groups}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handleAddGroupModal()}
|
||||
|
@@ -2,14 +2,17 @@ import { useMemo } from "react";
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faEllipsis,
|
||||
faCopy,
|
||||
faEdit,
|
||||
faEllipsisV,
|
||||
faMagnifyingGlass,
|
||||
faSearch,
|
||||
faTrash,
|
||||
faUserGroup,
|
||||
faUsers
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
@@ -261,7 +264,8 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
|
||||
<Select
|
||||
value={role === "custom" ? (customRole?.slug as string) : role}
|
||||
isDisabled={!isAllowed}
|
||||
className="w-48 bg-mineshaft-600"
|
||||
className="h-8 w-48 bg-mineshaft-700"
|
||||
position="popper"
|
||||
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
||||
onValueChange={(selectedRole) =>
|
||||
handleChangeRole({
|
||||
@@ -282,13 +286,19 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
|
||||
</Td>
|
||||
<Td>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
||||
</div>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
className="w-6"
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<DropdownMenuContent sideOffset={2} align="end">
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={faCopy} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
createNotification({
|
||||
@@ -306,10 +316,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed &&
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
icon={<FontAwesomeIcon icon={faEdit} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("group", {
|
||||
@@ -320,7 +327,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
|
||||
customRole
|
||||
});
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Edit Group
|
||||
</DropdownMenuItem>
|
||||
@@ -332,10 +339,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed &&
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
icon={<FontAwesomeIcon icon={faUserGroup} />}
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: "/organization/groups/$groupId",
|
||||
@@ -344,7 +348,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
|
||||
}
|
||||
})
|
||||
}
|
||||
disabled={!isAllowed}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Manage Members
|
||||
</DropdownMenuItem>
|
||||
@@ -356,11 +360,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
isAllowed
|
||||
? "hover:!bg-red-500 hover:!text-white"
|
||||
: "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("deleteGroup", {
|
||||
@@ -368,7 +368,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
|
||||
name
|
||||
});
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Delete Group
|
||||
</DropdownMenuItem>
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { faArrowUpRightFromSquare, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
@@ -71,20 +71,22 @@ export const IdentitySection = withPermission(
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Identities</p>
|
||||
<div className="flex w-full justify-end pr-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Identities</p>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/identities/overview"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://infisical.com/docs/documentation/platform/identities/overview"
|
||||
className="flex w-max cursor-pointer items-center rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white"
|
||||
>
|
||||
Documentation{" "}
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.06rem] ml-1 text-xs"
|
||||
/>
|
||||
<div className="ml-1 mt-[0.16rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
<span>Docs</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1.5 text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<OrgPermissionCan
|
||||
@@ -93,7 +95,7 @@ export const IdentitySection = withPermission(
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
|
@@ -1,12 +1,15 @@
|
||||
import { useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useCallback, useState } from "react";
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faEllipsis,
|
||||
faCheckCircle,
|
||||
faChevronRight,
|
||||
faEdit,
|
||||
faEllipsisV,
|
||||
faFilter,
|
||||
faMagnifyingGlass,
|
||||
faServer
|
||||
faServer,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
@@ -15,19 +18,18 @@ import { twMerge } from "tailwind-merge";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
DropdownSubMenu,
|
||||
DropdownSubMenuContent,
|
||||
DropdownSubMenuTrigger,
|
||||
EmptyState,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Pagination,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
Select,
|
||||
SelectItem,
|
||||
Spinner,
|
||||
@@ -38,7 +40,6 @@ import {
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { OrgPermissionIdentityActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
@@ -63,6 +64,10 @@ type Props = {
|
||||
) => void;
|
||||
};
|
||||
|
||||
type Filter = {
|
||||
roles: string[];
|
||||
};
|
||||
|
||||
export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
const navigate = useNavigate();
|
||||
const { currentOrg } = useOrganization();
|
||||
@@ -90,7 +95,9 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
setUserTablePreference("identityTable", PreferenceKey.PerPage, newPerPage);
|
||||
};
|
||||
|
||||
const [filteredRoles, setFilteredRoles] = useState<string[]>([]);
|
||||
const [filter, setFilter] = useState<Filter>({
|
||||
roles: []
|
||||
});
|
||||
|
||||
const organizationId = currentOrg?.id || "";
|
||||
|
||||
@@ -103,7 +110,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
orderBy,
|
||||
search: {
|
||||
name: debouncedSearch ? { $contains: debouncedSearch } : undefined,
|
||||
role: filteredRoles?.length ? { $in: filteredRoles } : undefined
|
||||
role: filter.roles?.length ? { $in: filter.roles } : undefined
|
||||
}
|
||||
});
|
||||
|
||||
@@ -113,7 +120,6 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
offset,
|
||||
setPage
|
||||
});
|
||||
const filterForm = useForm<{ roles: string }>();
|
||||
|
||||
const { data: roles } = useGetOrgRoles(organizationId);
|
||||
|
||||
@@ -153,79 +159,80 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRoleToggle = useCallback(
|
||||
(roleSlug: string) =>
|
||||
setFilter((state) => {
|
||||
const currentRoles = state.roles || [];
|
||||
|
||||
if (currentRoles.includes(roleSlug)) {
|
||||
return { ...state, roles: currentRoles.filter((role) => role !== roleSlug) };
|
||||
}
|
||||
return { ...state, roles: [...currentRoles, roleSlug] };
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const isTableFiltered = Boolean(filter.roles.length);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4 flex items-center space-x-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Filter Identities"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className={twMerge(
|
||||
"flex h-[2.375rem] w-[2.6rem] items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
|
||||
isTableFiltered && "border-primary/50 text-primary"
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faFilter} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-0">
|
||||
<DropdownMenuLabel>Filter By</DropdownMenuLabel>
|
||||
<DropdownSubMenu>
|
||||
<DropdownSubMenuTrigger
|
||||
iconPos="right"
|
||||
icon={<FontAwesomeIcon icon={faChevronRight} size="sm" />}
|
||||
>
|
||||
Roles
|
||||
</DropdownSubMenuTrigger>
|
||||
<DropdownSubMenuContent className="thin-scrollbar max-h-[20rem] overflow-y-auto rounded-l-none">
|
||||
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
|
||||
Apply Roles to Filter Identities
|
||||
</DropdownMenuLabel>
|
||||
{roles?.map(({ id, slug, name }) => (
|
||||
<DropdownMenuItem
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault();
|
||||
handleRoleToggle(slug);
|
||||
}}
|
||||
key={id}
|
||||
icon={filter.roles.includes(slug) && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||
iconPos="right"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className="mr-2 h-2 w-2 rounded-full"
|
||||
style={{ background: "#bec2c8" }}
|
||||
/>
|
||||
{name}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownSubMenuContent>
|
||||
</DropdownSubMenu>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search identities by name..."
|
||||
/>
|
||||
<div>
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<IconButton
|
||||
ariaLabel="filter"
|
||||
variant="outline_bg"
|
||||
className={filteredRoles?.length ? "border-primary" : ""}
|
||||
>
|
||||
<Tooltip content="Advance Filter">
|
||||
<FontAwesomeIcon icon={faFilter} />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto border border-mineshaft-600 bg-mineshaft-800 p-2 drop-shadow-2xl">
|
||||
<div className="mb-4 border-b border-b-gray-700 pb-2 text-sm text-mineshaft-300">
|
||||
Advance Filter
|
||||
</div>
|
||||
<form
|
||||
onSubmit={filterForm.handleSubmit((el) => {
|
||||
setFilteredRoles(el.roles?.split(",")?.filter(Boolean) || []);
|
||||
})}
|
||||
>
|
||||
<Controller
|
||||
control={filterForm.control}
|
||||
name="roles"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Roles"
|
||||
helperText="Eg: admin,viewer"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
type="submit"
|
||||
size="xs"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
className="mt-4"
|
||||
>
|
||||
Apply Filter
|
||||
</Button>
|
||||
{Boolean(filteredRoles.length) && (
|
||||
<Button
|
||||
size="xs"
|
||||
variant="link"
|
||||
className="ml-4 mt-4"
|
||||
onClick={() => {
|
||||
filterForm.reset({ roles: "" });
|
||||
setFilteredRoles([]);
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
@@ -251,8 +258,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th>Role</Th>
|
||||
{/* <Th>
|
||||
<Th>
|
||||
<div className="flex items-center">
|
||||
Role
|
||||
<IconButton
|
||||
@@ -271,7 +277,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
/>
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th> */}
|
||||
</Th>
|
||||
<Th className="w-16">{isFetching ? <Spinner size="xs" /> : null}</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
@@ -303,7 +309,8 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
<Select
|
||||
value={role === "custom" ? (customRole?.slug as string) : role}
|
||||
isDisabled={!isAllowed}
|
||||
className="w-48 bg-mineshaft-600"
|
||||
className="h-8 w-48 bg-mineshaft-700"
|
||||
position="popper"
|
||||
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
||||
onValueChange={(selectedRole) =>
|
||||
handleChangeRole({
|
||||
@@ -324,21 +331,24 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
</Td>
|
||||
<Td>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="flex justify-center hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
||||
</div>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
className="w-6"
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="mt-3 p-1">
|
||||
<DropdownMenuContent sideOffset={2} align="end">
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionIdentityActions.Edit}
|
||||
a={OrgPermissionSubjects.Identity}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
icon={<FontAwesomeIcon icon={faEdit} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate({
|
||||
@@ -348,7 +358,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
}
|
||||
});
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Edit Identity
|
||||
</DropdownMenuItem>
|
||||
@@ -360,11 +370,6 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
isAllowed
|
||||
? "hover:!bg-red-500 hover:!text-white"
|
||||
: "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("deleteIdentity", {
|
||||
@@ -372,7 +377,8 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
name
|
||||
});
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
isDisabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
>
|
||||
Delete Identity
|
||||
</DropdownMenuItem>
|
||||
@@ -398,7 +404,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
{!isPending && data && data?.identities.length === 0 && (
|
||||
<EmptyState
|
||||
title={
|
||||
debouncedSearch.trim().length > 0 || filteredRoles?.length > 0
|
||||
debouncedSearch.trim().length > 0 || filter.roles?.length > 0
|
||||
? "No identities match search filter"
|
||||
: "No identities have been created in this organization"
|
||||
}
|
||||
|
@@ -115,12 +115,12 @@ export const OrgMembersSection = () => {
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex justify-between">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Users</p>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Member}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handleAddMemberModal()}
|
||||
|
@@ -4,11 +4,14 @@ import {
|
||||
faArrowUp,
|
||||
faCheckCircle,
|
||||
faChevronRight,
|
||||
faEllipsis,
|
||||
faEdit,
|
||||
faEllipsisV,
|
||||
faFilter,
|
||||
faMagnifyingGlass,
|
||||
faSearch,
|
||||
faUsers
|
||||
faUsers,
|
||||
faUserSlash,
|
||||
faUserXmark
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
@@ -79,7 +82,8 @@ type Props = {
|
||||
|
||||
enum OrgMembersOrderBy {
|
||||
Name = "firstName",
|
||||
Email = "email"
|
||||
Email = "email",
|
||||
Role = "role"
|
||||
}
|
||||
|
||||
type Filter = {
|
||||
@@ -99,8 +103,10 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
|
||||
const { data: serverDetails } = useFetchServerStatus();
|
||||
const { data: members = [], isPending: isMembersLoading } = useGetOrgUsers(orgId);
|
||||
|
||||
const { mutateAsync: resendOrgMemberInvitation } = useResendOrgMemberInvitation();
|
||||
const { mutateAsync: resendOrgMemberInvitation, isPending: isResendInvitePending } =
|
||||
useResendOrgMemberInvitation();
|
||||
const { mutateAsync: updateOrgMembership } = useUpdateOrgMembership();
|
||||
const [resendInviteId, setResendInviteId] = useState<string | null>(null);
|
||||
|
||||
const onRoleChange = async (membershipId: string, role: string) => {
|
||||
if (!currentOrg?.id) return;
|
||||
@@ -136,6 +142,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
|
||||
};
|
||||
|
||||
const onResendInvite = async (membershipId: string) => {
|
||||
setResendInviteId(membershipId);
|
||||
try {
|
||||
const signupToken = await resendOrgMemberInvitation({
|
||||
membershipId
|
||||
@@ -156,6 +163,8 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
|
||||
text: "Failed to resend org invitation",
|
||||
type: "error"
|
||||
});
|
||||
} finally {
|
||||
setResendInviteId(null);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -229,6 +238,16 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
|
||||
valueOne = memberOne.user.email || memberOne.inviteEmail;
|
||||
valueTwo = memberTwo.user.email || memberTwo.inviteEmail;
|
||||
break;
|
||||
case OrgMembersOrderBy.Role:
|
||||
valueOne =
|
||||
memberOne.role === "custom"
|
||||
? findRoleFromId(memberOne.roleId)!.slug
|
||||
: memberOne.role;
|
||||
valueTwo =
|
||||
memberTwo.role === "custom"
|
||||
? findRoleFromId(memberTwo.roleId)!.slug
|
||||
: memberTwo.role;
|
||||
break;
|
||||
case OrgMembersOrderBy.Name:
|
||||
default:
|
||||
valueOne = memberOne.user.firstName;
|
||||
@@ -284,7 +303,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className={twMerge(
|
||||
"flex h-10 w-11 items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
|
||||
"flex h-[2.375rem] w-[2.6rem] items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
|
||||
isTableFiltered && "border-primary/50 text-primary"
|
||||
)}
|
||||
>
|
||||
@@ -378,7 +397,26 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th>Role</Th>
|
||||
<Th className="w-1/3">
|
||||
<div className="flex items-center">
|
||||
Role
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={`ml-2 ${orderBy === OrgMembersOrderBy.Role ? "" : "opacity-30"}`}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(OrgMembersOrderBy.Role)}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={
|
||||
orderDirection === OrderByDirection.DESC &&
|
||||
orderBy === OrgMembersOrderBy.Role
|
||||
? faArrowUp
|
||||
: faArrowDown
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
@@ -398,7 +436,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
|
||||
isActive
|
||||
}) => {
|
||||
const name =
|
||||
u && u.firstName ? `${u.firstName} ${u.lastName ?? ""}`.trim() : "-";
|
||||
u && u.firstName ? `${u.firstName} ${u.lastName ?? ""}`.trim() : null;
|
||||
const email = u?.email || inviteEmail;
|
||||
const username = u?.username ?? inviteEmail ?? "-";
|
||||
return (
|
||||
@@ -415,7 +453,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
|
||||
}
|
||||
>
|
||||
<Td className={isActive ? "" : "text-mineshaft-400"}>
|
||||
{name}
|
||||
{name ?? <span className="text-mineshaft-400">Not Set</span>}
|
||||
{u.superAdmin && (
|
||||
<Badge variant="primary" className="ml-2">
|
||||
Server Admin
|
||||
@@ -429,79 +467,77 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
|
||||
a={OrgPermissionSubjects.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<>
|
||||
{!isActive && (
|
||||
<Button
|
||||
isDisabled
|
||||
className="w-40"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
onClick={() => {}}
|
||||
>
|
||||
Suspended
|
||||
</Button>
|
||||
)}
|
||||
{isActive && status === "accepted" && (
|
||||
<Select
|
||||
value={role === "custom" ? findRoleFromId(roleId)?.slug : role}
|
||||
isDisabled={userId === u?.id || !isAllowed}
|
||||
className="w-48 bg-mineshaft-600"
|
||||
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
||||
onValueChange={(selectedRole) =>
|
||||
onRoleChange(orgMembershipId, selectedRole)
|
||||
}
|
||||
>
|
||||
{(roles || [])
|
||||
.filter(({ slug }) =>
|
||||
slug === "owner" ? isIamOwner || role === "owner" : true
|
||||
)
|
||||
.map(({ slug, name: roleName }) => (
|
||||
<SelectItem value={slug} key={`owner-option-${slug}`}>
|
||||
{roleName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
{isActive &&
|
||||
(status === "invited" || status === "verified") &&
|
||||
email &&
|
||||
serverDetails?.emailConfigured && (
|
||||
<Select
|
||||
value={role === "custom" ? findRoleFromId(roleId)?.slug : role}
|
||||
isDisabled={userId === u?.id || !isAllowed}
|
||||
className="h-8 w-48 bg-mineshaft-700"
|
||||
position="popper"
|
||||
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
||||
onValueChange={(selectedRole) =>
|
||||
onRoleChange(orgMembershipId, selectedRole)
|
||||
}
|
||||
>
|
||||
{(roles || [])
|
||||
.filter(({ slug }) =>
|
||||
slug === "owner" ? isIamOwner || role === "owner" : true
|
||||
)
|
||||
.map(({ slug, name: roleName }) => (
|
||||
<SelectItem value={slug} key={`owner-option-${slug}`}>
|
||||
{roleName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex items-center justify-end gap-6">
|
||||
{isActive &&
|
||||
(status === "invited" || status === "verified") &&
|
||||
email &&
|
||||
serverDetails?.emailConfigured && (
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Edit}
|
||||
a={OrgPermissionSubjects.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
isDisabled={!isAllowed}
|
||||
className="w-48"
|
||||
isDisabled={!isAllowed || isResendInvitePending}
|
||||
className="h-8 border-mineshaft-600 bg-mineshaft-700 font-normal"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
isLoading={
|
||||
isResendInvitePending && resendInviteId === orgMembershipId
|
||||
}
|
||||
onClick={(e) => {
|
||||
onResendInvite(orgMembershipId);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
Resend invite
|
||||
Resend Invite
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</Td>
|
||||
<Td>
|
||||
{userId !== u?.id && (
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
||||
</div>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
colorSchema="secondary"
|
||||
className={twMerge("w-6", userId === u?.id && "opacity-50")}
|
||||
variant="plain"
|
||||
isDisabled={userId === u?.id}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<DropdownMenuContent sideOffset={2} align="end">
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Edit}
|
||||
a={OrgPermissionSubjects.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed &&
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate({
|
||||
@@ -511,7 +547,8 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
|
||||
}
|
||||
});
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
isDisabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faEdit} />}
|
||||
>
|
||||
Edit User
|
||||
</DropdownMenuItem>
|
||||
@@ -523,15 +560,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={
|
||||
isActive
|
||||
? twMerge(
|
||||
isAllowed
|
||||
? "hover:!bg-red-500 hover:!text-white"
|
||||
: "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)
|
||||
: ""
|
||||
}
|
||||
icon={<FontAwesomeIcon icon={faUserSlash} />}
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -560,7 +589,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
|
||||
username
|
||||
});
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
{`${isActive ? "Deactivate" : "Activate"} User`}
|
||||
</DropdownMenuItem>
|
||||
@@ -572,11 +601,6 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
isAllowed
|
||||
? "hover:!bg-red-500 hover:!text-white"
|
||||
: "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -593,7 +617,8 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
|
||||
username
|
||||
});
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
isDisabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faUserXmark} />}
|
||||
>
|
||||
Remove User
|
||||
</DropdownMenuItem>
|
||||
@@ -601,7 +626,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
|
||||
</OrgPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
|
@@ -6,10 +6,10 @@ export const OrgRoleTabSection = () => {
|
||||
return (
|
||||
<motion.div
|
||||
key="role-list"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: -30 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
>
|
||||
<OrgRoleTable />
|
||||
</motion.div>
|
||||
|
@@ -1,4 +1,17 @@
|
||||
import { faEllipsis, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faCopy,
|
||||
faEdit,
|
||||
faEllipsisV,
|
||||
faEye,
|
||||
faIdBadge,
|
||||
faMagnifyingGlass,
|
||||
faPlus,
|
||||
faSearch,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
@@ -14,6 +27,10 @@ import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
Pagination,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
@@ -30,13 +47,25 @@ import {
|
||||
useOrganization,
|
||||
useSubscription
|
||||
} from "@app/context";
|
||||
import { isCustomOrgRole } from "@app/helpers/roles";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { isCustomOrgRole, isCustomProjectRole } from "@app/helpers/roles";
|
||||
import {
|
||||
getUserTablePreference,
|
||||
PreferenceKey,
|
||||
setUserTablePreference
|
||||
} from "@app/helpers/userTablePreferences";
|
||||
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
||||
import { useDeleteOrgRole, useGetOrgRoles, useUpdateOrg } from "@app/hooks/api";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { TOrgRole } from "@app/hooks/api/roles/types";
|
||||
import { DuplicateOrgRoleModal } from "@app/pages/organization/RoleByIDPage/components/DuplicateOrgRoleModal";
|
||||
import { RoleModal } from "@app/pages/organization/RoleByIDPage/components/RoleModal";
|
||||
|
||||
enum RolesOrderBy {
|
||||
Name = "name",
|
||||
Slug = "slug",
|
||||
Type = "type"
|
||||
}
|
||||
|
||||
export const OrgRoleTable = () => {
|
||||
const navigate = useNavigate();
|
||||
const { currentOrg } = useOrganization();
|
||||
@@ -93,14 +122,89 @@ export const OrgRoleTable = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
orderDirection,
|
||||
toggleOrderDirection,
|
||||
orderBy,
|
||||
setOrderDirection,
|
||||
setOrderBy,
|
||||
search,
|
||||
setSearch,
|
||||
page,
|
||||
perPage,
|
||||
setPerPage,
|
||||
setPage,
|
||||
offset
|
||||
} = usePagination<RolesOrderBy>(RolesOrderBy.Type, {
|
||||
initPerPage: getUserTablePreference("orgRolesTable", PreferenceKey.PerPage, 20)
|
||||
});
|
||||
|
||||
const handlePerPageChange = (newPerPage: number) => {
|
||||
setPerPage(newPerPage);
|
||||
setUserTablePreference("orgRolesTable", PreferenceKey.PerPage, newPerPage);
|
||||
};
|
||||
|
||||
const filteredRoles = useMemo(
|
||||
() =>
|
||||
roles
|
||||
?.filter((role) => {
|
||||
const { slug, name } = role;
|
||||
|
||||
const searchValue = search.trim().toLowerCase();
|
||||
|
||||
return (
|
||||
name.toLowerCase().includes(searchValue) || slug.toLowerCase().includes(searchValue)
|
||||
);
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const [roleOne, roleTwo] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
|
||||
|
||||
switch (orderBy) {
|
||||
case RolesOrderBy.Slug:
|
||||
return roleOne.slug.toLowerCase().localeCompare(roleTwo.slug.toLowerCase());
|
||||
case RolesOrderBy.Type: {
|
||||
const roleOneValue = isCustomOrgRole(roleOne.slug) ? -1 : 1;
|
||||
const roleTwoValue = isCustomOrgRole(roleTwo.slug) ? -1 : 1;
|
||||
|
||||
return roleTwoValue - roleOneValue;
|
||||
}
|
||||
case RolesOrderBy.Name:
|
||||
default:
|
||||
return roleOne.name.toLowerCase().localeCompare(roleTwo.name.toLowerCase());
|
||||
}
|
||||
}) ?? [],
|
||||
[roles, orderDirection, search, orderBy]
|
||||
);
|
||||
|
||||
useResetPageHelper({
|
||||
totalCount: filteredRoles.length,
|
||||
offset,
|
||||
setPage
|
||||
});
|
||||
|
||||
const handleSort = (column: RolesOrderBy) => {
|
||||
if (column === orderBy) {
|
||||
toggleOrderDirection();
|
||||
return;
|
||||
}
|
||||
|
||||
setOrderBy(column);
|
||||
setOrderDirection(OrderByDirection.ASC);
|
||||
};
|
||||
|
||||
const getClassName = (col: RolesOrderBy) => twMerge("ml-2", orderBy === col ? "" : "opacity-30");
|
||||
|
||||
const getColSortIcon = (col: RolesOrderBy) =>
|
||||
orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown;
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex justify-between">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Organization Roles</p>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Role}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
@@ -113,18 +217,63 @@ export const OrgRoleTable = () => {
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search roles..."
|
||||
className="flex-1"
|
||||
containerClassName="mb-4"
|
||||
/>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th>Slug</Th>
|
||||
<Th>
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(RolesOrderBy.Name)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(RolesOrderBy.Name)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(RolesOrderBy.Name)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th>
|
||||
<div className="flex items-center">
|
||||
Slug
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(RolesOrderBy.Slug)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(RolesOrderBy.Slug)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(RolesOrderBy.Slug)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th>
|
||||
<div className="flex items-center">
|
||||
Type
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(RolesOrderBy.Type)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(RolesOrderBy.Type)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(RolesOrderBy.Type)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th aria-label="actions" className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isRolesLoading && <TableSkeleton columns={3} innerKey="org-roles" />}
|
||||
{roles?.map((role) => {
|
||||
{isRolesLoading && <TableSkeleton columns={4} innerKey="org-roles" />}
|
||||
{filteredRoles?.slice(offset, perPage * page).map((role) => {
|
||||
const { id, name, slug } = role;
|
||||
const isNonMutatable = ["owner", "admin", "member", "no-access"].includes(slug);
|
||||
const isDefaultOrgRole = isCustomOrgRole(slug)
|
||||
@@ -162,23 +311,30 @@ export const OrgRoleTable = () => {
|
||||
<Td className="max-w-md overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{slug}
|
||||
</Td>
|
||||
<Td>
|
||||
<Badge className="w-min whitespace-nowrap bg-mineshaft-400/50 text-bunker-200">
|
||||
{isCustomProjectRole(slug) ? "Custom" : "Default"}
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
||||
</div>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
colorSchema="secondary"
|
||||
className="w-6"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<DropdownMenuContent className="min-w-[12rem]" sideOffset={2} align="end">
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Edit}
|
||||
a={OrgPermissionSubjects.Role}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate({
|
||||
@@ -188,7 +344,8 @@ export const OrgRoleTable = () => {
|
||||
}
|
||||
});
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
isDisabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={isNonMutatable ? faEye : faEdit} />}
|
||||
>
|
||||
{`${isNonMutatable ? "View" : "Edit"} Role`}
|
||||
</DropdownMenuItem>
|
||||
@@ -200,14 +357,12 @@ export const OrgRoleTable = () => {
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("duplicateRole", role);
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
isDisabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faCopy} />}
|
||||
>
|
||||
Duplicate Role
|
||||
</DropdownMenuItem>
|
||||
@@ -220,14 +375,12 @@ export const OrgRoleTable = () => {
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
disabled={!isAllowed}
|
||||
isDisabled={!isAllowed}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleSetRoleAsDefault(slug);
|
||||
}}
|
||||
icon={<FontAwesomeIcon icon={faIdBadge} />}
|
||||
>
|
||||
Set as Default Role
|
||||
</DropdownMenuItem>
|
||||
@@ -250,16 +403,12 @@ export const OrgRoleTable = () => {
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
isAllowed && !isDefaultOrgRole
|
||||
? "hover:!bg-red-500 hover:!text-white"
|
||||
: "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("deleteRole", role);
|
||||
}}
|
||||
disabled={!isAllowed || isDefaultOrgRole}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
isDisabled={!isAllowed || isDefaultOrgRole}
|
||||
>
|
||||
Delete Role
|
||||
</DropdownMenuItem>
|
||||
@@ -276,6 +425,25 @@ export const OrgRoleTable = () => {
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{Boolean(filteredRoles?.length) && (
|
||||
<Pagination
|
||||
count={filteredRoles!.length}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={handlePerPageChange}
|
||||
/>
|
||||
)}
|
||||
{!filteredRoles?.length && !isRolesLoading && (
|
||||
<EmptyState
|
||||
title={
|
||||
roles?.length
|
||||
? "No roles match search..."
|
||||
: "This organization does not have any roles"
|
||||
}
|
||||
icon={roles?.length ? faSearch : undefined}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
<RoleModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<DeleteActionModal
|
||||
|
@@ -197,7 +197,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className={twMerge(
|
||||
"flex h-10 w-11 items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
|
||||
"flex h-[2.375rem] w-[2.6rem] items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
|
||||
isTableFiltered && "border-primary/50 text-primary"
|
||||
)}
|
||||
>
|
||||
@@ -298,7 +298,8 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
|
||||
{!isMembersLoading &&
|
||||
filteredUsers.slice(offset, perPage * page).map((projectMember) => {
|
||||
const { user: u, inviteEmail, id: membershipId, roles } = projectMember;
|
||||
const name = u.firstName || u.lastName ? `${u.firstName} ${u.lastName || ""}` : "-";
|
||||
const name =
|
||||
u.firstName || u.lastName ? `${u.firstName} ${u.lastName || ""}` : null;
|
||||
const email = u?.email || inviteEmail;
|
||||
|
||||
return (
|
||||
@@ -328,7 +329,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
|
||||
})
|
||||
}
|
||||
>
|
||||
<Td>{name}</Td>
|
||||
<Td>{name ?? <span className="text-mineshaft-400">Not Set</span>}</Td>
|
||||
<Td>{email}</Td>
|
||||
<Td>
|
||||
<div className="flex items-center space-x-2">
|
||||
|
@@ -236,7 +236,7 @@ export const ProjectRoleList = () => {
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isRolesLoading && <TableSkeleton columns={3} innerKey="project-roles" />}
|
||||
{isRolesLoading && <TableSkeleton columns={4} innerKey="project-roles" />}
|
||||
{filteredRoles?.slice(offset, perPage * page).map((role) => {
|
||||
const { id, name, slug } = role;
|
||||
const isNonMutatable = Object.values(ProjectMembershipRole).includes(
|
||||
|
@@ -12,7 +12,7 @@ import {
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { Button, Checkbox, Select, SelectItem, Tag, Tooltip } from "@app/components/v2";
|
||||
import { Button, Checkbox, IconButton, Select, SelectItem, Tag, Tooltip } from "@app/components/v2";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
@@ -241,16 +241,19 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
|
||||
/>
|
||||
</Tooltip>
|
||||
{!isDisabled && (
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faTrash} />}
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
className="ml-auto mr-3"
|
||||
onClick={() => remove(rootIndex)}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
Remove Rule
|
||||
</Button>
|
||||
<Tooltip content="Remove Rule">
|
||||
<IconButton
|
||||
ariaLabel="Remove rule"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
size="xs"
|
||||
className="ml-auto mr-3 rounded"
|
||||
onClick={() => remove(rootIndex)}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!isDisabled && (
|
||||
<Tooltip position="left" content="Drag to reorder permission">
|
||||
@@ -271,16 +274,19 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
|
||||
<div className="flex w-full justify-between">
|
||||
<div className="mb-2">Actions</div>
|
||||
{!isDisabled && !isConditionalSubjects(subject) && (
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faTrash} />}
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
className="ml-auto"
|
||||
onClick={() => remove(rootIndex)}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
Remove Rule
|
||||
</Button>
|
||||
<Tooltip content="Remove Rule">
|
||||
<IconButton
|
||||
ariaLabel="Remove rule"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
size="xs"
|
||||
className="ml-auto rounded"
|
||||
onClick={() => remove(rootIndex)}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-grow flex-wrap justify-start gap-x-8 gap-y-4">
|
||||
|
@@ -247,9 +247,7 @@ export const AccessApprovalRequest = ({
|
||||
};
|
||||
else if (userReviewStatus === ApprovalStatus.APPROVED) {
|
||||
displayData = {
|
||||
label: `Pending ${request.policy.approvals - request.reviewers.length} review${
|
||||
request.policy.approvals - request.reviewers.length > 1 ? "s" : ""
|
||||
}`,
|
||||
label: "Pending Additional Reviews",
|
||||
type: "primary",
|
||||
icon: faClipboardCheck
|
||||
};
|
||||
|
@@ -1,10 +1,9 @@
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { ReactNode, useCallback, useMemo, useState } from "react";
|
||||
import {
|
||||
faCheckCircle,
|
||||
faCircle,
|
||||
faTriangleExclamation,
|
||||
faUsers,
|
||||
faXmarkCircle
|
||||
faBan,
|
||||
faCheck,
|
||||
faHourglass,
|
||||
faTriangleExclamation
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import ms from "ms";
|
||||
@@ -15,12 +14,10 @@ import {
|
||||
Button,
|
||||
Checkbox,
|
||||
FormControl,
|
||||
GenericFieldLabel,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v2/Badge";
|
||||
@@ -38,10 +35,22 @@ import { groupBy } from "@app/lib/fn/array";
|
||||
|
||||
const getReviewedStatusSymbol = (status?: ApprovalStatus) => {
|
||||
if (status === ApprovalStatus.APPROVED)
|
||||
return <FontAwesomeIcon icon={faCheckCircle} size="xs" style={{ color: "#15803d" }} />;
|
||||
return (
|
||||
<Badge variant="success" className="flex h-4 items-center justify-center">
|
||||
<FontAwesomeIcon icon={faCheck} size="xs" />
|
||||
</Badge>
|
||||
);
|
||||
if (status === ApprovalStatus.REJECTED)
|
||||
return <FontAwesomeIcon icon={faXmarkCircle} size="xs" style={{ color: "#b91c1c" }} />;
|
||||
return <FontAwesomeIcon icon={faCircle} size="xs" style={{ color: "#c2410c" }} />;
|
||||
return (
|
||||
<Badge variant="danger" className="flex h-4 items-center justify-center">
|
||||
<FontAwesomeIcon icon={faBan} size="xs" />
|
||||
</Badge>
|
||||
);
|
||||
return (
|
||||
<Badge variant="primary" className="flex h-4 items-center justify-center">
|
||||
<FontAwesomeIcon icon={faHourglass} size="xs" />
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReviewAccessRequestModal = ({
|
||||
@@ -267,139 +276,160 @@ export const ReviewAccessRequestModal = ({
|
||||
</div>
|
||||
<div className="">
|
||||
<div className="mb-2 mt-4 text-mineshaft-200">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div className="mb-1 text-xs font-semibold uppercase">Environment</div>
|
||||
<div>{accessDetails.env || "-"}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-1 text-xs font-semibold uppercase">Secret Path</div>
|
||||
<div>{accessDetails.secretPath || "-"}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-1 text-xs font-semibold uppercase">Access Type</div>
|
||||
<div>{getAccessLabel()}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-1 text-xs font-semibold uppercase">Permission</div>
|
||||
<div>{requestedAccess}</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<div className="mb-1 text-xs font-semibold uppercase">Note</div>
|
||||
<div>{request.note || "-"}</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-8">
|
||||
<GenericFieldLabel label="Environment">{accessDetails.env}</GenericFieldLabel>
|
||||
<GenericFieldLabel truncate label="Secret Path">
|
||||
{accessDetails.secretPath}
|
||||
</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Access Type">{getAccessLabel()}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Permission">{requestedAccess}</GenericFieldLabel>
|
||||
{request.note && (
|
||||
<GenericFieldLabel className="col-span-full" label="Note">
|
||||
{request.note}
|
||||
</GenericFieldLabel>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4 border-b-2 border-mineshaft-500 py-2 text-lg">Approvers</div>
|
||||
<div className="thin-scrollbar max-h-64 overflow-y-auto rounded p-2">
|
||||
{approverSequence?.approvers?.map((approver, index) => (
|
||||
<div
|
||||
key={`approval-list-${index + 1}`}
|
||||
className={twMerge(
|
||||
"relative mb-2 flex items-center rounded border border-mineshaft-500 bg-mineshaft-700 p-4",
|
||||
approverSequence?.currentSequence !== approver.sequence &&
|
||||
!hasApproved &&
|
||||
"text-mineshaft-400"
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className={twMerge(
|
||||
"mr-8 flex h-8 w-8 items-center justify-center text-3xl font-medium",
|
||||
approver.hasApproved && "border-green-400 text-green-400",
|
||||
approver.hasRejected && "border-red-500 text-red-500"
|
||||
)}
|
||||
>
|
||||
{index + 1}
|
||||
</div>
|
||||
{index !== (approverSequence?.approvers?.length || 0) - 1 && (
|
||||
<div
|
||||
className={twMerge(
|
||||
"absolute bottom-0 left-8 h-5 border-r-2 border-gray-400",
|
||||
approver.hasApproved && "border-green-400",
|
||||
approver.hasRejected && "border-red-500"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{index !== 0 && (
|
||||
<div
|
||||
className={twMerge(
|
||||
"absolute left-8 top-0 h-5 border-r-2 border-gray-400",
|
||||
approver.hasApproved && "border-green-400",
|
||||
approver.hasRejected && "border-red-500"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid flex-grow grid-cols-3">
|
||||
<div>
|
||||
<div className="mb-1 text-xs font-semibold uppercase">Users</div>
|
||||
<div>
|
||||
{approver?.user
|
||||
?.map(
|
||||
(el) => approverSequence?.membersGroupById?.[el.id]?.[0]?.user?.username
|
||||
)
|
||||
.join(",") || "-"}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-1 text-xs font-semibold uppercase">Groups</div>
|
||||
<div>
|
||||
{approver?.group
|
||||
?.map(
|
||||
(el) =>
|
||||
approverSequence?.projectGroupsGroupById?.[el.id]?.[0]?.group?.name
|
||||
)
|
||||
.join(",") || "-"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div>
|
||||
<div className="mb-1 text-xs font-semibold uppercase">Approvals Required</div>
|
||||
<div>{approver.approvals || "-"}</div>
|
||||
</div>
|
||||
<div className="ml-16">
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<FontAwesomeIcon icon={faUsers} />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent hideCloseBtn className="pt-3">
|
||||
<div>
|
||||
<div className="mb-1 text-sm text-bunker-300">Reviewers</div>
|
||||
<div className="thin-scrollbar flex max-h-64 flex-col gap-1 overflow-y-auto rounded">
|
||||
{approver.reviewers.map((el, idx) => (
|
||||
<div
|
||||
key={`reviewer-${idx + 1}`}
|
||||
className="flex items-center gap-2 bg-mineshaft-700 p-1 text-sm"
|
||||
>
|
||||
<div className="flex-grow">{el.username}</div>
|
||||
<Tooltip
|
||||
content={`Status: ${el?.status || ApprovalStatus.PENDING}`}
|
||||
>
|
||||
{getReviewedStatusSymbol(el?.status as ApprovalStatus)}
|
||||
</Tooltip>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="mt-4 flex items-center justify-between border-b-2 border-mineshaft-500 py-2">
|
||||
<span>Approvers</span>
|
||||
{approverSequence.isMyReviewInThisSequence &&
|
||||
request.status === ApprovalStatus.PENDING && (
|
||||
<Badge variant="primary" className="h-min">
|
||||
Awaiting Your Review
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="thin-scrollbar max-h-[40vh] overflow-y-auto rounded py-2">
|
||||
{approverSequence?.approvers &&
|
||||
approverSequence.approvers.map((approver, index) => {
|
||||
const isInactive =
|
||||
approverSequence?.currentSequence <
|
||||
(approver.sequence ?? approverSequence.approvers!.length);
|
||||
|
||||
const isPending = approverSequence?.currentSequence === approver.sequence;
|
||||
|
||||
let StepComponent: ReactNode;
|
||||
let BadgeComponent: ReactNode = null;
|
||||
if (approver.hasRejected) {
|
||||
StepComponent = (
|
||||
<Badge
|
||||
variant="danger"
|
||||
className="flex h-6 min-w-6 items-center justify-center"
|
||||
>
|
||||
<FontAwesomeIcon icon={faBan} />
|
||||
</Badge>
|
||||
);
|
||||
BadgeComponent = <Badge variant="danger">Rejected</Badge>;
|
||||
} else if (approver.hasApproved) {
|
||||
StepComponent = (
|
||||
<Badge
|
||||
variant="success"
|
||||
className="flex h-6 min-w-6 items-center justify-center"
|
||||
>
|
||||
<FontAwesomeIcon icon={faCheck} />
|
||||
</Badge>
|
||||
);
|
||||
BadgeComponent = <Badge variant="success">Approved</Badge>;
|
||||
} else if (isPending) {
|
||||
StepComponent = (
|
||||
<Badge
|
||||
variant="primary"
|
||||
className="flex h-6 min-w-6 items-center justify-center"
|
||||
>
|
||||
<FontAwesomeIcon icon={faHourglass} />
|
||||
</Badge>
|
||||
);
|
||||
BadgeComponent = <Badge variant="primary">Pending</Badge>;
|
||||
} else {
|
||||
StepComponent = (
|
||||
<Badge
|
||||
className={
|
||||
isInactive
|
||||
? "py-auto my-auto flex h-6 min-w-6 items-center justify-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-center text-bunker-200"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<span>{index + 1}</span>
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`approval-list-${index + 1}`}
|
||||
className={twMerge("flex", isInactive && "opacity-50")}
|
||||
>
|
||||
{approverSequence.approvers!.length > 1 && (
|
||||
<div className="flex w-12 flex-col items-center gap-2 pr-4">
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex-grow border-mineshaft-600",
|
||||
index !== 0 && "border-r"
|
||||
)}
|
||||
/>
|
||||
{StepComponent}
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex-grow border-mineshaft-600",
|
||||
index < approverSequence.approvers!.length - 1 && "border-r"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid flex-1 grid-cols-5 border-b border-mineshaft-600 p-4">
|
||||
<GenericFieldLabel className="col-span-2" label="Users">
|
||||
{approver?.user
|
||||
?.map(
|
||||
(el) => approverSequence?.membersGroupById?.[el.id]?.[0]?.user?.username
|
||||
)
|
||||
.join(", ")}
|
||||
</GenericFieldLabel>
|
||||
<GenericFieldLabel className="col-span-2" label="Groups">
|
||||
{approver?.group
|
||||
?.map(
|
||||
(el) =>
|
||||
approverSequence?.projectGroupsGroupById?.[el.id]?.[0]?.group?.name
|
||||
)
|
||||
.join(", ")}
|
||||
</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Approvals Required">
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2">{approver.approvals}</span>
|
||||
{BadgeComponent && (
|
||||
<Tooltip
|
||||
className="max-w-lg"
|
||||
content={
|
||||
<div>
|
||||
<div className="mb-1 text-sm text-bunker-300">Reviewers</div>
|
||||
<div className="thin-scrollbar flex max-h-64 flex-col divide-y divide-mineshaft-500 overflow-y-auto rounded">
|
||||
{approver.reviewers.map((el, idx) => (
|
||||
<div
|
||||
key={`reviewer-${idx + 1}`}
|
||||
className="flex items-center gap-2 px-2 py-2 text-sm"
|
||||
>
|
||||
<div className="flex-1">{el.username}</div>
|
||||
{getReviewedStatusSymbol(el?.status as ApprovalStatus)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div>{BadgeComponent}</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</GenericFieldLabel>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{approverSequence.isMyReviewInThisSequence &&
|
||||
request.status === ApprovalStatus.PENDING && (
|
||||
<div className="mb-4 rounded-r border-l-2 border-l-primary-400 bg-mineshaft-300/5 px-4 py-2.5 text-sm">
|
||||
Awaiting review from you.
|
||||
</div>
|
||||
)}
|
||||
{shouldBlockRequestActions ? (
|
||||
<div
|
||||
className={twMerge(
|
||||
"mb-4 rounded-r border-l-2 border-l-red-500 bg-mineshaft-300/5 px-4 py-2.5 text-sm",
|
||||
"mt-2 rounded-r border-l-2 border-l-red-500 bg-mineshaft-300/5 px-4 py-2.5 text-sm",
|
||||
isReviewedByMe && "border-l-green-400",
|
||||
!approverSequence.isMyReviewInThisSequence && "border-l-primary-400",
|
||||
hasRejected && "border-l-red-500"
|
||||
@@ -409,41 +439,6 @@ export const ReviewAccessRequestModal = ({
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="space-x-2">
|
||||
<Button
|
||||
isLoading={isLoading === "approved"}
|
||||
isDisabled={
|
||||
Boolean(isLoading) ||
|
||||
(!(
|
||||
request.isApprover &&
|
||||
(!request.isRequestedByCurrentUser || request.isSelfApproveAllowed)
|
||||
) &&
|
||||
!bypassApproval)
|
||||
}
|
||||
onClick={() => handleReview("approved")}
|
||||
className="mt-4"
|
||||
size="sm"
|
||||
colorSchema={!request.isApprover && isSoftEnforcement ? "danger" : "primary"}
|
||||
>
|
||||
Approve Request
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={isLoading === "rejected"}
|
||||
isDisabled={
|
||||
!!isLoading ||
|
||||
(!(
|
||||
request.isApprover &&
|
||||
(!request.isRequestedByCurrentUser || request.isSelfApproveAllowed)
|
||||
) &&
|
||||
!bypassApproval)
|
||||
}
|
||||
onClick={() => handleReview("rejected")}
|
||||
className="mt-4 border-transparent bg-transparent text-mineshaft-200 hover:border-red hover:bg-red/20 hover:text-mineshaft-200"
|
||||
size="sm"
|
||||
>
|
||||
Reject Request
|
||||
</Button>
|
||||
</div>
|
||||
{isSoftEnforcement &&
|
||||
request.isRequestedByCurrentUser &&
|
||||
!(request.isApprover && request.isSelfApproveAllowed) &&
|
||||
@@ -453,11 +448,7 @@ export const ReviewAccessRequestModal = ({
|
||||
onCheckedChange={(checked) => setBypassApproval(checked === true)}
|
||||
isChecked={bypassApproval}
|
||||
id="byPassApproval"
|
||||
checkIndicatorBg="text-white"
|
||||
className={twMerge(
|
||||
"mr-2",
|
||||
bypassApproval ? "border-red bg-red hover:bg-red-600" : ""
|
||||
)}
|
||||
className={twMerge("mr-2", bypassApproval ? "border-red/30 bg-red/10" : "")}
|
||||
>
|
||||
<span className="text-xs text-red">
|
||||
Approve without waiting for requirements to be met (bypass policy
|
||||
@@ -481,6 +472,42 @@ export const ReviewAccessRequestModal = ({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="space-x-2">
|
||||
<Button
|
||||
isLoading={isLoading === "approved"}
|
||||
isDisabled={
|
||||
Boolean(isLoading) ||
|
||||
(!(
|
||||
request.isApprover &&
|
||||
(!request.isRequestedByCurrentUser || request.isSelfApproveAllowed)
|
||||
) &&
|
||||
!bypassApproval)
|
||||
}
|
||||
onClick={() => handleReview("approved")}
|
||||
className="mt-4"
|
||||
size="sm"
|
||||
variant="outline_bg"
|
||||
colorSchema={!request.isApprover && isSoftEnforcement ? "danger" : "primary"}
|
||||
>
|
||||
Approve Request
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={isLoading === "rejected"}
|
||||
isDisabled={
|
||||
!!isLoading ||
|
||||
(!(
|
||||
request.isApprover &&
|
||||
(!request.isRequestedByCurrentUser || request.isSelfApproveAllowed)
|
||||
) &&
|
||||
!bypassApproval)
|
||||
}
|
||||
onClick={() => handleReview("rejected")}
|
||||
className="mt-4 border-transparent bg-transparent text-mineshaft-200 hover:border-red hover:bg-red/20 hover:text-mineshaft-200"
|
||||
size="sm"
|
||||
>
|
||||
Reject Request
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
@@ -1,5 +1,12 @@
|
||||
import { useMemo } from "react";
|
||||
import { faEdit, faEllipsisV, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
faClipboardCheck,
|
||||
faEdit,
|
||||
faEllipsisV,
|
||||
faTrash,
|
||||
faUser,
|
||||
faUserGroup
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
@@ -179,26 +186,40 @@ export const ApprovalPolicyRow = ({
|
||||
}`}
|
||||
>
|
||||
<div className="p-4">
|
||||
<div className="mb-4 border-b-2 border-mineshaft-500 pb-2">Approvers</div>
|
||||
<div className="border-b-2 border-mineshaft-500 pb-2">Approvers</div>
|
||||
{labels?.map((el, index) => (
|
||||
<div
|
||||
key={`approval-list-${index + 1}`}
|
||||
className="relative mb-2 flex rounded border border-mineshaft-500 bg-mineshaft-800 p-4"
|
||||
>
|
||||
<div className="my-auto mr-8 flex h-8 w-8 items-center justify-center rounded border border-mineshaft-400 bg-bunker-500/50 text-white">
|
||||
<div>{index + 1}</div>
|
||||
</div>
|
||||
{index !== labels.length - 1 && (
|
||||
<div className="absolute bottom-0 left-8 h-[1.25rem] border-r border-mineshaft-400" />
|
||||
<div key={`approval-list-${index + 1}`} className="flex">
|
||||
{labels.length > 1 && (
|
||||
<div className="flex w-12 flex-col items-center gap-2 pr-4">
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex-grow border-mineshaft-600",
|
||||
index !== 0 && "border-r"
|
||||
)}
|
||||
/>
|
||||
{labels.length > 1 && (
|
||||
<Badge className="my-auto flex h-5 w-min min-w-5 items-center justify-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-center text-bunker-200">
|
||||
<span>{index + 1}</span>
|
||||
</Badge>
|
||||
)}
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex-grow border-mineshaft-600",
|
||||
index < labels.length - 1 && "border-r"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{index !== 0 && (
|
||||
<div className="absolute left-8 top-0 h-[1.25rem] border-r border-mineshaft-400" />
|
||||
)}
|
||||
|
||||
<div className="grid flex-grow grid-cols-3">
|
||||
<GenericFieldLabel label="Users">{el.userLabels}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Groups">{el.groupLabels}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Approvals Required">{el.approvals}</GenericFieldLabel>
|
||||
<div className="grid flex-1 grid-cols-5 border-b border-mineshaft-600 p-4">
|
||||
<GenericFieldLabel className="col-span-2" icon={faUser} label="Users">
|
||||
{el.userLabels}
|
||||
</GenericFieldLabel>
|
||||
<GenericFieldLabel className="col-span-2" icon={faUserGroup} label="Groups">
|
||||
{el.groupLabels}
|
||||
</GenericFieldLabel>
|
||||
<GenericFieldLabel icon={faClipboardCheck} label="Approvals Required">
|
||||
{el.approvals}
|
||||
</GenericFieldLabel>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
Reference in New Issue
Block a user