Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 49 additions & 39 deletions frontend/src/components/layout/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
* by the Apache License, Version 2.0
*/

import { Box, Button, ColorModeSwitch, CopyButton, Flex } from '@redpanda-data/ui';
import { Button, ColorModeSwitch, CopyButton } from '@redpanda-data/ui';
import { Link, useLocation, useMatchRoute } from '@tanstack/react-router';
import { Heading } from 'components/redpanda-ui/components/typography';
import { cn } from 'components/redpanda-ui/lib/utils';
import { ChevronLeft } from 'lucide-react';
import { Fragment, useMemo } from 'react';

import { isEmbedded, isFeatureFlagEnabled } from '../../config';
Expand All @@ -28,6 +29,7 @@ import {
BreadcrumbList,
BreadcrumbSeparator,
} from '../redpanda-ui/components/breadcrumb';
import { Button as RegistryButton } from '../redpanda-ui/components/button';
import { Separator } from '../redpanda-ui/components/separator';
import { SidebarTrigger } from '../redpanda-ui/components/sidebar';

Expand All @@ -38,8 +40,8 @@ type BreadcrumbHeaderRowProps = {

function BreadcrumbHeaderRow({ useNewSidebar, breadcrumbItems }: BreadcrumbHeaderRowProps) {
return (
<Flex alignItems="center" justifyContent="space-between">
<Flex alignItems="center" gap={2}>
<div className="w-full border-b">
<div className="flex items-center gap-2 px-6 py-4">
{useNewSidebar ? (
<>
<SidebarTrigger />
Expand All @@ -50,7 +52,7 @@ function BreadcrumbHeaderRow({ useNewSidebar, breadcrumbItems }: BreadcrumbHeade
<Breadcrumb>
<BreadcrumbList>
{breadcrumbItems.map((item, index) => (
<Fragment key={item.linkTo}>
<Fragment key={`${index}-${item.linkTo}`}>
{index > 0 && <BreadcrumbSeparator />}
<BreadcrumbItem>
<BreadcrumbLink asChild>
Expand All @@ -62,8 +64,8 @@ function BreadcrumbHeaderRow({ useNewSidebar, breadcrumbItems }: BreadcrumbHeade
</BreadcrumbList>
</Breadcrumb>
)}
</Flex>
</Flex>
</div>
</div>
);
}

Expand All @@ -74,9 +76,10 @@ function AppPageHeader() {
const useNewSidebar = !isEmbedded();

const pageBreadcrumbs = useUIStateStore((s) => s.pageBreadcrumbs);
const pageTitle = useUIStateStore((s) => s._pageTitle);
const backLink = useUIStateStore((s) => s.backLink);
const selectedClusterName = useUIStateStore((s) => s.selectedClusterName);
const shouldHidePageHeader = useUIStateStore((s) => s.shouldHidePageHeader);

const breadcrumbItems = useMemo(() => {
const items: BreadcrumbEntry[] = [...pageBreadcrumbs];

Expand All @@ -92,38 +95,41 @@ function AppPageHeader() {
}, [pageBreadcrumbs, selectedClusterName]);

const lastBreadcrumb = breadcrumbItems.at(-1);
const breadcrumbsExceptLast = breadcrumbItems.slice(0, -1);

if (shouldHideHeader || shouldHidePageHeader) {
return null;
}

return (
<Box>
{/* we need to refactor out #mainLayout > div rule, for now I've added this box as a workaround */}
<BreadcrumbHeaderRow breadcrumbItems={breadcrumbsExceptLast} useNewSidebar={useNewSidebar} />

<Flex alignItems="center" justifyContent="space-between" pb={2}>
<Flex alignItems="center">
{lastBreadcrumb ? (
<Heading
// as="span"
className={cn('mr-2', lastBreadcrumb.options?.canBeTruncated ? 'break-spaces break-all' : 'nowrap')}
level={1}
>
{lastBreadcrumb.titleNode ?? lastBreadcrumb.title}
</Heading>
) : null}
{lastBreadcrumb ? (
<Box>
{lastBreadcrumb.options?.canBeCopied ? (
<CopyButton content={lastBreadcrumb.title} variant="ghost" />
) : null}
</Box>
) : null}
{Boolean(showRefresh) && <DataRefreshButton />}
</Flex>
<Flex alignItems="center" gap={2}>
<div>
<BreadcrumbHeaderRow breadcrumbItems={breadcrumbItems} useNewSidebar={useNewSidebar} />

<div className="flex items-center justify-between px-12 pt-6">
<div className="flex flex-col gap-1">
{backLink && (
<RegistryButton asChild className="-ml-2 w-fit text-muted-foreground" variant="ghost">
<Link to={backLink.linkTo}>
<ChevronLeft className="h-4 w-4" />
{backLink.title}
</Link>
</RegistryButton>
)}
<div className="flex items-center">
{pageTitle ? (
<Heading
className={cn('mr-2', lastBreadcrumb?.options?.canBeTruncated ? 'break-spaces break-all' : 'nowrap')}
level={1}
>
{pageTitle}
</Heading>
) : null}
{lastBreadcrumb?.options?.canBeCopied ? (
<CopyButton content={lastBreadcrumb.title} variant="ghost" />
) : null}
{Boolean(showRefresh) && <DataRefreshButton />}
</div>
</div>
<div className="flex items-center gap-2">
{!isEmbedded() && api.isRedpanda && (
<Link to="/debug-bundle">
<Button
Expand All @@ -139,9 +145,9 @@ function AppPageHeader() {
)}
<UserPreferencesButton />
{IsDev && !isEmbedded() && <ColorModeSwitch m={0} p={0} variant="ghost" />}
</Flex>
</Flex>
</Box>
</div>
</div>
</div>
);
}

Expand All @@ -165,17 +171,18 @@ function useShouldShowRefresh() {
const getStartedApiMatch = matchRoute({ to: '/get-started/api' });

// matches acls
const aclCreateMatch = matchRoute({ to: '/security/acls/create' });
const aclUpdateMatch = matchRoute({ to: '/security/acls/$aclName/update' });
const aclDetailMatch = matchRoute({ to: '/security/acls/$aclName/details' });
const isACLRelated = aclCreateMatch || aclUpdateMatch || aclDetailMatch;
const isACLRelated = aclDetailMatch;

// matches roles
const roleCreateMatch = matchRoute({ to: '/security/roles/create' });
const roleUpdateMatch = matchRoute({ to: '/security/roles/$roleName/update' });
const roleDetailMatch = matchRoute({ to: '/security/roles/$roleName/details' });
const isRoleRelated = roleCreateMatch || roleUpdateMatch || roleDetailMatch;

// matches user detail
const userDetailMatch = matchRoute({ to: '/security/users/$userName/details' });

if (connectClusterMatch && connectClusterMatch.connector === 'create-connector') {
return false;
}
Expand All @@ -194,6 +201,9 @@ function useShouldShowRefresh() {
if (isRoleRelated) {
return false;
}
if (userDetailMatch) {
return false;
}
if (connectWizardPagesMatch) {
return false;
}
Expand Down
105 changes: 56 additions & 49 deletions frontend/src/components/license/feature-license-notification.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Alert, AlertDescription, AlertIcon, Box, Flex, Text } from '@redpanda-data/ui';
import { Link } from 'components/redpanda-ui/components/typography';
import { type FC, type ReactElement, useEffect, useState } from 'react';

Expand Down Expand Up @@ -27,6 +26,8 @@ import {
type ListEnterpriseFeaturesResponse_Feature,
} from '../../protogen/redpanda/api/console/v1alpha1/license_pb';
import { api } from '../../state/backend-api';
import { Alert, AlertDescription } from '../redpanda-ui/components/alert';
import { Badge } from '../redpanda-ui/components/badge';

// biome-ignore lint/nursery/useMaxParams: Refactoring to options object would require updating all call sites
const getLicenseAlertContentForFeature = (
Expand All @@ -36,7 +37,7 @@ const getLicenseAlertContentForFeature = (
bakedInTrial: boolean,
onRegisterModalOpen: () => void
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: complex business logic
): { message: ReactElement; status: 'warning' | 'info' } | null => {
): { message: ReactElement; variant: 'destructive' | 'info' } | null => {
if (license === undefined) {
return null;
}
Expand All @@ -47,23 +48,23 @@ const getLicenseAlertContentForFeature = (
if (bakedInTrial) {
return {
message: (
<Box>
<Text>This is an enterprise feature. Register for an additional 30 days of enterprise features.</Text>
<Flex gap={2} my={2}>
<div>
<p>This is an enterprise feature. Register for an additional 30 days of enterprise features.</p>
<div className="my-2 flex gap-2">
<RegisterButton onRegisterModalOpen={onRegisterModalOpen} />
</Flex>
</Box>
</div>
</div>
),
status: msToExpiration > WARNING_THRESHOLD_DAYS * MS_IN_DAY ? 'info' : 'warning',
variant: msToExpiration > WARNING_THRESHOLD_DAYS * MS_IN_DAY ? 'info' : 'destructive',
};
}
return {
message: (
<Box>
<Text>This is an enterprise feature.</Text>
</Box>
<div>
<p>This is an enterprise feature.</p>
</div>
),
status: msToExpiration > WARNING_THRESHOLD_DAYS * MS_IN_DAY ? 'info' : 'warning',
variant: msToExpiration > WARNING_THRESHOLD_DAYS * MS_IN_DAY ? 'info' : 'destructive',
};
}

Expand All @@ -76,22 +77,22 @@ const getLicenseAlertContentForFeature = (
) {
return {
message: (
<Box>
<Text>This is an enterprise feature, active until {getPrettyExpirationDate(license)}.</Text>
<Flex gap={2} my={2}>
<div>
<p>This is an enterprise feature, active until {getPrettyExpirationDate(license)}.</p>
<div className="my-2 flex gap-2">
<UploadLicenseButton />
<UpgradeButton />
</Flex>
</Box>
</div>
</div>
),
status: 'info',
variant: 'info',
};
}
if (msToExpiration > -1 && msToExpiration < 15 * MS_IN_DAY && coreHasEnterpriseFeatures(enterpriseFeaturesUsed)) {
return {
message: (
<Box>
<Text>
<div>
<p>
Your Redpanda Enterprise trial is expiring in {getPrettyTimeToExpiration(license)}; at that point, your{' '}
<Link href={ENTERPRISE_FEATURES_DOCS_LINK} rel="noopener noreferrer" target="_blank">
enterprise features
Expand All @@ -101,14 +102,14 @@ const getLicenseAlertContentForFeature = (
contact us
</Link>
.
</Text>
<Flex gap={2} my={2}>
</p>
<div className="my-2 flex gap-2">
<UploadLicenseButton />
<UpgradeButton />
</Flex>
</Box>
</div>
</div>
),
status: 'warning',
variant: 'destructive',
};
}
} else {
Expand All @@ -117,37 +118,37 @@ const getLicenseAlertContentForFeature = (
if (license.type === License_Type.TRIAL) {
return {
message: (
<Box>
<Text>This is an enterprise feature. Your trial is active until {getPrettyExpirationDate(license)}</Text>
<Flex gap={2} my={2}>
<div>
<p>This is an enterprise feature. Your trial is active until {getPrettyExpirationDate(license)}</p>
<div className="my-2 flex gap-2">
<UploadLicenseButton />
<UpgradeButton />
</Flex>
</Box>
</div>
</div>
),
status: 'info',
variant: 'info',
};
}
return {
message: (
<Box>
<Text>
<div>
<p>
This is a Redpanda Enterprise feature. Try it with our{' '}
<Link href={getEnterpriseCTALink('tryEnterprise')} rel="noopener noreferrer" target="_blank">
Redpanda Enterprise Trial
</Link>
.
</Text>
</Box>
</p>
</div>
),
status: 'info',
variant: 'info',
};
}
if (msToExpiration > 0 && msToExpiration < 15 * MS_IN_DAY && license.type === License_Type.TRIAL) {
return {
message: (
<Box>
<Text>
<div>
<p>
Your Redpanda Enterprise trial is expiring in {getPrettyTimeToExpiration(license)}; at that point, your{' '}
<Link href={ENTERPRISE_FEATURES_DOCS_LINK} rel="noopener noreferrer" target="_blank">
enterprise features
Expand All @@ -157,22 +158,25 @@ const getLicenseAlertContentForFeature = (
contact us
</Link>
.
</Text>
<Flex gap={2} my={2}>
</p>
<div className="my-2 flex gap-2">
<UploadLicenseButton />
<UpgradeButton />
</Flex>
</Box>
</div>
</div>
),
status: 'warning',
variant: 'destructive',
};
}
}

return null;
};

export const FeatureLicenseNotification: FC<{ featureName: 'reassignPartitions' | 'rbac' }> = ({ featureName }) => {
export const FeatureLicenseNotification: FC<{
featureName: 'reassignPartitions' | 'rbac';
as?: 'alert' | 'badge';
}> = ({ featureName, as: renderAs = 'alert' }) => {
const [registerModalOpen, setIsRegisterModalOpen] = useState(false);

useEffect(() => {
Expand Down Expand Up @@ -220,16 +224,19 @@ export const FeatureLicenseNotification: FC<{ featureName: 'reassignPartitions'
return null;
}

const { message, status } = alertContent;
const { message, variant } = alertContent;

if (renderAs === 'badge') {
return <Badge variant={variant === 'destructive' ? 'destructive' : 'simple'}>{message}</Badge>;
}

return (
<Box>
<Alert mb={4} status={status} variant="subtle">
<AlertIcon />
<>
<Alert variant={variant}>
<AlertDescription>{message}</AlertDescription>
</Alert>

<RegisterModal isOpen={registerModalOpen} onClose={() => setIsRegisterModalOpen(false)} />
</Box>
</>
);
};
Loading
Loading