Merge branch 'heads/main' into daniel/vault-import

This commit is contained in:
Daniel Hougaard
2025-07-24 17:29:55 +04:00
697 changed files with 18099 additions and 7050 deletions
@@ -105,7 +105,7 @@ export const OrgAdminProjects = withPermission(
<TBody>
{isProjectsLoading && <TableSkeleton columns={4} innerKey="projects" />}
{!isProjectsLoading &&
projects?.map(({ name, slug, createdAt, id, defaultProduct }) => (
projects?.map(({ name, slug, createdAt, id, type }) => (
<Tr key={`project-${id}`} className="group w-full">
<Td>{name}</Td>
<Td>{slug}</Td>
@@ -126,7 +126,7 @@ export const OrgAdminProjects = withPermission(
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
handleAccessProject(defaultProduct, id);
handleAccessProject(type, id);
}}
icon={<FontAwesomeIcon icon={faSignIn} />}
disabled={
@@ -21,6 +21,7 @@ import { CamundaConnectionForm } from "./CamundaConnectionForm";
import { ChecklyConnectionForm } from "./ChecklyConnectionForm";
import { CloudflareConnectionForm } from "./CloudflareConnectionForm";
import { DatabricksConnectionForm } from "./DatabricksConnectionForm";
import { DigitalOceanConnectionForm } from "./DigitalOceanConnectionForm";
import { FlyioConnectionForm } from "./FlyioConnectionForm";
import { GcpConnectionForm } from "./GcpConnectionForm";
import { GitHubConnectionForm } from "./GitHubConnectionForm";
@@ -33,6 +34,7 @@ import { LdapConnectionForm } from "./LdapConnectionForm";
import { MsSqlConnectionForm } from "./MsSqlConnectionForm";
import { MySqlConnectionForm } from "./MySqlConnectionForm";
import { OCIConnectionForm } from "./OCIConnectionForm";
import { OktaConnectionForm } from "./OktaConnectionForm";
import { OracleDBConnectionForm } from "./OracleDBConnectionForm";
import { PostgresConnectionForm } from "./PostgresConnectionForm";
import { RailwayConnectionForm } from "./RailwayConnectionForm";
@@ -149,6 +151,10 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
return <ChecklyConnectionForm onSubmit={onSubmit} />;
case AppConnection.Supabase:
return <SupabaseConnectionForm onSubmit={onSubmit} />;
case AppConnection.DigitalOcean:
return <DigitalOceanConnectionForm onSubmit={onSubmit} />;
case AppConnection.Okta:
return <OktaConnectionForm onSubmit={onSubmit} />;
default:
throw new Error(`Unhandled App ${app}`);
}
@@ -253,6 +259,10 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
return <ChecklyConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Supabase:
return <SupabaseConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.DigitalOcean:
return <DigitalOceanConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Okta:
return <OktaConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
default:
throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`);
}
@@ -0,0 +1,136 @@
import { Controller, FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
Button,
FormControl,
ModalClose,
SecretInput,
Select,
SelectItem
} from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
DigitalOceanConnectionMethod,
TDigitalOceanConnection
} from "@app/hooks/api/appConnections/types/digital-ocean";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
} from "./GenericAppConnectionFields";
type Props = {
appConnection?: TDigitalOceanConnection;
onSubmit: (formData: FormData) => void;
};
const rootSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.DigitalOcean)
});
const formSchema = z.discriminatedUnion("method", [
rootSchema.extend({
method: z.literal(DigitalOceanConnectionMethod.ApiToken),
credentials: z.object({
apiToken: z.string().trim().min(1, "API Token required")
})
})
]);
type FormData = z.infer<typeof formSchema>;
export const DigitalOceanConnectionForm = ({ appConnection, onSubmit }: Props) => {
const isUpdate = Boolean(appConnection);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: appConnection ?? {
app: AppConnection.DigitalOcean,
method: DigitalOceanConnectionMethod.ApiToken
}
});
const {
handleSubmit,
control,
formState: { isSubmitting, isDirty }
} = form;
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="method"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText={`The type of token you would like to use to connect with ${
APP_CONNECTION_MAP[AppConnection.DigitalOcean].name
}. This field cannot be changed after creation.`}
errorText={error?.message}
isError={Boolean(error?.message)}
label="Token Type"
>
<Select
isDisabled={isUpdate}
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
{Object.values(DigitalOceanConnectionMethod).map((method) => {
return (
<SelectItem value={method} key={method}>
{getAppConnectionMethodDetails(method).name}{" "}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<Controller
name="credentials.apiToken"
control={control}
shouldUnregister
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="API Key Value"
>
<SecretInput
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Credentials" : "Connect to DigitalOcean"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};
@@ -0,0 +1,157 @@
import { Controller, FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
Button,
FormControl,
Input,
ModalClose,
SecretInput,
Select,
SelectItem
} from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import { OktaConnectionMethod, TOktaConnection } from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
} from "./GenericAppConnectionFields";
type Props = {
appConnection?: TOktaConnection;
onSubmit: (formData: FormData) => void;
};
const rootSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.Okta)
});
const formSchema = z.discriminatedUnion("method", [
rootSchema.extend({
method: z.literal(OktaConnectionMethod.ApiToken),
credentials: z.object({
instanceUrl: z
.string()
.trim()
.url("Invalid Instance URL")
.min(1, "Instance URL required")
.max(255),
apiToken: z
.string()
.trim()
.min(1, "API Token required")
.regex(/^00[a-zA-Z0-9_-]{40}$/, "Invalid Okta API Token format")
})
})
]);
type FormData = z.infer<typeof formSchema>;
export const OktaConnectionForm = ({ appConnection, onSubmit }: Props) => {
const isUpdate = Boolean(appConnection);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: appConnection ?? {
app: AppConnection.Okta,
method: OktaConnectionMethod.ApiToken
}
});
const {
handleSubmit,
control,
formState: { isSubmitting, isDirty }
} = form;
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="method"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText={`The method you would like to use to connect with ${
APP_CONNECTION_MAP[AppConnection.Okta].name
}. This field cannot be changed after creation.`}
errorText={error?.message}
isError={Boolean(error?.message)}
label="Method"
>
<Select
isDisabled={isUpdate}
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
{Object.values(OktaConnectionMethod).map((method) => {
return (
<SelectItem value={method} key={method}>
{getAppConnectionMethodDetails(method).name}{" "}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<Controller
name="credentials.instanceUrl"
control={control}
shouldUnregister
render={({ field, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Instance URL"
>
<Input {...field} placeholder="https://example.okta.com" />
</FormControl>
)}
/>
<Controller
name="credentials.apiToken"
control={control}
shouldUnregister
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="API Token"
>
<SecretInput
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Credentials" : "Connect to Okta"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};
@@ -18,7 +18,7 @@ export const AuditLogsPage = () => {
title="Audit logs"
description="Audit logs for security and compliance teams to monitor information access."
/>
<LogsSection />
<LogsSection pageView />
</div>
</div>
</div>
@@ -1,9 +1,8 @@
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { faArrowRight, faCalendar, faChevronRight } from "@fortawesome/free-solid-svg-icons";
import { faCalendar, faChevronRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { format } from "date-fns";
import ms from "ms";
import { twMerge } from "tailwind-merge";
@@ -18,6 +17,7 @@ import {
Select,
SelectItem
} from "@app/components/v2";
import { formatDateTime, Timezone } from "@app/helpers/datetime";
import {
auditLogDateFilterFormSchema,
@@ -28,9 +28,19 @@ import {
type Props = {
setFilter: (data: TAuditLogDateFilterFormData) => void;
filter: TAuditLogDateFilterFormData;
setTimezone: (timezone: Timezone) => void;
timezone: Timezone;
};
const RELATIVE_VALUES = ["5m", "30m", "1h", "3h", "12h"];
export const LogsDateFilter = ({ setFilter, filter }: Props) => {
const RELATIVE_OPTIONS = [
{ label: "Minutes", unit: "m", values: [5, 10, 15, 30, 45] },
{ label: "Hours", unit: "h", values: [1, 2, 3, 6, 8, 12] },
{ label: "Days", unit: "d", values: [1, 2, 3, 4, 5, 6] },
{ label: "Weeks", unit: "w", values: [1, 2, 3, 4] }
];
export const LogsDateFilter = ({ setFilter, filter, timezone, setTimezone }: Props) => {
const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false);
const [isPopupOpen, setIsPopOpen] = useState(false);
@@ -59,154 +69,258 @@ export const LogsDateFilter = ({ setFilter, filter }: Props) => {
};
return (
<DropdownMenu open={isPopupOpen} onOpenChange={(el) => setIsPopOpen(el)}>
<div className="mr-2 flex items-center">
{filter.type === AuditLogDateFilterType.Relative ? (
<>
{RELATIVE_VALUES.map((el) => (
<Button
variant="outline_bg"
className={twMerge(
"rounded-none px-3 py-2 first:rounded-l-md",
filter.type === AuditLogDateFilterType.Relative &&
filter.relativeModeValue === el &&
"border-primary/40 bg-primary/[0.1]"
)}
key={`${el}-relative`}
onClick={() =>
setFilter({
relativeModeValue: el,
type: AuditLogDateFilterType.Relative,
endDate: new Date(),
startDate: new Date(Number(new Date()) - ms(el))
})
}
>
{el}
</Button>
))}
</>
) : (
<>
<div className="rounded-l-md border border-transparent bg-mineshaft-600 px-3 py-2 text-sm text-bunker-200">
{format(filter.startDate, "yyyy-MM-dd HH:mm")}
</div>
<div className="border border-transparent bg-mineshaft-600 px-3 py-2 text-sm text-bunker-200">
<FontAwesomeIcon icon={faChevronRight} />
</div>
<div className="border border-transparent bg-mineshaft-600 px-3 py-2 text-sm text-bunker-200">
{format(filter.endDate, "yyyy-MM-dd HH:mm")}
</div>
</>
)}
<DropdownMenuTrigger asChild>
<Button
variant="outline_bg"
className={twMerge(
"rounded-none rounded-r-md px-3 py-2",
(filter.type === AuditLogDateFilterType.Absolute || isCustomRelative) &&
"border-primary/40 bg-primary/[0.1]"
)}
>
<FontAwesomeIcon icon={faCalendar} />
{filter.type === AuditLogDateFilterType.Relative && isCustomRelative && (
<span className="ml-1">({filter.relativeModeValue})</span>
)}
</Button>
</DropdownMenuTrigger>
</div>
<DropdownMenuContent className="min-w-80 p-4" align="center" alignOffset={16}>
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
control={control}
name="type"
render={({ field }) => (
<FormControl className="mb-2 w-full">
<Select
value={field.value}
onValueChange={(el) => field.onChange(el)}
className="w-full"
<>
<DropdownMenu open={isPopupOpen} onOpenChange={(el) => setIsPopOpen(el)}>
<div className="flex items-center">
{filter.type === AuditLogDateFilterType.Relative ? (
<>
{RELATIVE_VALUES.map((el) => (
<Button
variant="outline_bg"
className={twMerge(
"w-[3.82rem] rounded-none px-3 py-2 font-normal first:rounded-l-md",
filter.type === AuditLogDateFilterType.Relative &&
filter.relativeModeValue === el &&
"border-primary/40 bg-primary/[0.1]"
)}
key={`${el}-relative`}
onClick={() =>
setFilter({
relativeModeValue: el,
type: AuditLogDateFilterType.Relative,
endDate: new Date(),
startDate: new Date(Number(new Date()) - ms(el))
})
}
>
<SelectItem value={AuditLogDateFilterType.Relative}>Relative</SelectItem>
<SelectItem value={AuditLogDateFilterType.Absolute}>Absolute</SelectItem>
</Select>
</FormControl>
)}
/>
{selectType === AuditLogDateFilterType.Relative && (
{el}
</Button>
))}
</>
) : (
<div className="flex w-[19.1rem] items-center justify-between rounded-l-md border border-transparent bg-mineshaft-600 px-5 py-2 text-sm text-bunker-200">
<div>
{formatDateTime({
timezone,
timestamp: filter.startDate,
dateFormat: "yyyy/MM/dd HH:mm"
})}
</div>
<div>
<FontAwesomeIcon className="text-bunker-300" size="sm" icon={faChevronRight} />
</div>
<div>
{formatDateTime({
timezone,
timestamp: filter.endDate,
dateFormat: "yyyy/MM/dd HH:mm"
})}
</div>
</div>
)}
<DropdownMenuTrigger asChild>
<Button
variant="outline_bg"
className={twMerge(
"w-[8rem] rounded-none rounded-r-md px-3 py-2 font-normal",
(filter.type === AuditLogDateFilterType.Absolute || isCustomRelative) &&
"border-primary/40 bg-primary/[0.1]"
)}
>
<span>Custom</span> <FontAwesomeIcon className="ml-1" icon={faCalendar} />
{filter.type === AuditLogDateFilterType.Relative && isCustomRelative && (
<span className="ml-1">({filter.relativeModeValue})</span>
)}
</Button>
</DropdownMenuTrigger>
</div>
<DropdownMenuContent
className="!min-w-[434px] bg-mineshaft-800 p-4"
align="end"
sideOffset={8}
>
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
control={control}
name="relativeModeValue"
name="type"
render={({ field }) => (
<FormControl className="mb-0 w-full" helperText="Example: 1h, 1d, 2d">
<Input {...field} value={field.value || ""} />
</FormControl>
<div className="mb-7">
<Button
onClick={() => field.onChange(AuditLogDateFilterType.Absolute)}
variant="outline_bg"
className={twMerge(
"h-8 rounded-r-none font-normal",
field.value === AuditLogDateFilterType.Absolute &&
"border-primary/40 bg-primary/[0.1]"
)}
>
Absolute
</Button>
<Button
onClick={() => field.onChange(AuditLogDateFilterType.Relative)}
variant="outline_bg"
className={twMerge(
"h-8 rounded-l-none font-normal",
field.value === AuditLogDateFilterType.Relative &&
"border-primary/40 bg-primary/[0.1]"
)}
>
Relative
</Button>
</div>
)}
/>
)}
{selectType === AuditLogDateFilterType.Absolute && (
<div className="mb-2 flex h-10 w-full items-center justify-between gap-2">
{selectType === AuditLogDateFilterType.Relative && (
<Controller
name="startDate"
control={control}
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
name="relativeModeValue"
render={({ field }) => {
const duration = field.value?.substring(0, field.value.length - 1);
const unitOfTime = field.value?.at(-1);
return (
<FormControl
className="relative top-2"
errorText={error?.message}
isError={Boolean(error)}
>
<DatePicker
value={field.value || undefined}
onChange={onChange}
dateFormat="P"
popUpProps={{
open: isStartDatePickerOpen,
onOpenChange: setIsStartDatePickerOpen
}}
popUpContentProps={{}}
/>
</FormControl>
<div className="flex flex-col gap-4">
{RELATIVE_OPTIONS.map(({ label, unit, values }) => (
<div key={unit} className="flex items-center gap-2">
<div className="w-16">{label}</div>
{values.map((v) => {
const value = `${v}${unit}`;
return (
<Button
key={value}
variant="outline_bg"
onClick={() => field.onChange(value)}
className={twMerge(
"h-8 w-12",
field.value === value && "border-primary/40 bg-primary/[0.1]"
)}
>
{v}
</Button>
);
})}
</div>
))}
<div className="flex items-center gap-2">
<FormControl className="mb-0 w-28" label="Duration">
<Input
type="number"
value={duration || "1"}
onChange={(val) => field.onChange(`${val}${unitOfTime}`)}
max={60}
min={1}
/>
</FormControl>
<FormControl className="mb-0 w-36" label="Unit of Time">
<Select
value={unitOfTime}
onValueChange={(val) => field.onChange(`${duration}${val}`)}
className="w-full"
position="popper"
>
{RELATIVE_OPTIONS.map((opt) => (
<SelectItem value={opt.unit}>{opt.label}</SelectItem>
))}
</Select>
</FormControl>
</div>
</div>
);
}}
/>
<div className="flex items-center -space-x-3">
<div className="h-[2px] w-[20px] rounded-full bg-mineshaft-500" />
<FontAwesomeIcon icon={faArrowRight} className="text-mineshaft-500" />
)}
{selectType === AuditLogDateFilterType.Absolute && (
<div className="flex h-10 w-full items-center justify-between gap-2">
<Controller
name="startDate"
control={control}
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
return (
<FormControl
className="relative top-2"
errorText={error?.message}
isError={Boolean(error)}
label="Start Date"
>
<DatePicker
value={field.value || undefined}
onChange={onChange}
timezone={timezone}
dateFormat="P"
buttonClassName="w-44 h-8 font-normal"
popUpProps={{
open: isStartDatePickerOpen,
onOpenChange: setIsStartDatePickerOpen
}}
popUpContentProps={{}}
/>
</FormControl>
);
}}
/>
<FontAwesomeIcon
icon={faChevronRight}
size="xs"
className="mt-6 text-mineshaft-400"
/>
<Controller
name="endDate"
control={control}
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
return (
<FormControl
className="relative top-2"
errorText={error?.message}
isError={Boolean(error)}
label="End Date"
>
<DatePicker
value={field.value || undefined}
onChange={onChange}
dateFormat="P"
buttonClassName="w-44 h-8 font-normal"
timezone={timezone}
popUpProps={{
open: isEndDatePickerOpen,
onOpenChange: setIsEndDatePickerOpen
}}
popUpContentProps={{}}
/>
</FormControl>
);
}}
/>
</div>
<Controller
name="endDate"
control={control}
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
return (
<FormControl
className="relative top-2"
errorText={error?.message}
isError={Boolean(error)}
>
<DatePicker
value={field.value || undefined}
onChange={onChange}
dateFormat="P"
popUpProps={{
open: isEndDatePickerOpen,
onOpenChange: setIsEndDatePickerOpen
}}
popUpContentProps={{}}
/>
</FormControl>
);
}}
/>
)}
<div className="mt-8 w-full justify-end">
<Button
size="sm"
type="submit"
className="h-9 w-24 font-normal"
variant="outline_bg"
isDisabled={!formState.isDirty}
>
Apply
</Button>
</div>
)}
<div className="mt-4">
<Button size="xs" type="submit" isDisabled={!formState.isDirty}>
Apply
</Button>
</div>
</form>
</DropdownMenuContent>
</DropdownMenu>
</form>
</DropdownMenuContent>
</DropdownMenu>
<Select
value={timezone}
onValueChange={(val) => setTimezone(val as Timezone)}
className="w-[10.6rem] border !border-mineshaft-500 !bg-mineshaft-600 capitalize"
dropdownContainerClassName="max-w-none"
position="popper"
dropdownContainerStyle={{
width: "100%"
}}
>
{Object.values(Timezone).map((tz) => (
<SelectItem value={tz} className="capitalize" key={tz}>
{tz} Timezone
</SelectItem>
))}
</Select>
</>
);
};
@@ -300,10 +300,10 @@ export const LogsFilter = ({ presets, setFilter, filter }: Props) => {
onChange(e);
}}
placeholder="All projects"
options={workspacesInOrg.map(({ name, id, defaultProduct }) => ({
options={workspacesInOrg.map(({ name, id, type }) => ({
name,
id,
type: defaultProduct
type
}))}
getOptionValue={(option) => option.id}
getOptionLabel={(option) => option.name}
@@ -1,8 +1,11 @@
import { useEffect, useState } from "react";
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import ms from "ms";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context";
import { Timezone } from "@app/helpers/datetime";
import { withPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks/usePopUp";
@@ -20,10 +23,11 @@ type Props = {
presets?: Presets;
refetchInterval?: number;
showFilters?: boolean;
pageView?: boolean;
};
export const LogsSection = withPermission(
({ presets, refetchInterval, showFilters = true }: Props) => {
({ presets, refetchInterval, showFilters = true, pageView = false }: Props) => {
const { subscription } = useSubscription();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
@@ -31,6 +35,8 @@ export const LogsSection = withPermission(
eventType: presets?.eventType || [],
actor: presets?.actorId
});
const [timezone, setTimezone] = useState<Timezone>(Timezone.Local);
const [dateFilter, setDateFilter] = useState<TAuditLogDateFilterFormData>({
startDate: new Date(Number(new Date()) - ms("1h")),
endDate: new Date(),
@@ -43,10 +49,85 @@ export const LogsSection = withPermission(
handlePopUpOpen("upgradePlan");
}
}, [subscription]);
if (pageView)
return (
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex flex-wrap items-center justify-between gap-y-2">
<div>
<div className="flex items-center gap-1 whitespace-nowrap">
<p className="text-xl font-semibold text-mineshaft-100">Audit History</p>
<a
href="https://infisical.com/docs/documentation/platform/audit-logs"
target="_blank"
rel="noopener noreferrer"
>
<div className="ml-1 mt-[0.1rem] 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>
</div>
<div className="flex flex-wrap items-center gap-2 lg:justify-end">
{showFilters && (
<LogsDateFilter
filter={dateFilter}
setFilter={setDateFilter}
timezone={timezone}
setTimezone={setTimezone}
/>
)}
{showFilters && (
<LogsFilter presets={presets} setFilter={setLogFilter} filter={logFilter} />
)}
</div>
</div>
<div className="space-y-2">
<LogsTable
refetchInterval={refetchInterval}
filter={{
secretPath: logFilter.secretPath || undefined,
secretKey: logFilter.secretKey || undefined,
eventMetadata: logFilter?.eventMetadata,
projectId: logFilter?.project?.id,
actorType: presets?.actorType,
limit: 15,
eventType: logFilter?.eventType,
userAgentType: logFilter?.userAgentType,
startDate: dateFilter?.startDate,
endDate: dateFilter?.endDate,
environment: logFilter?.environment?.slug,
actor: logFilter?.actor
}}
timezone={timezone}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("upgradePlan", isOpen);
}}
text="You can use audit logs if you switch to a paid Infisical plan."
/>
</div>
</div>
);
return (
<div className="space-y-2">
<div className="flex w-full justify-end">
{showFilters && <LogsDateFilter filter={dateFilter} setFilter={setDateFilter} />}
{showFilters && (
<LogsDateFilter
filter={dateFilter}
setFilter={setDateFilter}
timezone={timezone}
setTimezone={setTimezone}
/>
)}
{showFilters && (
<LogsFilter presets={presets} setFilter={setLogFilter} filter={logFilter} />
)}
@@ -67,6 +148,7 @@ export const LogsSection = withPermission(
environment: logFilter?.environment?.slug,
actor: logFilter?.actor
}}
timezone={timezone}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
@@ -1,6 +1,5 @@
import { Fragment } from "react";
import { faFile, faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faFile } from "@fortawesome/free-solid-svg-icons";
import { twMerge } from "tailwind-merge";
import {
@@ -14,9 +13,9 @@ import {
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import { Timezone } from "@app/helpers/datetime";
import { useGetAuditLogs } from "@app/hooks/api";
import { TGetAuditLogsFilter } from "@app/hooks/api/auditLogs/types";
@@ -25,11 +24,12 @@ import { LogsTableRow } from "./LogsTableRow";
type Props = {
filter: TGetAuditLogsFilter;
refetchInterval?: number;
timezone: Timezone;
};
const AUDIT_LOG_LIMIT = 30;
export const LogsTable = ({ filter, refetchInterval }: Props) => {
export const LogsTable = ({ filter, refetchInterval, timezone }: Props) => {
// Determine the project ID for filtering
const filterProjectId =
// Use the projectId from the filter if it exists
@@ -57,16 +57,7 @@ export const LogsTable = ({ filter, refetchInterval }: Props) => {
<Th className="w-24">
<Spinner size="xs" className={twMerge(isPending ? "opacity-100" : "opacity-0")} />
</Th>
<Th className="w-64">
Timestamp
<Tooltip
className="normal-case"
content="Time displayed in your system's time zone."
sideOffset={10}
>
<FontAwesomeIcon icon={faInfoCircle} className="ml-1" />
</Tooltip>
</Th>
<Th className="w-64">Timestamp</Th>
<Th>Event</Th>
</Tr>
</THead>
@@ -79,6 +70,7 @@ export const LogsTable = ({ filter, refetchInterval }: Props) => {
rowNumber={index + i * AUDIT_LOG_LIMIT + 1}
auditLog={auditLog}
key={`audit-log-${auditLog.id}`}
timezone={timezone}
/>
))}
</Fragment>
@@ -96,7 +88,7 @@ export const LogsTable = ({ filter, refetchInterval }: Props) => {
</TableContainer>
{!isEmpty && (
<Button
className="mb-20 mt-4 px-4 py-3 text-sm"
className="mt-4 px-4 py-3 text-sm"
isFullWidth
variant="outline_bg"
isLoading={isFetchingNextPage}
@@ -1,8 +1,8 @@
import { faCaretDown, faCaretRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { Td, Tr } from "@app/components/v2";
import { formatDateTime, Timezone } from "@app/helpers/datetime";
import { useToggle } from "@app/hooks";
import { ActorType } from "@app/hooks/api/auditLogs/enums";
import { AuditLog } from "@app/hooks/api/auditLogs/types";
@@ -10,6 +10,7 @@ import { AuditLog } from "@app/hooks/api/auditLogs/types";
type Props = {
auditLog: AuditLog;
rowNumber: number;
timezone: Timezone;
};
type TagProps = {
@@ -27,7 +28,7 @@ const Tag = ({ label, value }: TagProps) => {
);
};
export const LogsTableRow = ({ auditLog, rowNumber }: Props) => {
export const LogsTableRow = ({ auditLog, rowNumber, timezone }: Props) => {
const [isOpen, setIsOpen] = useToggle();
return (
@@ -46,9 +47,7 @@ export const LogsTableRow = ({ auditLog, rowNumber }: Props) => {
<FontAwesomeIcon icon={isOpen ? faCaretDown : faCaretRight} />
{rowNumber}
</Td>
<Td className="align-top">
{format(new Date(auditLog.createdAt), "MMM do yyyy, hh:mm a")}
</Td>
<Td className="align-top">{formatDateTime({ timestamp: auditLog.createdAt, timezone })}</Td>
<Td>
<div className="flex flex-wrap gap-4 text-sm">
<Tag label="event" value={auditLog.event.type} />
@@ -6,6 +6,7 @@ import { format } from "date-fns";
import { createNotification } from "@app/components/notifications";
import { IconButton, Td, Tooltip, Tr } from "@app/components/v2";
import { getProjectBaseURL } from "@app/helpers/project";
import { formatProjectRoleName } from "@app/helpers/roles";
import { useGetUserWorkspaces } from "@app/hooks/api";
import { IdentityMembership } from "@app/hooks/api/identities/types";
@@ -50,7 +51,7 @@ export const IdentityProjectRow = ({
onClick={() => {
if (isAccessible) {
navigate({
to: "/projects/$projectId/access-management",
to: `${getProjectBaseURL(project.type)}/access-management` as const,
params: {
projectId: project.id
},
@@ -38,7 +38,8 @@ export const ProjectsPage = () => {
"upgradePlan"
] as const);
const { data: serverDetails } = useFetchServerStatus();
const { data: serverDetails, isLoading } = useFetchServerStatus();
const { subscription } = useSubscription();
const isAddingProjectsAllowed = subscription?.workspaceLimit
@@ -51,7 +52,8 @@ export const ProjectsPage = () => {
<title>{t("common.head-title", { title: t("settings.members.title") })}</title>
<link rel="icon" href="/infisical.ico" />
</Helmet>
{!serverDetails?.redisConfigured && (
{!isLoading && !serverDetails?.redisConfigured && (
<div className="mb-4 flex flex-col items-start justify-start text-3xl">
<p className="mb-4 mr-4 font-semibold text-white">Announcements</p>
<div className="flex w-full items-center rounded-md border border-blue-400/70 bg-blue-900/70 p-2 text-base text-mineshaft-100">
@@ -4,6 +4,7 @@ import {
faArrowDownAZ,
faBorderAll,
faCheck,
faCheckCircle,
faFolderOpen,
faList,
faMagnifyingGlass,
@@ -16,10 +17,17 @@ import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Badge,
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
FormControl,
IconButton,
Input,
Lottie,
Modal,
ModalContent,
Pagination,
@@ -27,7 +35,7 @@ import {
Tooltip
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { getProjectHomePage } from "@app/helpers/project";
import { getProjectHomePage, getProjectLottieIcon, getProjectTitle } from "@app/helpers/project";
import {
getUserTablePreference,
PreferenceKey,
@@ -35,7 +43,7 @@ import {
} from "@app/helpers/userTablePreferences";
import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import { useRequestProjectAccess, useSearchProjects } from "@app/hooks/api";
import { Workspace } from "@app/hooks/api/workspace/types";
import { ProjectType, Workspace } from "@app/hooks/api/workspace/types";
type Props = {
onAddNewProject: () => void;
@@ -98,6 +106,7 @@ export const AllProjectView = ({
const navigate = useNavigate();
const [searchFilter, setSearchFilter] = useState("");
const [debouncedSearch] = useDebounce(searchFilter);
const [projectTypeFilter, setProjectTypeFilter] = useState<ProjectType>();
const {
setPage,
perPage,
@@ -124,7 +133,8 @@ export const AllProjectView = ({
limit,
offset,
name: debouncedSearch || undefined,
orderDirection
orderDirection,
type: projectTypeFilter
});
useResetPageHelper({
@@ -134,6 +144,9 @@ export const AllProjectView = ({
});
const requestedWorkspaceDetails = (popUp.requestAccessConfirmation.data || {}) as Workspace;
const handleToggleFilterByProjectType = (el: ProjectType) =>
setProjectTypeFilter((state) => (state === el ? undefined : el));
return (
<div>
<div className="flex w-full flex-row">
@@ -160,16 +173,62 @@ export const AllProjectView = ({
</IconButton>
</Tooltip>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div
className={twMerge(
"ml-2 flex rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1",
projectTypeFilter && "border-primary-400 text-primary-400"
)}
>
<Tooltip content="Choose visible project type" className="mb-2">
<IconButton
ariaLabel="project-types"
className={twMerge(
"min-w-[2.4rem] border-none hover:bg-mineshaft-600",
projectTypeFilter && "text-primary-400"
)}
variant="plain"
size="xs"
colorSchema="secondary"
>
<FontAwesomeIcon icon={faList} />
</IconButton>
</Tooltip>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent className="thin-scrollbar overflow-y-auto" align="end">
<DropdownMenuLabel>Filter By Project Type</DropdownMenuLabel>
{Object.values(ProjectType).map((el) => (
<DropdownMenuItem
key={`filter-item-${el}`}
onClick={(e) => {
e.preventDefault();
handleToggleFilterByProjectType(el);
}}
icon={projectTypeFilter === el && <FontAwesomeIcon icon={faCheckCircle} />}
iconPos="right"
>
<div className="flex items-center gap-2">
<span>{getProjectTitle(el)}</span>
</div>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<div className="ml-2 flex rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
<Tooltip content="Disabled across All Project view.">
<IconButton
variant="outline_bg"
ariaLabel="grid"
size="xs"
className="min-w-[2.4rem] border-none bg-transparent hover:bg-mineshaft-600"
>
<FontAwesomeIcon icon={faBorderAll} />
</IconButton>
<div className="flex cursor-not-allowed items-center justify-center">
<IconButton
variant="outline_bg"
ariaLabel="grid"
size="xs"
isDisabled
className="pointer-events-none min-w-[2.4rem] border-none bg-transparent hover:bg-mineshaft-600"
>
<FontAwesomeIcon icon={faBorderAll} />
</IconButton>
</div>
</Tooltip>
<IconButton
variant="outline_bg"
@@ -184,7 +243,7 @@ export const AllProjectView = ({
{(isAllowed) => (
<Button
isDisabled={!isAllowed}
colorSchema="primary"
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
if (isAddingProjectsAllowed) {
@@ -222,7 +281,7 @@ export const AllProjectView = ({
onKeyDown={(evt) => {
if (evt.key === "Enter" && workspace.isMember) {
navigate({
to: getProjectHomePage(workspace.defaultProduct),
to: getProjectHomePage(workspace.type),
params: {
projectId: workspace.id
}
@@ -232,7 +291,7 @@ export const AllProjectView = ({
onClick={() => {
if (workspace.isMember) {
navigate({
to: getProjectHomePage(workspace.defaultProduct),
to: getProjectHomePage(workspace.type),
params: {
projectId: workspace.id
}
@@ -241,38 +300,41 @@ export const AllProjectView = ({
}}
key={workspace.id}
className={twMerge(
"group flex min-w-72 grid-cols-6 items-center justify-center border-l border-r border-t border-mineshaft-600 bg-mineshaft-800 px-6 py-3 first:rounded-t-md",
"group flex min-w-72 items-center justify-center border-l border-r border-t border-mineshaft-600 bg-mineshaft-800 px-6 py-3 first:rounded-t-md",
workspace.isMember ? "cursor-pointer hover:bg-mineshaft-700" : "cursor-default"
)}
>
<div className="w-full items-center">
<div className="flex flex-grow items-center">
<div className="flex-grow truncate text-sm text-mineshaft-100">
{workspace.name}
</div>
<div className="flex items-center">
{workspace.isMember ? (
<div className="flex items-center text-center text-sm text-primary">
<FontAwesomeIcon icon={faCheck} className="mr-2" />
Joined
</div>
) : (
<div className="opacity-0 transition-all group-hover:opacity-100">
<Button
size="xs"
variant="outline_bg"
onClick={() => handlePopUpOpen("requestAccessConfirmation", workspace)}
>
Request Access
</Button>
</div>
)}
</div>
<div className="mr-3 flex min-w-0 flex-1 items-center gap-3">
<div className="rounded border border-mineshaft-500 bg-mineshaft-600 p-1 shadow-inner">
<Lottie
className="h-[1.35rem] w-[1.35rem] shrink-0"
icon={getProjectLottieIcon(workspace.type)}
/>
</div>
<div className="mt-1 max-w-lg overflow-hidden text-ellipsis whitespace-nowrap text-xs text-mineshaft-300">
{workspace.description}
<div className="-mt-0.5 flex min-w-0 flex-col">
<p className="truncate text-sm text-mineshaft-100">{workspace.name}</p>
<p className="truncate text-xs leading-4 text-mineshaft-300">
{getProjectTitle(workspace.type)}{" "}
{workspace.description ? `- ${workspace.description}` : ""}
</p>
</div>
</div>
{workspace.isMember ? (
<Badge className="flex items-center" variant="success">
<FontAwesomeIcon icon={faCheck} className="mr-1" />
<span>Joined</span>
</Badge>
) : (
<div className="opacity-0 transition-all group-hover:opacity-100">
<Button
size="xs"
variant="outline_bg"
onClick={() => handlePopUpOpen("requestAccessConfirmation", workspace)}
>
Request Access
</Button>
</div>
)}
</div>
))}
</div>
@@ -2,9 +2,9 @@ import { ReactNode, useMemo, useState } from "react";
import { faFolderOpen, faStar } from "@fortawesome/free-regular-svg-icons";
import {
faArrowDownAZ,
faArrowRight,
faArrowUpZA,
faBorderAll,
faCheckCircle,
faList,
faMagnifyingGlass,
faPlus,
@@ -13,12 +13,26 @@ import {
} 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";
import { Button, IconButton, Input, Pagination, Skeleton, Tooltip } from "@app/components/v2";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
IconButton,
Input,
Lottie,
Pagination,
Skeleton,
Tooltip
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { getProjectHomePage } from "@app/helpers/project";
import { getProjectHomePage, getProjectLottieIcon, getProjectTitle } from "@app/helpers/project";
import {
getUserTablePreference,
PreferenceKey,
@@ -29,7 +43,7 @@ import { useGetUserWorkspaces } from "@app/hooks/api";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
import { Workspace } from "@app/hooks/api/workspace/types";
import { ProjectType, Workspace } from "@app/hooks/api/workspace/types";
type Props = {
onAddNewProject: () => void;
@@ -53,6 +67,9 @@ export const MyProjectView = ({
}: Props) => {
const navigate = useNavigate();
const { currentOrg } = useOrganization();
const [projectTypeFilter, setProjectTypeFilter] = useState<Partial<Record<ProjectType, boolean>>>(
{}
);
const { data: workspaces = [], isPending: isWorkspaceLoading } = useGetUserWorkspaces();
const {
@@ -67,6 +84,7 @@ export const MyProjectView = ({
} = usePagination(ProjectOrderBy.Name, {
initPerPage: getUserTablePreference("myProjectsTable", PreferenceKey.PerPage, 24)
});
const isTableFilteredByType = Boolean(Object.values(projectTypeFilter).some((el) => el));
const handlePerPageChange = (newPerPage: number) => {
setPerPage(newPerPage);
@@ -88,13 +106,18 @@ export const MyProjectView = ({
const filteredWorkspaces = useMemo(
() =>
workspaces
.filter((ws) => ws?.name?.toLowerCase().includes(searchFilter.toLowerCase()))
.filter((ws) => {
if (isTableFilteredByType && !projectTypeFilter?.[ws.type]) {
return false;
}
return ws?.name?.toLowerCase().includes(searchFilter.toLowerCase());
})
.sort((a, b) =>
orderDirection === OrderByDirection.ASC
? a.name.toLowerCase().localeCompare(b.name.toLowerCase())
: b.name.toLowerCase().localeCompare(a.name.toLowerCase())
),
[searchFilter, orderDirection, workspaces]
[searchFilter, orderDirection, workspaces, projectTypeFilter]
);
useResetPageHelper({
@@ -117,6 +140,15 @@ export const MyProjectView = ({
};
}, [filteredWorkspaces, projectFavorites, offset, limit, page]);
const handleToggleFilterByProjectType = (type: ProjectType) => {
setProjectTypeFilter((state) => {
return {
...(state || {}),
[type]: !state?.[type]
};
});
};
const addProjectToFavorites = async (projectId: string) => {
try {
if (currentOrg?.id) {
@@ -153,61 +185,53 @@ export const MyProjectView = ({
<div
onClick={() => {
navigate({
to: getProjectHomePage(workspace.defaultProduct),
to: getProjectHomePage(workspace.type),
params: {
projectId: workspace.id
}
});
}}
key={workspace.id}
className="flex h-40 min-w-72 cursor-pointer flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
className="cursor-pointer overflow-clip rounded border border-l-[4px] border-mineshaft-600 border-l-mineshaft-400 bg-mineshaft-800 p-4 transition-transform duration-100 hover:scale-[103%] hover:border-l-primary hover:bg-mineshaft-700"
>
<div className="flex flex-row justify-between">
<div className="mt-0 truncate text-lg text-mineshaft-100">{workspace.name}</div>
{isFavorite ? (
<FontAwesomeIcon
icon={faSolidStar}
className="text-sm text-yellow-600 hover:text-mineshaft-400"
onClick={(e) => {
e.stopPropagation();
removeProjectFromFavorites(workspace.id);
}}
<div className="flex items-center gap-4">
<div className="rounded border border-mineshaft-500 bg-mineshaft-600 p-1.5 shadow-inner">
<Lottie
className="h-[1.75rem] w-[1.75rem] shrink-0"
icon={getProjectLottieIcon(workspace.type)}
/>
) : (
<FontAwesomeIcon
icon={faStar}
className="text-sm text-mineshaft-400 hover:text-mineshaft-300"
onClick={(e) => {
e.stopPropagation();
addProjectToFavorites(workspace.id);
}}
/>
)}
</div>
<div
className="mb-2.5 mt-1 grow text-sm text-mineshaft-300"
style={{
overflow: "hidden",
display: "-webkit-box",
WebkitBoxOrient: "vertical",
WebkitLineClamp: 2
}}
>
{workspace.description}
</div>
<div className="flex w-full flex-row items-end justify-between place-self-end">
<button type="button">
<div className="group ml-auto w-max cursor-pointer rounded-full border border-mineshaft-600 bg-mineshaft-900 px-4 py-2 text-sm text-mineshaft-300 transition-all hover:border-primary-500/80 hover:bg-primary-800/20 hover:text-mineshaft-200">
Explore{" "}
</div>
<div className="min-w-0 flex-1">
<p className="truncate text-lg font-semibold text-mineshaft-100">{workspace.name}</p>
<p className="truncate text-sm leading-4 text-mineshaft-300">
{getProjectTitle(workspace.type)}
</p>
</div>
<div className="mt-0.5 self-start">
{isFavorite ? (
<FontAwesomeIcon
icon={faArrowRight}
className="pl-1.5 pr-0.5 duration-200 hover:pl-2 hover:pr-0"
icon={faSolidStar}
className="text-sm text-yellow-600 hover:text-mineshaft-400"
onClick={(e) => {
e.stopPropagation();
removeProjectFromFavorites(workspace.id);
}}
/>
</div>
</button>
) : (
<FontAwesomeIcon
icon={faStar}
className="text-sm text-mineshaft-400 hover:text-mineshaft-300"
onClick={(e) => {
e.stopPropagation();
addProjectToFavorites(workspace.id);
}}
/>
)}
</div>
</div>
<p className="mt-4 truncate text-sm text-mineshaft-400">
{workspace.description || "No description"}
</p>
</div>
);
const renderProjectListItem = (workspace: Workspace, isFavorite: boolean, index: number) => (
@@ -215,21 +239,33 @@ export const MyProjectView = ({
<div
onClick={() => {
navigate({
to: getProjectHomePage(workspace.defaultProduct),
to: getProjectHomePage(workspace.type),
params: {
projectId: workspace.id
}
});
}}
key={workspace.id}
className={`group grid h-14 min-w-72 cursor-pointer grid-cols-6 border-l border-r border-t border-mineshaft-600 bg-mineshaft-800 px-6 hover:bg-mineshaft-700 ${
className={`group flex min-w-72 cursor-pointer border-l border-r border-t border-mineshaft-600 bg-mineshaft-800 px-6 py-3 hover:bg-mineshaft-700 ${
index === 0 && "rounded-t-md"
}`}
>
<div className="flex items-center sm:col-span-3 lg:col-span-4">
<div className="truncate text-sm text-mineshaft-100">{workspace.name}</div>
<div className="flex min-w-0 flex-1 items-center gap-3">
<div className="rounded border border-mineshaft-500 bg-mineshaft-600 p-1 shadow-inner">
<Lottie
className="h-[1.35rem] w-[1.35rem] shrink-0"
icon={getProjectLottieIcon(workspace.type)}
/>
</div>
<div className="-mt-0.5 flex min-w-0 flex-col">
<p className="truncate text-sm text-mineshaft-100">{workspace.name}</p>
<p className="truncate text-xs leading-4 text-mineshaft-300">
{getProjectTitle(workspace.type)}{" "}
{workspace.description ? `- ${workspace.description}` : ""}
</p>
</div>
</div>
<div className="flex items-center justify-end sm:col-span-3 lg:col-span-2">
<div className="flex items-center justify-end">
{isFavorite ? (
<FontAwesomeIcon
icon={faSolidStar}
@@ -258,7 +294,7 @@ export const MyProjectView = ({
switch (projectsViewMode) {
case ProjectsViewMode.GRID:
projectsComponents = (
<div className="mt-4 grid w-full grid-cols-1 gap-4 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
<div className="mt-4 grid w-full grid-cols-1 gap-5 lg:grid-cols-2 xl:grid-cols-3">
{isProjectViewLoading &&
Array.apply(0, Array(3)).map((_x, i) => (
<div
@@ -320,7 +356,18 @@ export const MyProjectView = ({
<div className="text-center font-light">No projects match search...</div>
</div>
);
} else if (filteredWorkspaces.length === 0 && isTableFilteredByType) {
projectsComponents = (
<div className="mt-4 w-full rounded-md border border-mineshaft-700 bg-mineshaft-800 px-4 py-6 text-base text-mineshaft-300">
<FontAwesomeIcon
icon={faSearch}
className="mb-4 mt-2 w-full text-center text-5xl text-mineshaft-400"
/>
<div className="text-center font-light">No projects match filters...</div>
</div>
);
}
return (
<div>
<div className="flex w-full flex-row">
@@ -351,6 +398,49 @@ export const MyProjectView = ({
</IconButton>
</Tooltip>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div
className={twMerge(
"ml-2 flex rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1",
isTableFilteredByType && "border-primary-400 text-primary-400"
)}
>
<Tooltip content="Choose visible project type" className="mb-2">
<IconButton
ariaLabel="project-types"
className={twMerge(
"min-w-[2.4rem] border-none hover:bg-mineshaft-600",
isTableFilteredByType && "text-primary-400"
)}
variant="plain"
size="xs"
colorSchema="secondary"
>
<FontAwesomeIcon icon={faList} />
</IconButton>
</Tooltip>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent className="thin-scrollbar overflow-y-auto" align="end">
<DropdownMenuLabel>Filter By Project Type</DropdownMenuLabel>
{Object.values(ProjectType).map((el) => (
<DropdownMenuItem
key={`filter-item-${el}`}
onClick={(e) => {
e.preventDefault();
handleToggleFilterByProjectType(el);
}}
icon={projectTypeFilter?.[el] && <FontAwesomeIcon icon={faCheckCircle} />}
iconPos="right"
>
<div className="flex items-center gap-2">
<span>{getProjectTitle(el)}</span>
</div>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<div className="ml-2 flex rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
<IconButton
variant="outline_bg"
@@ -385,7 +475,7 @@ export const MyProjectView = ({
{(isAllowed) => (
<Button
isDisabled={!isAllowed}
colorSchema="primary"
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
if (isAddingProjectsAllowed) {
@@ -417,7 +507,7 @@ export const MyProjectView = ({
onChangePerPage={handlePerPageChange}
/>
)}
{isWorkspaceEmpty && (
{isWorkspaceEmpty && !isTableFilteredByType && (
<div className="mt-4 w-full rounded-md border border-mineshaft-700 bg-mineshaft-800 px-4 py-6 text-base text-mineshaft-300">
<FontAwesomeIcon
icon={faFolderOpen}
@@ -27,7 +27,7 @@ export const ProjectListToggle = ({ value, onChange }: Props) => {
value={value}
onValueChange={onChange}
className="absolute left-0 top-0 h-full w-full cursor-pointer opacity-0"
dropdownContainerClassName="mt-10 ml-5"
position="popper"
>
<SelectItem value={ProjectListView.MyProjects}>My Projects</SelectItem>
<SelectItem value={ProjectListView.AllProjects}>All Projects</SelectItem>
@@ -1,7 +1,8 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { faCopy, faEllipsisV } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
@@ -12,8 +13,7 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
PageHeader,
Tooltip
PageHeader
} from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
@@ -22,7 +22,7 @@ import { usePopUp } from "@app/hooks/usePopUp";
import { DuplicateOrgRoleModal } from "@app/pages/organization/RoleByIDPage/components/DuplicateOrgRoleModal";
import { OrgAccessControlTabSections } from "@app/types/org";
import { RoleDetailsSection, RoleModal, RolePermissionsSection } from "./components";
import { RoleModal, RolePermissionsSection } from "./components";
export const Page = () => {
const navigate = useNavigate();
@@ -80,29 +80,64 @@ export const Page = () => {
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && (
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader title={data.name}>
<PageHeader
title={
<div className="flex flex-col">
<div>
<span>{data.name}</span>
<p className="text-sm font-[400] normal-case leading-3 text-mineshaft-400">
{data.slug} {data.description && `- ${data.description}`}
</p>
</div>
</div>
}
>
{isCustomRole && (
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options">
<Button variant="outline_bg">More</Button>
</Tooltip>
</div>
<DropdownMenuTrigger asChild>
<Button
colorSchema="secondary"
rightIcon={<FontAwesomeIcon icon={faEllipsisV} className="ml-2" />}
>
Options
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<DropdownMenuContent align="end" sideOffset={2} className="p-1">
<DropdownMenuItem
onClick={() => {
navigator.clipboard.writeText(data.id);
createNotification({
text: "Copied ID to clipboard",
type: "info"
});
}}
icon={<FontAwesomeIcon icon={faCopy} />}
>
Copy ID
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
navigator.clipboard.writeText(data.slug);
createNotification({
text: "Copied slug to clipboard",
type: "info"
});
}}
icon={<FontAwesomeIcon icon={faCopy} />}
>
Copy Slug
</DropdownMenuItem>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Role}>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () => {
onClick={() => {
handlePopUpOpen("role", {
roleId
});
}}
disabled={!isAllowed}
isDisabled={!isAllowed}
>
Edit Role
</DropdownMenuItem>
@@ -111,13 +146,10 @@ export const Page = () => {
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Role}>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={() => {
handlePopUpOpen("duplicateRole");
}}
disabled={!isAllowed}
isDisabled={!isAllowed}
>
Duplicate Role
</DropdownMenuItem>
@@ -126,15 +158,10 @@ export const Page = () => {
<OrgPermissionCan I={OrgPermissionActions.Delete} a={OrgPermissionSubjects.Role}>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () => {
onClick={() => {
handlePopUpOpen("deleteOrgRole");
}}
disabled={!isAllowed}
isDisabled={!isAllowed}
>
Delete Role
</DropdownMenuItem>
@@ -144,12 +171,7 @@ export const Page = () => {
</DropdownMenu>
)}
</PageHeader>
<div className="flex">
<div className="mr-4 w-96">
<RoleDetailsSection roleId={roleId} handlePopUpOpen={handlePopUpOpen} />
</div>
<RolePermissionsSection roleId={roleId} />
</div>
<RolePermissionsSection roleId={roleId} />
</div>
)}
<RoleModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
@@ -1,95 +0,0 @@
import { faCheck, faCopy, faPencil } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions";
import { IconButton, Tooltip } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useTimedReset } from "@app/hooks";
import { useGetOrgRole } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
roleId: string;
handlePopUpOpen: (popUpName: keyof UsePopUpState<["role"]>, data?: object) => void;
};
export const RoleDetailsSection = ({ roleId, handlePopUpOpen }: Props) => {
const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset<string>({
initialState: "Copy ID to clipboard"
});
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { data } = useGetOrgRole(orgId, roleId);
const isCustomRole = !["admin", "member", "no-access"].includes(data?.slug ?? "");
return data ? (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Org Role Details</h3>
{isCustomRole && (
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Role}>
{(isAllowed) => {
return (
<Tooltip content="Edit Role">
<IconButton
isDisabled={!isAllowed}
ariaLabel="copy icon"
variant="plain"
className="group relative"
onClick={() =>
handlePopUpOpen("role", {
roleId
})
}
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
</Tooltip>
);
}}
</OrgPermissionCan>
)}
</div>
<div className="pt-4">
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Role ID</p>
<div className="group flex align-top">
<p className="text-sm text-mineshaft-300">{roleId}</p>
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<Tooltip content={copyTextId}>
<IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative ml-2"
onClick={() => {
navigator.clipboard.writeText(roleId);
setCopyTextId("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingId ? faCheck : faCopy} />
</IconButton>
</Tooltip>
</div>
</div>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Name</p>
<p className="text-sm text-mineshaft-300">{data.name}</p>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Slug</p>
<p className="text-sm text-mineshaft-300">{data.slug}</p>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Description</p>
<p className="text-sm text-mineshaft-300">
{data.description?.length ? data.description : "-"}
</p>
</div>
</div>
</div>
) : (
<div />
);
};
@@ -76,17 +76,18 @@ export const OrgPermissionAdminConsoleRow = ({ isEditable, control, setValue }:
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => setIsRowExpanded.toggle()}
>
<Td>
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
<Td className="w-4">
<FontAwesomeIcon className="w-4" icon={isRowExpanded ? faChevronDown : faChevronRight} />
</Td>
<Td>Organization Admin Console</Td>
<Td className="w-full select-none">Organization Admin Console</Td>
<Td>
<Select
value={selectedPermissionCategory}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
className="h-8 w-40 bg-mineshaft-700"
dropdownContainerClassName="border text-left border-mineshaft-600 bg-mineshaft-800"
onValueChange={handlePermissionChange}
isDisabled={!isEditable}
position="popper"
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.Custom}>Custom</SelectItem>
@@ -95,11 +96,8 @@ export const OrgPermissionAdminConsoleRow = ({ isEditable, control, setValue }:
</Tr>
{isRowExpanded && (
<Tr>
<Td
colSpan={3}
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && "border-mineshaft-500 p-8"}`}
>
<div className="grid grid-cols-3 gap-4">
<Td colSpan={3} className="border-mineshaft-500 bg-mineshaft-900 p-8">
<div className="flex flex-grow flex-wrap justify-start gap-x-8 gap-y-4">
{PERMISSION_ACTIONS.map(({ action, label }) => {
return (
<Controller
@@ -124,17 +124,18 @@ export const OrgPermissionAppConnectionRow = ({ isEditable, control, setValue }:
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => setIsRowExpanded.toggle()}
>
<Td>
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
<Td className="w-4">
<FontAwesomeIcon className="w-4" icon={isRowExpanded ? faChevronDown : faChevronRight} />
</Td>
<Td>App Connections</Td>
<Td className="w-full select-none">App Connections</Td>
<Td>
<Select
value={selectedPermissionCategory}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
className="h-8 w-40 bg-mineshaft-700"
dropdownContainerClassName="border text-left border-mineshaft-600 bg-mineshaft-800"
onValueChange={handlePermissionChange}
isDisabled={!isEditable}
position="popper"
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
@@ -145,11 +146,8 @@ export const OrgPermissionAppConnectionRow = ({ isEditable, control, setValue }:
</Tr>
{isRowExpanded && (
<Tr>
<Td
colSpan={3}
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && "border-mineshaft-500 p-8"}`}
>
<div className="grid grid-cols-3 gap-4">
<Td colSpan={3} className="border-mineshaft-500 bg-mineshaft-900 p-8">
<div className="flex flex-grow flex-wrap justify-start gap-x-8 gap-y-4">
{PERMISSION_ACTIONS.map(({ action, label }) => {
return (
<Controller
@@ -112,17 +112,18 @@ export const OrgPermissionBillingRow = ({ isEditable, control, setValue }: Props
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => setIsRowExpanded.toggle()}
>
<Td>
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
<Td className="w-4">
<FontAwesomeIcon className="w-4" icon={isRowExpanded ? faChevronDown : faChevronRight} />
</Td>
<Td>Billing</Td>
<Td className="w-full select-none">Billing</Td>
<Td>
<Select
value={selectedPermissionCategory}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
className="h-8 w-40 bg-mineshaft-700"
dropdownContainerClassName="border text-left border-mineshaft-600 bg-mineshaft-800"
onValueChange={handlePermissionChange}
isDisabled={!isEditable}
position="popper"
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
@@ -133,11 +134,8 @@ export const OrgPermissionBillingRow = ({ isEditable, control, setValue }: Props
</Tr>
{isRowExpanded && (
<Tr>
<Td
colSpan={3}
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && "border-mineshaft-500 p-8"}`}
>
<div className="grid grid-cols-3 gap-4">
<Td colSpan={3} className="border-mineshaft-500 bg-mineshaft-900 p-8">
<div className="flex flex-grow flex-wrap justify-start gap-x-8 gap-y-4">
{PERMISSION_ACTIONS.map(({ action, label }) => {
return (
<Controller
@@ -121,17 +121,18 @@ export const OrgGatewayPermissionRow = ({ isEditable, control, setValue }: Props
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => setIsRowExpanded.toggle()}
>
<Td>
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
<Td className="w-4">
<FontAwesomeIcon className="w-4" icon={isRowExpanded ? faChevronDown : faChevronRight} />
</Td>
<Td>Gateways</Td>
<Td className="w-full select-none">Gateways</Td>
<Td>
<Select
value={selectedPermissionCategory}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
className="h-8 w-40 bg-mineshaft-700"
dropdownContainerClassName="border text-left border-mineshaft-600 bg-mineshaft-800"
onValueChange={handlePermissionChange}
isDisabled={!isEditable}
position="popper"
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
@@ -142,11 +143,8 @@ export const OrgGatewayPermissionRow = ({ isEditable, control, setValue }: Props
</Tr>
{isRowExpanded && (
<Tr>
<Td
colSpan={3}
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && "border-mineshaft-500 p-8"}`}
>
<div className="grid grid-cols-3 gap-4">
<Td colSpan={3} className="border-mineshaft-500 bg-mineshaft-900 p-8">
<div className="flex flex-grow flex-wrap justify-start gap-x-8 gap-y-4">
{PERMISSION_ACTIONS.map(({ action, label }) => {
return (
<Controller
@@ -145,17 +145,18 @@ export const OrgPermissionGroupRow = ({ isEditable, control, setValue }: Props)
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => setIsRowExpanded.toggle()}
>
<Td>
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
<Td className="w-4">
<FontAwesomeIcon className="w-4" icon={isRowExpanded ? faChevronDown : faChevronRight} />
</Td>
<Td>Group Management</Td>
<Td className="w-full select-none">Group Management</Td>
<Td>
<Select
value={selectedPermissionCategory}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
className="h-8 w-40 bg-mineshaft-700"
dropdownContainerClassName="border text-left border-mineshaft-600 bg-mineshaft-800"
onValueChange={handlePermissionChange}
isDisabled={!isEditable}
position="popper"
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
@@ -166,11 +167,8 @@ export const OrgPermissionGroupRow = ({ isEditable, control, setValue }: Props)
</Tr>
{isRowExpanded && (
<Tr>
<Td
colSpan={3}
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && "border-mineshaft-500 p-8"}`}
>
<div className="grid grid-cols-3 gap-4">
<Td colSpan={3} className="border-mineshaft-500 bg-mineshaft-900 p-8">
<div className="flex flex-grow flex-wrap justify-start gap-x-8 gap-y-4">
{PERMISSION_ACTIONS.map(({ action, label }) => {
return (
<Controller
@@ -155,17 +155,18 @@ export const OrgPermissionIdentityRow = ({ isEditable, control, setValue }: Prop
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => setIsRowExpanded.toggle()}
>
<Td>
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
<Td className="w-4">
<FontAwesomeIcon className="w-4" icon={isRowExpanded ? faChevronDown : faChevronRight} />
</Td>
<Td>Machine Identity Management</Td>
<Td className="w-full select-none">Machine Identity Management</Td>
<Td>
<Select
value={selectedPermissionCategory}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
className="h-8 w-40 bg-mineshaft-700"
dropdownContainerClassName="border text-left border-mineshaft-600 bg-mineshaft-800"
onValueChange={handlePermissionChange}
isDisabled={!isEditable}
position="popper"
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
@@ -176,11 +177,8 @@ export const OrgPermissionIdentityRow = ({ isEditable, control, setValue }: Prop
</Tr>
{isRowExpanded && (
<Tr>
<Td
colSpan={3}
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && "border-mineshaft-500 p-8"}`}
>
<div className="grid grid-cols-3 gap-4">
<Td colSpan={3} className="border-mineshaft-500 bg-mineshaft-900 p-8">
<div className="flex flex-grow flex-wrap justify-start gap-x-8 gap-y-4">
{PERMISSION_ACTIONS.map(({ action, label }) => {
return (
<Controller
@@ -73,17 +73,18 @@ export const OrgPermissionKmipRow = ({ isEditable, control, setValue }: Props) =
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => setIsRowExpanded.toggle()}
>
<Td>
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
<Td className="w-4">
<FontAwesomeIcon className="w-4" icon={isRowExpanded ? faChevronDown : faChevronRight} />
</Td>
<Td>KMIP</Td>
<Td className="w-full select-none">KMIP</Td>
<Td>
<Select
value={selectedPermissionCategory}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
className="h-8 w-40 bg-mineshaft-700"
dropdownContainerClassName="border text-left border-mineshaft-600 bg-mineshaft-800"
onValueChange={handlePermissionChange}
isDisabled={!isEditable}
position="popper"
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.Custom}>Custom</SelectItem>
@@ -92,11 +93,8 @@ export const OrgPermissionKmipRow = ({ isEditable, control, setValue }: Props) =
</Tr>
{isRowExpanded && (
<Tr>
<Td
colSpan={3}
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && "border-mineshaft-500 p-8"}`}
>
<div className="grid grid-cols-3 gap-4">
<Td colSpan={3} className="border-mineshaft-500 bg-mineshaft-900 p-8">
<div className="flex flex-grow flex-wrap justify-start gap-x-8 gap-y-4">
{PERMISSION_ACTIONS.map(({ action, label }) => {
return (
<Controller
@@ -70,17 +70,18 @@ export const OrgPermissionSecretShareRow = ({ isEditable, control, setValue }: P
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => setIsRowExpanded.toggle()}
>
<Td>
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
<Td className="w-4">
<FontAwesomeIcon className="w-4" icon={isRowExpanded ? faChevronDown : faChevronRight} />
</Td>
<Td>Secret Share</Td>
<Td className="w-full select-none">Secret Share</Td>
<Td>
<Select
value={selectedPermissionCategory}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
className="h-8 w-40 bg-mineshaft-700"
dropdownContainerClassName="border text-left border-mineshaft-600 bg-mineshaft-800"
onValueChange={handlePermissionChange}
isDisabled={!isEditable}
position="popper"
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.Custom}>Custom</SelectItem>
@@ -89,11 +90,8 @@ export const OrgPermissionSecretShareRow = ({ isEditable, control, setValue }: P
</Tr>
{isRowExpanded && (
<Tr>
<Td
colSpan={3}
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && "border-mineshaft-500 p-8"}`}
>
<div className="grid grid-cols-3 gap-4">
<Td colSpan={3} className="border-mineshaft-500 bg-mineshaft-900 p-8">
<div className="flex flex-grow flex-wrap justify-start gap-x-8 gap-y-4">
{PERMISSION_ACTIONS.map(({ action, label }) => {
return (
<Controller
@@ -70,17 +70,18 @@ export const OrgRoleWorkspaceRow = ({ isEditable, control, setValue }: Props) =>
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => setIsRowExpanded.toggle()}
>
<Td>
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
<Td className="w-4">
<FontAwesomeIcon className="w-4" icon={isRowExpanded ? faChevronDown : faChevronRight} />
</Td>
<Td>Project</Td>
<Td className="w-full select-none">Project</Td>
<Td>
<Select
value={selectedPermissionCategory}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
className="h-8 w-40 bg-mineshaft-700"
dropdownContainerClassName="border text-left border-mineshaft-600 bg-mineshaft-800"
onValueChange={handlePermissionChange}
isDisabled={!isEditable}
position="popper"
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.Custom}>Custom</SelectItem>
@@ -89,11 +90,8 @@ export const OrgRoleWorkspaceRow = ({ isEditable, control, setValue }: Props) =>
</Tr>
{isRowExpanded && (
<Tr>
<Td
colSpan={3}
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && "border-mineshaft-500 p-8"}`}
>
<div className="grid grid-cols-3 gap-4">
<Td colSpan={3} className="border-mineshaft-500 bg-mineshaft-900 p-8">
<div className="flex flex-grow flex-wrap justify-start gap-x-8 gap-y-4">
{PERMISSION_ACTIONS.map(({ action, label }) => {
return (
<Controller
@@ -158,17 +158,18 @@ export const RolePermissionRow = ({ isEditable, title, formName, control, setVal
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => setIsRowExpanded.toggle()}
>
<Td>
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
<Td className="w-4">
<FontAwesomeIcon className="w-4" icon={isRowExpanded ? faChevronDown : faChevronRight} />
</Td>
<Td>{title}</Td>
<Td className="w-full select-none">{title}</Td>
<Td>
<Select
value={selectedPermissionCategory}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
className="h-8 w-40 bg-mineshaft-700"
dropdownContainerClassName="border text-left border-mineshaft-600 bg-mineshaft-800"
onValueChange={handlePermissionChange}
isDisabled={!isEditable}
position="popper"
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
@@ -179,11 +180,8 @@ export const RolePermissionRow = ({ isEditable, title, formName, control, setVal
</Tr>
{isRowExpanded && (
<Tr>
<Td
colSpan={3}
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && "border-mineshaft-500 p-8"}`}
>
<div className="grid grid-cols-3 gap-4">
<Td colSpan={3} className="border-mineshaft-500 bg-mineshaft-900 p-8">
<div className="flex flex-grow flex-wrap justify-start gap-x-8 gap-y-4">
{getPermissionList(formName).map(({ action, label }) => {
return (
<Controller
@@ -1,8 +1,10 @@
import { useForm } from "react-hook-form";
import { faSave } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { createNotification } from "@app/components/notifications";
import { Button, Table, TableContainer, TBody, Th, THead, Tr } from "@app/components/v2";
import { Button, Table, TableContainer, TBody } from "@app/components/v2";
import { OrgPermissionSubjects, useOrganization } from "@app/context";
import { useGetOrgRole, useUpdateOrgRole } from "@app/hooks/api";
import { OrgPermissionAppConnectionRow } from "@app/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionAppConnectionRow";
@@ -117,39 +119,39 @@ export const RolePermissionsSection = ({ roleId }: Props) => {
className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
>
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Permissions</h3>
<div>
<h3 className="text-lg font-semibold text-mineshaft-100">Policies</h3>
<p className="text-sm leading-3 text-mineshaft-400">Configure granular access policies</p>
</div>
{isCustomRole && (
<div className="flex items-center">
{isDirty && (
<Button
className="mr-4 text-mineshaft-300"
variant="link"
isDisabled={isSubmitting || !isDirty}
isLoading={isSubmitting}
onClick={() => reset()}
>
Discard
</Button>
)}
<Button
colorSchema="primary"
colorSchema="secondary"
type="submit"
className="h-10 border"
leftIcon={<FontAwesomeIcon icon={faSave} />}
isDisabled={isSubmitting || !isDirty}
isLoading={isSubmitting}
>
Save
</Button>
<Button
className="ml-4 text-mineshaft-300"
variant="link"
isDisabled={isSubmitting || !isDirty}
isLoading={isSubmitting}
onClick={() => reset()}
>
Cancel
</Button>
</div>
)}
</div>
<div className="py-4">
<TableContainer>
<Table>
<THead>
<Tr>
<Th className="w-5" />
<Th>Resource</Th>
<Th>Permission</Th>
</Tr>
</THead>
<TBody>
{SIMPLE_PERMISSION_OPTIONS.map((permission) => {
return (
@@ -1,3 +1,2 @@
export { RoleDetailsSection } from "./RoleDetailsSection";
export { RoleModal } from "./RoleModal";
export { RolePermissionsSection } from "./RolePermissionsSection";
@@ -24,6 +24,7 @@ export const EditProjectTemplateSection = ({ template, onBack }: Props) => {
initialData: template,
enabled: !isInfisicalTemplate
});
const finalTemplate = isInfisicalTemplate ? template : projectTemplate;
return (
<div>
@@ -41,10 +42,10 @@ export const EditProjectTemplateSection = ({ template, onBack }: Props) => {
<div className="flex h-[60vh] w-full items-center justify-center p-24">
<Spinner />
</div>
) : projectTemplate ? (
) : finalTemplate ? (
<EditProjectTemplate
isInfisicalTemplate={isInfisicalTemplate}
projectTemplate={projectTemplate}
projectTemplate={finalTemplate}
onBack={onBack}
/>
) : (
@@ -5,8 +5,10 @@ import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { getProjectTitle } from "@app/helpers/project";
import { usePopUp } from "@app/hooks";
import { TProjectTemplate, useDeleteProjectTemplate } from "@app/hooks/api/projectTemplates";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { ProjectTemplateDetailsModal } from "../../ProjectTemplateDetailsModal";
import { ProjectTemplateEnvironmentsForm } from "./ProjectTemplateEnvironmentsForm";
@@ -24,7 +26,7 @@ export const EditProjectTemplate = ({ isInfisicalTemplate, projectTemplate, onBa
"editDetails"
] as const);
const { id: templateId, name, description } = projectTemplate;
const { id: templateId, name, description, type } = projectTemplate;
const deleteProjectTemplate = useDeleteProjectTemplate();
@@ -53,7 +55,10 @@ export const EditProjectTemplate = ({ isInfisicalTemplate, projectTemplate, onBa
<div className="mb-4 flex items-start justify-between border-b border-bunker-400 pb-4">
<div className="flex-col">
<h3 className="text-xl font-semibold">{name}</h3>
<h2 className="text-sm text-mineshaft-400">{description || "Project Template"}</h2>
<h2 className="text-sm text-mineshaft-400">
{`${getProjectTitle(type)} - `}
{description || "Project Template"}
</h2>
</div>
{!isInfisicalTemplate && (
<div className="flex gap-2">
@@ -94,10 +99,12 @@ export const EditProjectTemplate = ({ isInfisicalTemplate, projectTemplate, onBa
</div>
)}
</div>
<ProjectTemplateEnvironmentsForm
isInfisicalTemplate={isInfisicalTemplate}
projectTemplate={projectTemplate}
/>
{type === ProjectType.SecretManager && (
<ProjectTemplateEnvironmentsForm
isInfisicalTemplate={isInfisicalTemplate}
projectTemplate={projectTemplate}
/>
)}
<ProjectTemplateRolesSection
isInfisicalTemplate={isInfisicalTemplate}
projectTemplate={projectTemplate}
@@ -126,7 +126,7 @@ export const ProjectTemplateEditRoleForm = ({
>
Save
</Button>
<AddPoliciesButton isDisabled={isDisabled} />
<AddPoliciesButton isDisabled={isDisabled} projectType={projectTemplate.type} />
</div>
</div>
)}
@@ -1,5 +1,6 @@
import { useForm } from "react-hook-form";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { twMerge } from "tailwind-merge";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
@@ -7,21 +8,25 @@ import {
Button,
FormControl,
Input,
Lottie,
Modal,
ModalClose,
ModalContent,
TextArea
} from "@app/components/v2";
import { getProjectLottieIcon } from "@app/helpers/project";
import {
TProjectTemplate,
useCreateProjectTemplate,
useUpdateProjectTemplate
} from "@app/hooks/api/projectTemplates";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { slugSchema } from "@app/lib/schemas";
const formSchema = z.object({
name: slugSchema({ min: 1, max: 64, field: "Name" }),
description: z.string().max(500).optional()
description: z.string().max(500).optional(),
type: z.nativeEnum(ProjectType).optional()
});
export type FormData = z.infer<typeof formSchema>;
@@ -38,6 +43,29 @@ type FormProps = {
onComplete: (template: TProjectTemplate) => void;
};
const PROJECT_TYPE_MENU_ITEMS = [
{
label: "Secrets Management",
value: ProjectType.SecretManager
},
{
label: "Certificates Management",
value: ProjectType.CertificateManager
},
{
label: "KMS",
value: ProjectType.KMS
},
{
label: "SSH",
value: ProjectType.SSH
},
{
label: "Secret Scanning",
value: ProjectType.SecretScanning
}
];
const ProjectTemplateForm = ({ onComplete, projectTemplate }: FormProps) => {
const createProjectTemplate = useCreateProjectTemplate();
const updateProjectTemplate = useUpdateProjectTemplate();
@@ -45,12 +73,14 @@ const ProjectTemplateForm = ({ onComplete, projectTemplate }: FormProps) => {
const {
handleSubmit,
register,
control,
formState: { isSubmitting, errors }
} = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
name: projectTemplate?.name,
description: projectTemplate?.description
description: projectTemplate?.description,
type: ProjectType.SecretManager
}
});
@@ -90,6 +120,42 @@ const ProjectTemplateForm = ({ onComplete, projectTemplate }: FormProps) => {
>
<Input autoFocus placeholder="my-project-template" {...register("name")} />
</FormControl>
<Controller
control={control}
name="type"
defaultValue={ProjectType.SecretManager}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Project Type"
isError={Boolean(error)}
errorText={error?.message}
className="flex-1"
>
<div className="mt-2 grid grid-cols-5 gap-4">
{PROJECT_TYPE_MENU_ITEMS.map((el) => (
<div
key={el.value}
className={twMerge(
"flex cursor-pointer flex-col items-center gap-2 rounded border border-mineshaft-600 p-4 opacity-75 transition-all hover:border-primary-400 hover:bg-mineshaft-600",
field.value === el.value && "border-primary-400 bg-mineshaft-600 opacity-100"
)}
onClick={() => field.onChange(el.value)}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter") {
field.onChange(el.value);
}
}}
>
<Lottie icon={getProjectLottieIcon(el.value)} className="h-8 w-8" />
<div className="text-center text-xs">{el.label}</div>
</div>
))}
</div>
</FormControl>
)}
/>
<FormControl
label="Description (optional)"
errorText={errors.description?.message}
@@ -23,6 +23,7 @@ import {
Tr
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context";
import { getProjectTitle } from "@app/helpers/project";
import { usePopUp } from "@app/hooks";
import { TProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates";
@@ -66,8 +67,8 @@ export const ProjectTemplatesTable = ({ onEdit }: Props) => {
<THead>
<Tr>
<Th>Name</Th>
<Th>Type</Th>
<Th>Roles</Th>
<Th>Environments</Th>
<Th />
</Tr>
</THead>
@@ -80,7 +81,7 @@ export const ProjectTemplatesTable = ({ onEdit }: Props) => {
/>
)}
{filteredTemplates.map((template) => {
const { id, name, roles, environments = [], description } = template;
const { id, name, roles, description, type } = template;
return (
<Tr
onClick={() => onEdit(template)}
@@ -99,6 +100,7 @@ export const ProjectTemplatesTable = ({ onEdit }: Props) => {
</Tooltip>
)}
</Td>
<Td>{getProjectTitle(type)}</Td>
<Td className="pl-8">
{roles.length}
{roles.length > 0 && (
@@ -119,26 +121,6 @@ export const ProjectTemplatesTable = ({ onEdit }: Props) => {
</Tooltip>
)}
</Td>
<Td className="pl-14">
{environments?.length || 0}
{environments?.length && (
<Tooltip
content={
<ul className="ml-2 list-disc">
{environments
?.sort((a, b) => (a.position > b.position ? 1 : -1))
.map((env) => <li key={env.slug}>{env.name}</li>)}
</ul>
}
>
<FontAwesomeIcon
size="sm"
className="ml-2 text-mineshaft-400"
icon={faCircleInfo}
/>
</Tooltip>
)}
</Td>
<Td className="w-5">
{name !== "default" && (
<OrgPermissionCan
@@ -5,6 +5,7 @@ import { useNavigate } from "@tanstack/react-router";
import { createNotification } from "@app/components/notifications";
import { IconButton, Td, Tooltip, Tr } from "@app/components/v2";
import { getProjectBaseURL } from "@app/helpers/project";
import { formatProjectRoleName } from "@app/helpers/roles";
import { useGetUserWorkspaces } from "@app/hooks/api";
import { TWorkspaceUser } from "@app/hooks/api/types";
@@ -43,7 +44,7 @@ export const UserProjectRow = ({
onClick={() => {
if (isAccessible) {
navigate({
to: "/projects/$projectId/access-management",
to: `${getProjectBaseURL(project.type)}/access-management` as const,
params: {
projectId: project.id
},