Merge remote-tracking branch 'origin' into ssh-non-interactive

This commit is contained in:
Tuan Dang
2025-04-25 09:31:39 -07:00
272 changed files with 4532 additions and 772 deletions
@@ -0,0 +1,29 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { ProjectPermissionCan } from "@app/components/permissions";
import { PageHeader } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { PkiAlertsSection } from "./components";
export const AlertingPage = () => {
const { t } = useTranslation();
return (
<div className="container mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
<Helmet>
<title>{t("common.head-title", { title: "Alerting" })}</title>
</Helmet>
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader title="Alerting" />
<ProjectPermissionCan
renderGuardBanner
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.PkiAlerts}
>
<PkiAlertsSection />
</ProjectPermissionCan>
</div>
</div>
);
};
@@ -0,0 +1,19 @@
import { createFileRoute } from "@tanstack/react-router";
import { AlertingPage } from "./AlertingPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/alerting"
)({
component: AlertingPage,
beforeLoad: ({ context }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Alerting"
}
]
};
}
});
@@ -20,9 +20,9 @@ import { useDeleteCa, useGetCaById } from "@app/hooks/api";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { usePopUp } from "@app/hooks/usePopUp";
import { CaInstallCertModal } from "../CertificatesPage/components/CaTab/components/CaInstallCertModal";
import { CaModal } from "../CertificatesPage/components/CaTab/components/CaModal";
import { CertificateTemplatesSection } from "../CertificatesPage/components/CertificatesTab/components/CertificateTemplatesSection";
import { CaInstallCertModal } from "../CertificateAuthoritiesPage/components/CaInstallCertModal";
import { CaModal } from "../CertificateAuthoritiesPage/components/CaModal";
import { CertificateTemplatesSection } from "../CertificatesPage/components/CertificateTemplatesSection";
import {
CaCertificatesSection,
CaCrlsSection,
@@ -13,7 +13,7 @@ export const Route = createFileRoute(
{
label: "Certificate Authorities",
link: linkOptions({
to: "/cert-manager/$projectId/overview",
to: "/cert-manager/$projectId/certificate-authorities",
params: {
projectId: params.projectId
}
@@ -0,0 +1,29 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { ProjectPermissionCan } from "@app/components/permissions";
import { PageHeader } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { CaSection } from "./components";
export const CertificateAuthoritiesPage = () => {
const { t } = useTranslation();
return (
<div className="container mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
<Helmet>
<title>{t("common.head-title", { title: "Certificate Authorities" })}</title>
</Helmet>
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader title="Certificate Authorities" />
<ProjectPermissionCan
renderGuardBanner
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.CertificateAuthorities}
>
<CaSection />
</ProjectPermissionCan>
</div>
</div>
);
};
@@ -2,7 +2,7 @@ import { Modal, ModalContent } from "@app/components/v2";
import { useGetCaCert } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { CertificateContent } from "../../CertificatesTab/components/CertificateContent";
import { CertificateContent } from "../../CertificatesPage/components/CertificateContent";
type Props = {
popUp: UsePopUpState<["caCert"]>;
@@ -0,0 +1,19 @@
import { createFileRoute } from "@tanstack/react-router";
import { CertificateAuthoritiesPage } from "./CertificateAuthoritiesPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificate-authorities"
)({
component: CertificateAuthoritiesPage,
beforeLoad: ({ context }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Certificate Authorities"
}
]
};
}
});
@@ -2,63 +2,49 @@ import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { ProjectPermissionCan } from "@app/components/permissions";
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { PageHeader } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
import { CaTab, CertificatesTab, PkiAlertsTab } from "./components";
enum TabSections {
Ca = "certificate-authorities",
Certificates = "certificates",
Alerting = "alerting"
}
import { PkiCollectionSection } from "../AlertingPage/components";
import { CertificatesSection } from "./components";
export const CertificatesPage = () => {
const { t } = useTranslation();
const { permission } = useProjectPermission();
const canAccessPkiColl = permission.can(
ProjectPermissionActions.Read,
ProjectPermissionSub.PkiCollections
);
const canAccessCerts = permission.can(
ProjectPermissionActions.Read,
ProjectPermissionSub.Certificates
);
return (
<div className="container mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
<Helmet>
<title>{t("common.head-title", { title: "Certificates" })}</title>
</Helmet>
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader title="Overview" />
<Tabs defaultValue={TabSections.Certificates}>
<TabList>
<Tab value={TabSections.Certificates}>Certificates</Tab>
<Tab value={TabSections.Ca}>Certificate Authorities</Tab>
<Tab value={TabSections.Alerting}>Alerting</Tab>
</TabList>
<TabPanel value={TabSections.Certificates}>
<ProjectPermissionCan
renderGuardBanner
passThrough={false}
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.Certificates}
>
<CertificatesTab />
</ProjectPermissionCan>
</TabPanel>
<TabPanel value={TabSections.Ca}>
<ProjectPermissionCan
renderGuardBanner
passThrough={false}
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.CertificateAuthorities}
>
<CaTab />
</ProjectPermissionCan>
</TabPanel>
<TabPanel value={TabSections.Alerting}>
<ProjectPermissionCan
renderGuardBanner
passThrough={false}
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.PkiAlerts}
>
<PkiAlertsTab />
</ProjectPermissionCan>
</TabPanel>
</Tabs>
<PageHeader title="Certificates" />
{/* If both are false, the section does not render. This is to prevent duplicate banners. */}
{(canAccessCerts || canAccessPkiColl) && (
<ProjectPermissionCan
renderGuardBanner
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.PkiCollections}
>
<PkiCollectionSection />
</ProjectPermissionCan>
)}
<ProjectPermissionCan
renderGuardBanner
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.Certificates}
>
<CertificatesSection />
</ProjectPermissionCan>
</div>
</div>
);
@@ -1,17 +0,0 @@
import { motion } from "framer-motion";
import { CaSection } from "./components";
export const CaTab = () => {
return (
<motion.div
key="panel-certificate-authorities"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<CaSection />
</motion.div>
);
};
@@ -1 +0,0 @@
export { CaTab } from "./CaTab";
@@ -1,21 +0,0 @@
import { motion } from "framer-motion";
import { PkiCollectionSection } from "../PkiAlertsTab/components";
// import { CertificateTemplatesSection } from "./components/CertificateTemplatesSection";
import { CertificatesSection } from "./components";
export const CertificatesTab = () => {
return (
<motion.div
key="panel-certificates"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<PkiCollectionSection />
{/* <CertificateTemplatesSection /> */}
<CertificatesSection />
</motion.div>
);
};
@@ -1 +0,0 @@
export { CertificatesSection } from "./CertificatesSection";
@@ -1 +0,0 @@
export { CertificatesTab } from "./CertificatesTab";
@@ -1,17 +0,0 @@
import { motion } from "framer-motion";
import { PkiAlertsSection } from "./components";
export const PkiAlertsTab = () => {
return (
<motion.div
key="panel-alerts"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<PkiAlertsSection />
</motion.div>
);
};
@@ -1 +0,0 @@
export { PkiAlertsTab } from "./PkiAlertsTab";
@@ -1,3 +1 @@
export { CaTab } from "./CaTab";
export { CertificatesTab } from "./CertificatesTab";
export { PkiAlertsTab } from "./PkiAlertsTab";
export { CertificatesSection } from "./CertificatesSection";
@@ -22,7 +22,7 @@ import { PkiItemType } from "@app/hooks/api/pkiCollections/constants";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { usePopUp } from "@app/hooks/usePopUp";
import { PkiCollectionModal } from "../CertificatesPage/components/PkiAlertsTab/components/PkiCollectionModal";
import { PkiCollectionModal } from "../AlertingPage/components/PkiCollectionModal";
import { PkiCollectionDetailsSection, PkiCollectionItemsSection } from "./components";
export const PkiCollectionPage = () => {
@@ -1,6 +1,6 @@
import { Modal, ModalContent } from "@app/components/v2";
import { KmipClientCertificate } from "@app/hooks/api/kmip/types";
import { CertificateContent } from "@app/pages/cert-manager/CertificatesPage/components/CertificatesTab/components/CertificateContent";
import { CertificateContent } from "@app/pages/cert-manager/CertificatesPage/components/CertificateContent";
type Props = {
isOpen: boolean;
@@ -22,6 +22,7 @@ import { MsSqlConnectionForm } from "./MsSqlConnectionForm";
import { PostgresConnectionForm } from "./PostgresConnectionForm";
import { TerraformCloudConnectionForm } from "./TerraformCloudConnectionForm";
import { VercelConnectionForm } from "./VercelConnectionForm";
import { WindmillConnectionForm } from "./WindmillConnectionForm";
type FormProps = {
onComplete: (appConnection: TAppConnection) => void;
@@ -84,6 +85,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
return <MsSqlConnectionForm onSubmit={onSubmit} />;
case AppConnection.Camunda:
return <CamundaConnectionForm onSubmit={onSubmit} />;
case AppConnection.Windmill:
return <WindmillConnectionForm onSubmit={onSubmit} />;
case AppConnection.Auth0:
return <Auth0ConnectionForm onSubmit={onSubmit} />;
default:
@@ -146,6 +149,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
return <MsSqlConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Camunda:
return <CamundaConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Windmill:
return <WindmillConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Auth0:
return <Auth0ConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
default:
@@ -0,0 +1,158 @@
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 { TWindmillConnection, WindmillConnectionMethod } from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
} from "./GenericAppConnectionFields";
type Props = {
appConnection?: TWindmillConnection;
onSubmit: (formData: FormData) => void;
};
const rootSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.Windmill)
});
const formSchema = z.discriminatedUnion("method", [
rootSchema.extend({
method: z.literal(WindmillConnectionMethod.AccessToken),
credentials: z.object({
accessToken: z.string().trim().min(1, "Access Token required"),
instanceUrl: z
.string()
.trim()
.transform((value) => value || undefined)
.refine((value) => (!value ? true : z.string().url().safeParse(value).success), {
message: "Invalid instance URL"
})
.optional()
})
})
]);
type FormData = z.infer<typeof formSchema>;
export const WindmillConnectionForm = ({ appConnection, onSubmit }: Props) => {
const isUpdate = Boolean(appConnection);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: appConnection ?? {
app: AppConnection.Windmill,
method: WindmillConnectionMethod.AccessToken
}
});
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.Windmill].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(WindmillConnectionMethod).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"
isOptional
tooltipClassName="max-w-sm"
tooltipText="Will default to Windmill Cloud if not specified."
>
<Input {...field} placeholder="https://app.windmill.dev" />
</FormControl>
)}
/>
<Controller
name="credentials.accessToken"
control={control}
shouldUnregister
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Access 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 Windmill"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};
@@ -45,6 +45,7 @@ export const OrgAuthTab = withPermission(
const { data: samlConfig, isPending: isLoadingSamlConfig } = useGetSSOConfig(
currentOrg?.id ?? ""
);
const { data: ldapConfig, isPending: isLoadingLdapConfig } = useGetLDAPConfig(
currentOrg?.id ?? ""
);
@@ -54,7 +55,8 @@ export const OrgAuthTab = withPermission(
!enabledLoginMethods || enabledLoginMethods.includes(method);
const isOidcConfigured = oidcConfig && (oidcConfig.discoveryURL || oidcConfig.issuer);
const isSamlConfigured = samlConfig && samlConfig.entryPoint;
const isSamlConfigured =
samlConfig && (samlConfig.entryPoint || samlConfig.issuer || samlConfig.cert);
const isLdapConfigured = ldapConfig && ldapConfig.url;
const shouldShowCreateIdentityProviderView =
@@ -301,6 +301,7 @@ export const SSOModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDelet
label={renderLabels(authProvider).entryPoint}
errorText={error?.message}
isError={Boolean(error)}
isRequired
>
<Input
{...field}
@@ -0,0 +1,152 @@
import { faAws, faGoogle } from "@fortawesome/free-brands-svg-icons";
import { faCheck, faCopy, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions/OrgPermissionCan";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from "@app/components/v2/Dropdown";
import { IconButton } from "@app/components/v2/IconButton";
import { Td, Tr } from "@app/components/v2/Table";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context/OrgPermissionContext";
import { useToggle } from "@app/hooks";
import { ExternalKmsProvider, KmsListEntry } from "@app/hooks/api/kms/types";
import { SubscriptionPlan } from "@app/hooks/api/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
kms: KmsListEntry;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["editExternalKms", "removeExternalKms", "upgradePlan"]>,
data?: {
kmsId?: string;
name?: string;
provider?: string;
}
) => void;
subscription: SubscriptionPlan;
};
export const ExternalKmsItem = ({ kms, handlePopUpOpen, subscription }: Props) => {
const [isKmsIdCopied, { timedToggle: toggleKmsIdCopied }] = useToggle(false);
const [isKmsAliasCopied, { timedToggle: toggleKmsAliasCopied }] = useToggle(false);
return (
<Tr key={kms.id}>
<Td className="flex max-w-xs items-center overflow-hidden text-ellipsis hover:overflow-auto hover:break-all">
{kms.externalKms.provider === ExternalKmsProvider.Aws && <FontAwesomeIcon icon={faAws} />}
{kms.externalKms.provider === ExternalKmsProvider.Gcp && (
<FontAwesomeIcon icon={faGoogle} />
)}
<div className="ml-2">{kms.externalKms.provider.toUpperCase()}</div>
</Td>
<Td>
<div className="group flex items-center gap-2">
{kms.name}
<IconButton
size="xs"
ariaLabel="copy icon"
colorSchema="secondary"
className="relative rounded-md opacity-0 group-hover:opacity-100"
onClick={() => {
if (isKmsAliasCopied) {
return;
}
navigator.clipboard.writeText(kms.name);
createNotification({
text: "KMS alias copied to clipboard",
type: "success"
});
toggleKmsAliasCopied(2000);
}}
>
<FontAwesomeIcon icon={isKmsAliasCopied ? faCheck : faCopy} />
</IconButton>
</div>
</Td>
<Td>
<div className="group flex items-center gap-2">
{kms.id}
<IconButton
size="xs"
ariaLabel="copy icon"
colorSchema="secondary"
className="relative rounded-md opacity-0 group-hover:opacity-100"
onClick={() => {
if (isKmsIdCopied) {
return;
}
navigator.clipboard.writeText(kms.id);
createNotification({
text: "KMS ID copied to clipboard",
type: "success"
});
toggleKmsIdCopied(2000);
}}
>
<FontAwesomeIcon icon={isKmsIdCopied ? faCheck : faCopy} />
</IconButton>
</div>
</Td>
<Td>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="flex justify-end hover:text-primary-400 data-[state=open]:text-primary-400">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<OrgPermissionCan I={OrgPermissionActions.Edit} an={OrgPermissionSubjects.Kms}>
{(isAllowed) => (
<DropdownMenuItem
disabled={!isAllowed}
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
if (subscription && !subscription?.externalKms) {
handlePopUpOpen("upgradePlan");
return;
}
handlePopUpOpen("editExternalKms", {
kmsId: kms.id
});
}}
>
Edit
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan I={OrgPermissionActions.Delete} an={OrgPermissionSubjects.Kms}>
{(isAllowed) => (
<DropdownMenuItem
disabled={!isAllowed}
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("removeExternalKms", {
name: kms.name,
kmsId: kms.id,
provider: kms.externalKms.provider
});
}}
>
Delete
</DropdownMenuItem>
)}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Td>
</Tr>
);
};
@@ -1,7 +1,5 @@
import { faAws, faGoogle } from "@fortawesome/free-brands-svg-icons";
import { faEllipsis, faLock, faPlus } from "@fortawesome/free-solid-svg-icons";
import { faLock, faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
@@ -9,10 +7,6 @@ import { OrgPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState,
Table,
TableContainer,
@@ -31,9 +25,9 @@ import {
import { withPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks";
import { useGetExternalKmsList, useRemoveExternalKms } from "@app/hooks/api";
import { ExternalKmsProvider } from "@app/hooks/api/kms/types";
import { AddExternalKmsForm } from "./AddExternalKmsForm";
import { ExternalKmsItem } from "./ExternalKmsItem";
import { UpdateExternalKmsForm } from "./UpdateExternalKmsForm";
export const OrgEncryptionTab = withPermission(
@@ -102,6 +96,7 @@ export const OrgEncryptionTab = withPermission(
<Tr>
<Td>Provider</Td>
<Td>Alias</Td>
<Td>ID</Td>
</Tr>
</THead>
<TBody>
@@ -115,78 +110,12 @@ export const OrgEncryptionTab = withPermission(
)}
{!isExternalKmsListLoading &&
externalKmsList?.map((kms) => (
<Tr key={kms.id}>
<Td className="flex max-w-xs items-center overflow-hidden text-ellipsis hover:overflow-auto hover:break-all">
{kms.externalKms.provider === ExternalKmsProvider.Aws && (
<FontAwesomeIcon icon={faAws} />
)}
{kms.externalKms.provider === ExternalKmsProvider.Gcp && (
<FontAwesomeIcon icon={faGoogle} />
)}
<div className="ml-2">{kms.externalKms.provider.toUpperCase()}</div>
</Td>
<Td>{kms.name}</Td>
<Td>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="flex justify-end hover:text-primary-400 data-[state=open]:text-primary-400">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<OrgPermissionCan
I={OrgPermissionActions.Edit}
an={OrgPermissionSubjects.Kms}
>
{(isAllowed) => (
<DropdownMenuItem
disabled={!isAllowed}
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
if (subscription && !subscription?.externalKms) {
handlePopUpOpen("upgradePlan");
return;
}
handlePopUpOpen("editExternalKms", {
kmsId: kms.id
});
}}
>
Edit
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
an={OrgPermissionSubjects.Kms}
>
{(isAllowed) => (
<DropdownMenuItem
disabled={!isAllowed}
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("removeExternalKms", {
name: kms.name,
kmsId: kms.id,
provider: kms.externalKms.provider
});
}}
>
Delete
</DropdownMenuItem>
)}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Td>
</Tr>
<ExternalKmsItem
key={kms.id}
kms={kms}
handlePopUpOpen={handlePopUpOpen}
subscription={subscription}
/>
))}
</TBody>
</Table>
@@ -11,6 +11,7 @@ import { GitHubSyncDestinationCol } from "./GitHubSyncDestinationCol";
import { HumanitecSyncDestinationCol } from "./HumanitecSyncDestinationCol";
import { TerraformCloudSyncDestinationCol } from "./TerraformCloudSyncDestinationCol";
import { VercelSyncDestinationCol } from "./VercelSyncDestinationCol";
import { WindmillSyncDestinationCol } from "./WindmillSyncDestinationCol";
type Props = {
secretSync: TSecretSync;
@@ -40,6 +41,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => {
return <CamundaSyncDestinationCol secretSync={secretSync} />;
case SecretSync.Vercel:
return <VercelSyncDestinationCol secretSync={secretSync} />;
case SecretSync.Windmill:
return <WindmillSyncDestinationCol secretSync={secretSync} />;
default:
throw new Error(
`Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}`
@@ -0,0 +1,14 @@
import { TWindmillSync } from "@app/hooks/api/secretSyncs/types/windmill-sync";
import { getSecretSyncDestinationColValues } from "../helpers";
import { SecretSyncTableCell } from "../SecretSyncTableCell";
type Props = {
secretSync: TWindmillSync;
};
export const WindmillSyncDestinationCol = ({ secretSync }: Props) => {
const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync);
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
};
@@ -90,6 +90,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
primaryText = destinationConfig.appName || destinationConfig.app;
secondaryText = destinationConfig.env;
break;
case SecretSync.Windmill:
primaryText = destinationConfig.workspace;
secondaryText = destinationConfig.path;
break;
default:
throw new Error(`Unhandled Destination Col Values ${destination}`);
}
@@ -252,8 +252,12 @@ export const SecretDetailSidebar = ({
switch (userType) {
case ActorType.PLATFORM:
return "System-generated";
case ActorType.IDENTITY:
return userName || "Deleted Identity";
case ActorType.USER:
return userName || "Deleted User";
default:
return userName;
return "Unknown";
}
};
@@ -9,18 +9,19 @@ import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionSecretSyncActions } from "@app/context/ProjectPermissionContext/types";
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs";
import { AwsParameterStoreSyncDestinationSection } from "@app/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/AwsParameterStoreSyncDestinationSection";
import { AwsSecretsManagerSyncDestinationSection } from "@app/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/AwsSecretsManagerSyncDestinationSection";
import { DatabricksSyncDestinationSection } from "@app/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/DatabricksSyncDestinationSection";
import { GitHubSyncDestinationSection } from "@app/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/GitHubSyncDestinationSection";
import { AwsParameterStoreSyncDestinationSection } from "./AwsParameterStoreSyncDestinationSection";
import { AwsSecretsManagerSyncDestinationSection } from "./AwsSecretsManagerSyncDestinationSection";
import { AzureAppConfigurationSyncDestinationSection } from "./AzureAppConfigurationSyncDestinationSection";
import { AzureKeyVaultSyncDestinationSection } from "./AzureKeyVaultSyncDestinationSection";
import { CamundaSyncDestinationSection } from "./CamundaSyncDestinationSection";
import { DatabricksSyncDestinationSection } from "./DatabricksSyncDestinationSection";
import { GcpSyncDestinationSection } from "./GcpSyncDestinationSection";
import { GitHubSyncDestinationSection } from "./GitHubSyncDestinationSection";
import { HumanitecSyncDestinationSection } from "./HumanitecSyncDestinationSection";
import { TerraformCloudSyncDestinationSection } from "./TerraformCloudSyncDestinationSection";
import { VercelSyncDestinationSection } from "./VercelSyncDestinationSection";
import { WindmillSyncDestinationSection } from "./WindmillSyncDestinationSection";
type Props = {
secretSync: TSecretSync;
@@ -69,6 +70,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }:
case SecretSync.Vercel:
DestinationComponents = <VercelSyncDestinationSection secretSync={secretSync} />;
break;
case SecretSync.Windmill:
DestinationComponents = <WindmillSyncDestinationSection secretSync={secretSync} />;
break;
default:
throw new Error(`Unhandled Destination Section components: ${destination}`);
}
@@ -0,0 +1,19 @@
import { GenericFieldLabel } from "@app/components/secret-syncs";
import { TWindmillSync } from "@app/hooks/api/secretSyncs/types/windmill-sync";
type Props = {
secretSync: TWindmillSync;
};
export const WindmillSyncDestinationSection = ({ secretSync }: Props) => {
const {
destinationConfig: { path, workspace }
} = secretSync;
return (
<>
<GenericFieldLabel label="Workspace">{workspace}</GenericFieldLabel>
<GenericFieldLabel label="Path">{path}</GenericFieldLabel>
</>
);
};
@@ -51,6 +51,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
case SecretSync.TerraformCloud:
case SecretSync.Camunda:
case SecretSync.Vercel:
case SecretSync.Windmill:
AdditionalSyncOptionsComponent = null;
break;
default:
@@ -3,7 +3,7 @@ import { useNavigate } from "@tanstack/react-router";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2";
import { Button, DeleteActionModal, Tooltip } from "@app/components/v2";
import { LeaveProjectModal } from "@app/components/v2/LeaveProjectModal";
import {
ProjectPermissionActions,
@@ -142,16 +142,22 @@ export const DeleteProjectSection = () => {
<div className="space-x-4">
<ProjectPermissionCan I={ProjectPermissionActions.Delete} a={ProjectPermissionSub.Project}>
{(isAllowed) => (
<Button
isLoading={isDeleting}
isDisabled={!isAllowed || isDeleting || currentWorkspace?.hasDeleteProtection}
colorSchema="danger"
variant="outline_bg"
type="submit"
onClick={() => handlePopUpOpen("deleteWorkspace")}
<Tooltip
className="max-w-sm"
content="This project is protected from deletion. To delete it, disable delete protection first."
isDisabled={!currentWorkspace?.hasDeleteProtection}
>
{`Delete ${currentWorkspace?.name}`}
</Button>
<Button
isLoading={isDeleting}
isDisabled={!isAllowed || isDeleting || currentWorkspace?.hasDeleteProtection}
colorSchema="danger"
variant="outline_bg"
type="submit"
onClick={() => handlePopUpOpen("deleteWorkspace")}
>
{`Delete ${currentWorkspace?.name}`}
</Button>
</Tooltip>
)}
</ProjectPermissionCan>
{!isOnlyAdminMember && (
@@ -1,6 +1,8 @@
import { faServer } from "@fortawesome/free-solid-svg-icons";
import { createNotification } from "@app/components/notifications";
import {
DeleteActionModal,
EmptyState,
Table,
TableContainer,
@@ -9,59 +11,131 @@ import {
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import { useGetMySessions } from "@app/hooks/api";
import { Button } from "@app/components/v2/Button";
import { useGetMySessions, useRevokeMySessionById } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { timeAgo } from "@app/lib/fn/date";
import { formatSessionUserAgent } from "@app/lib/fn/string";
const formatLocalDateTime = (date: Date): string => {
return date.toLocaleString(undefined, {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit"
});
};
export const SessionsTable = () => {
const { data, isPending } = useGetMySessions();
const { mutateAsync: revokeMySessionById } = useRevokeMySessionById();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"deleteSession"
] as const);
const formatDate = (dateToFormat: string) => {
const date = new Date(dateToFormat);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const handleSignOut = async (sessionId: string) => {
try {
await revokeMySessionById(sessionId);
createNotification({
text: "Session revoked successfully",
type: "success"
});
} catch (error) {
console.error(error);
createNotification({
text: "Failed to revoke session",
type: "error"
});
}
const formattedDate = `${day}/${month}/${year}`;
return formattedDate;
handlePopUpClose("deleteSession");
};
return (
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th>Created</Th>
<Th>Last active</Th>
<Th>IP address</Th>
<Th>Device</Th>
</Tr>
</THead>
<TBody>
{isPending && <TableSkeleton columns={4} innerKey="sesssions" />}
{!isPending &&
data &&
data.length > 0 &&
data.map(({ id, createdAt, lastUsed, ip, userAgent }) => {
return (
<Tr className="h-10" key={`session-${id}`}>
<Td>{formatDate(createdAt)}</Td>
<Td>{formatDate(lastUsed)}</Td>
<Td>{ip}</Td>
<Td>{userAgent}</Td>
</Tr>
);
})}
{!isPending && data && data?.length === 0 && (
<>
<DeleteActionModal
isOpen={popUp.deleteSession.isOpen}
title="Are you sure want to sign out of this session?"
onChange={(isOpen) => handlePopUpToggle("deleteSession", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
handleSignOut((popUp?.deleteSession?.data as { sessionId: string })?.sessionId)
}
/>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Td colSpan={4}>
<EmptyState title="No sessions on file" icon={faServer} />
</Td>
<Th>IP & Session ID</Th>
<Th>OS & Browser</Th>
<Th>Last accessed</Th>
<Th>Manage</Th>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
</THead>
<TBody>
{isPending && <TableSkeleton columns={4} innerKey="sessions" />}
{!isPending &&
data &&
data.length > 0 &&
data.map(({ id, createdAt, lastUsed, ip, userAgent }) => {
const { os, browser } = formatSessionUserAgent(userAgent);
const lastUsedDate = new Date(lastUsed);
const createdAtDate = new Date(createdAt);
return (
<Tr className="h-20" key={`session-${id}`}>
<Td>
<div className="flex flex-col">
<span className="font-medium">{ip}</span>
<span className="text-sm text-gray-500">ID: {id}</span>
</div>
</Td>
<Td>
<div className="flex flex-col">
<span className="font-medium">{os}</span>
<span className="text-sm text-gray-500">{browser}</span>
</div>
</Td>
<Td>
<div className="flex flex-col">
<Tooltip content={formatLocalDateTime(lastUsedDate)}>
<span className="font-medium">{timeAgo(lastUsedDate, new Date())}</span>
</Tooltip>
<Tooltip content={formatLocalDateTime(createdAtDate)}>
<span className="text-sm text-gray-500">
Created {timeAgo(createdAtDate, new Date())}
</span>
</Tooltip>
</div>
</Td>
<Td>
<Button
variant="plain"
colorSchema="danger"
onClick={() => handlePopUpOpen("deleteSession", { sessionId: id })}
>
Sign out
</Button>
</Td>
</Tr>
);
})}
{!isPending && data && data?.length === 0 && (
<Tr>
<Td colSpan={4}>
<EmptyState title="No sessions on file" icon={faServer} />
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
</>
);
};