Skip to content

Commit

Permalink
feat: adding feature flag possibility
Browse files Browse the repository at this point in the history
* wip: updated license managing

* wip: remove featureAccessService & export each function into it

* fix: missing dependency in useEffect

* fix: eslint yelling for format + some unused eslint directives

* fix: eslint errors & warning

* chore: remove server part from featire-access file

* feat: add nudge compoenent for workflow & testrun

* wip: adding nudge on webhook + analytics + user creation

* feat: add snooze rule nudge

* fix: wrong typo for analytics nudge entry

* feat: adding test nudge and connect entitlements to backend

* feat: adding github banner for stars & finishing rule snooze feature flag

* fix: hide github banner when clicking on the link

* chore: update traduction for arabic & french version
  • Loading branch information
carere authored Jan 14, 2025
1 parent 679ecbf commit a289152
Show file tree
Hide file tree
Showing 71 changed files with 1,335 additions and 922 deletions.
32 changes: 15 additions & 17 deletions packages/app-builder/src/components/Cases/CaseDecisions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
type RuleExecution,
} from '@app-builder/models/decision';
import { type OperatorFunction } from '@app-builder/models/editable-operators';
import { type LicenseEntitlements } from '@app-builder/models/license';
import { type RuleSnoozeWithRuleId } from '@app-builder/models/rule-snooze';
import { type ScenarioIterationRule } from '@app-builder/models/scenario-iteration-rule';
import { ReviewDecisionModal } from '@app-builder/routes/ressources+/cases+/review-decision';
Expand Down Expand Up @@ -64,19 +65,18 @@ interface DecisionsDetail {

export function CaseDecisions({
decisions,
featureAccess,
entitlements,
caseDecisionsPromise,
}: {
decisions: Decision[];
featureAccess: {
isReadSnoozeAvailable: boolean;
isCreateSnoozeAvailable: boolean;
};
entitlements: LicenseEntitlements;
caseDecisionsPromise: Promise<
[
TableModel[],
CustomList[],
DecisionsDetail[],
{
isReadSnoozeAvailable: boolean;
isCreateSnoozeAvailable: boolean;
},
]
[TableModel[], CustomList[], DecisionsDetail[]]
>;
}) {
const { t } = useTranslation(casesI18n);
Expand Down Expand Up @@ -151,19 +151,15 @@ export function CaseDecisions({
<CollapsibleV2.Content className="col-span-full">
<React.Suspense fallback={<DecisionDetailSkeleton />}>
<Await resolve={caseDecisionsPromise}>
{([
dataModel,
customLists,
decisionsDetail,
featureAccess,
]) => {
{([dataModel, customLists, decisionsDetail]) => {
return (
<DecisionDetail
key={row.id}
decision={row}
decisionsDetail={decisionsDetail}
dataModel={dataModel}
customLists={customLists}
entitlements={entitlements}
featureAccess={featureAccess}
/>
);
Expand Down Expand Up @@ -273,12 +269,14 @@ function DecisionDetail({
decisionsDetail,
dataModel,
customLists,
entitlements,
featureAccess,
}: {
decision: Decision;
decisionsDetail: DecisionsDetail[];
dataModel: TableModel[];
customLists: CustomList[];
entitlements: LicenseEntitlements;
featureAccess: {
isReadSnoozeAvailable: boolean;
isCreateSnoozeAvailable: boolean;
Expand Down Expand Up @@ -365,11 +363,11 @@ function DecisionDetail({
}}
/>

{featureAccess.isReadSnoozeAvailable &&
pivotValues.length > 0 ? (
{pivotValues.length > 0 ? (
<RuleSnoozes
ruleSnoozes={ruleSnoozes}
pivotValues={pivotValues}
entitlements={entitlements}
isCreateSnoozeAvailable={
featureAccess.isCreateSnoozeAvailable
}
Expand Down
68 changes: 58 additions & 10 deletions packages/app-builder/src/components/Cases/RuleSnoozes.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
import { type Pivot } from '@app-builder/models';
import { type LicenseEntitlements } from '@app-builder/models/license';
import { type RuleSnooze } from '@app-builder/models/rule-snooze';
import { AddRuleSnooze } from '@app-builder/routes/ressources+/cases+/add-rule-snooze';
import { formatDateTime, useFormatLanguage } from '@app-builder/utils/format';
import * as Ariakit from '@ariakit/react';
import clsx from 'clsx';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { match } from 'ts-pattern';
import { Button, Tag } from 'ui-design-system';
import { Icon } from 'ui-icons';

import { CopyToClipboardButton } from '../CopyToClipboardButton';
import { PivotDetails } from '../Data/PivotDetails';
import { Nudge } from '../Nudge';
import { casesI18n } from './cases-i18n';

export function RuleSnoozes({
ruleSnoozes,
pivotValues,
isCreateSnoozeAvailable,
entitlements,
decisionId,
ruleId,
}: {
ruleSnoozes: RuleSnooze[];
entitlements: LicenseEntitlements;
pivotValues: {
pivot: Pivot;
value: string;
Expand Down Expand Up @@ -115,17 +120,60 @@ export function RuleSnoozes({
</Ariakit.Hovercard>
</Ariakit.HovercardProvider>
</div>
) : isCreateSnoozeAvailable ? (
<AddRuleSnooze decisionId={decisionId} ruleId={ruleId}>
<Button className="h-8 w-fit pl-2">
<Icon icon="snooze" className="size-6" />
{t('cases:case_detail.add_rule_snooze.snooze_this_value')}
</Button>
</AddRuleSnooze>
) : (
<span className="text-grey-50 col-span-2 text-xs">
{t('cases:case_detail.add_rule_snooze.no_access')}
</span>
match(entitlements.ruleSnoozes)
.with('allowed', () =>
isCreateSnoozeAvailable ? (
<AddRuleSnooze decisionId={decisionId} ruleId={ruleId}>
<Button className="h-8 w-fit pl-2">
<Icon icon="snooze" className="size-6" />
{t(
'cases:case_detail.add_rule_snooze.snooze_this_value',
)}
</Button>
</AddRuleSnooze>
) : (
<span className="text-grey-50 col-span-2 text-xs">
{t('cases:case_detail.add_rule_snooze.no_access')}
</span>
),
)
.with('restricted', () => (
<Button className="relative h-8 w-fit pl-2" disabled>
<Icon icon="snooze" className="size-6" />
{t('cases:case_detail.add_rule_snooze.snooze_this_value')}
<Nudge
className="border-purple-25 absolute -right-3 -top-3 size-6 border"
content={t('cases:case_detail.add_rule_snooze.nudge')}
link="https://docs.checkmarble.com/docs/rule-snoozes"
/>
</Button>
))
.with('test', () =>
isCreateSnoozeAvailable ? (
<AddRuleSnooze decisionId={decisionId} ruleId={ruleId}>
<Button className="relative h-8 w-fit pl-2">
<Icon icon="snooze" className="size-6" />
{t(
'cases:case_detail.add_rule_snooze.snooze_this_value',
)}
<Nudge
className="absolute -right-3 -top-3 size-6 border border-purple-50"
kind="test"
content={t(
'cases:case_detail.add_rule_snooze.nudge',
)}
link="https://docs.checkmarble.com/docs/rule-snoozes"
/>
</Button>
</AddRuleSnooze>
) : (
<span className="text-grey-50 col-span-2 text-xs">
{t('cases:case_detail.add_rule_snooze.no_access')}
</span>
),
)
.exhaustive()
)}
</React.Fragment>
);
Expand Down
42 changes: 42 additions & 0 deletions packages/app-builder/src/components/GithubBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Icon } from 'ui-icons';

export const GithubBanner = () => {
const [isShown, setVisibility] = useState(
(localStorage.getItem('show-github-banner') as 'true' | 'false') ?? 'true',
);

const { t } = useTranslation(['common']);

return isShown === 'true' ? (
<div className="text-s text-grey-00 absolute left-0 top-0 flex w-full flex-row justify-between gap-2 bg-purple-100 p-4 font-normal">
<div className="flex w-full flex-row items-center gap-4">
<Icon icon="notifications" className="size-6" />
<span className="inline-flex gap-1 font-semibold">
<span>{t('common:github_banner')}</span>
<a
href="https://github.com/checkmarble/marble"
className="underline"
target="_blank"
rel="noreferrer"
onClick={() => {
localStorage.setItem('show-github-banner', 'false');
setVisibility('false');
}}
>
Github
</a>
</span>
</div>
<Icon
icon="cross"
className="size-6 hover:cursor-pointer"
onClick={() => {
localStorage.setItem('show-github-banner', 'false');
setVisibility('false');
}}
/>
</div>
) : null;
};
9 changes: 8 additions & 1 deletion packages/app-builder/src/components/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface SidebarLinkProps {
Icon: (props: Omit<IconProps, 'icon'>) => JSX.Element;
labelTKey: ParseKeys<['navigation']>;
to: string;
children?: React.ReactNode;
}

export const sidebarLink = cva(
Expand All @@ -30,7 +31,12 @@ export const sidebarLink = cva(
},
);

export function SidebarLink({ Icon, labelTKey, to }: SidebarLinkProps) {
export function SidebarLink({
Icon,
labelTKey,
to,
children,
}: SidebarLinkProps) {
const { t } = useTranslation(navigationI18n);

return (
Expand All @@ -39,6 +45,7 @@ export function SidebarLink({ Icon, labelTKey, to }: SidebarLinkProps) {
<span className="line-clamp-1 text-start opacity-0 transition-opacity group-aria-expanded/nav:opacity-100">
{t(labelTKey)}
</span>
{children}
</NavLink>
);
}
Expand Down
77 changes: 77 additions & 0 deletions packages/app-builder/src/components/Nudge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import {
Hovercard,
HovercardAnchor,
HovercardProvider,
} from '@ariakit/react/hovercard';
import clsx from 'clsx';
import { useTranslation } from 'react-i18next';
import { CtaClassName } from 'ui-design-system';
import { Icon } from 'ui-icons';

type NudgeProps = {
content: string;
className?: string;
link?: string;
kind?: 'test' | 'restricted';
};

export const Nudge = ({
content,
link,
className,
kind = 'restricted',
}: NudgeProps) => {
const { t } = useTranslation(['scenarios', 'common']);

return (
<HovercardProvider showTimeout={0} hideTimeout={0} placement="right">
<HovercardAnchor
tabIndex={-1}
className={clsx(
'text-grey-00 flex flex-row items-center justify-center rounded',
{ 'bg-purple-100': kind === 'test' },
{ 'bg-purple-50': kind === 'restricted' },
className,
)}
>
<Icon
icon={kind === 'restricted' ? 'lock' : 'unlock-right'}
className="size-3.5"
aria-hidden
/>
</HovercardAnchor>
<Hovercard
portal
gutter={8}
className="bg-grey-00 flex w-60 flex-col items-center gap-6 rounded border border-purple-50 p-4 shadow-lg"
>
<span className="text-m font-bold">{t('common:premium')}</span>
<div className="flex w-full flex-col items-center gap-2">
<p className="text-s w-full text-center font-medium">{content}</p>
{link ? (
<a
className="text-s inline-block w-full text-center text-purple-100 hover:underline"
target="_blank"
rel="noreferrer"
href={link}
>
{t('common:check_on_docs')}
</a>
) : null}
</div>
<a
className={CtaClassName({
variant: 'primary',
color: 'purple',
className: 'mt-4',
})}
href="https://checkmarble.com/upgrade"
target="_blank"
rel="noreferrer"
>
{t('common:upgrade')}
</a>
</Hovercard>
</HovercardProvider>
);
};
16 changes: 13 additions & 3 deletions packages/app-builder/src/components/Page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import { Link, useNavigate } from '@remix-run/react';
import { cva } from 'class-variance-authority';
import clsx from 'clsx';
import { useTranslation } from 'react-i18next';
import { ClientOnly } from 'remix-utils/client-only';
import { Tooltip } from 'ui-design-system';
import { Icon } from 'ui-icons';

import { GithubBanner } from './GithubBanner';

function PageMain({ className, ...props }: React.ComponentProps<'div'>) {
return (
<main
Expand All @@ -31,16 +34,23 @@ export const headerHeight = cva(undefined, {
},
});

function PageHeader({ className, ...props }: React.ComponentProps<'div'>) {
function PageHeader({
className,
children,
...props
}: React.ComponentProps<'div'>) {
return (
<div
className={clsx(
'border-b-grey-10 bg-grey-00 text-l text-grey-100 flex shrink-0 flex-row items-center border-b px-4 font-bold lg:px-6',
'border-b-grey-10 bg-grey-00 text-l text-grey-100 relative flex shrink-0 flex-row items-center border-b px-4 font-bold lg:px-6',
headerHeight({ type: 'height' }),
className,
)}
{...props}
/>
>
<ClientOnly fallback={null}>{() => <GithubBanner />}</ClientOnly>
{children}
</div>
);
}

Expand Down
Loading

0 comments on commit a289152

Please sign in to comment.