Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improving Licensing & Feature Access Check #641

Merged
merged 15 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from 12 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
29 changes: 14 additions & 15 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 @@ -370,6 +368,7 @@ function DecisionDetail({
<RuleSnoozes
ruleSnoozes={ruleSnoozes}
pivotValues={pivotValues}
entitlements={entitlements}
isCreateSnoozeAvailable={
featureAccess.isCreateSnoozeAvailable
}
Expand Down
36 changes: 26 additions & 10 deletions packages/app-builder/src/components/Cases/RuleSnoozes.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
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';
Expand All @@ -11,16 +12,19 @@ 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 +119,29 @@ 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>
) : entitlements.ruleSnoozes ? (
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>
)
) : (
<span className="text-grey-50 col-span-2 text-xs">
{t('cases:case_detail.add_rule_snooze.no_access')}
</span>
<Button className="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="absolute -right-3 -top-3 size-6"
content={t('cases:case_detail.add_rule_snooze.nudge')}
link="https://checkmarble.com/docs"
/>
</Button>
)}
</React.Fragment>
);
Expand Down
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
66 changes: 66 additions & 0 deletions packages/app-builder/src/components/Nudge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import {
Hovercard,
HovercardAnchor,
HovercardProvider,
} from '@ariakit/react/hovercard';
import clsx from 'clsx';
import { useTranslation } from 'react-i18next';
import { Button } 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 flex-col items-center gap-2">
<p className="text-s text-center font-medium">{content}</p>
<a
className="text-s text-purple-100 hover:underline"
target="_blank"
rel="noreferrer"
href={link}
>
{link}
</a>
</div>
<Button variant="primary" className="mt-4">
{t('common:upgrade')}
</Button>
</Hovercard>
</HovercardProvider>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { CalloutV2 } from '@app-builder/components/Callout';
import { Nudge } from '@app-builder/components/Nudge';
import { useTranslation } from 'react-i18next';
import { Button } from 'ui-design-system';
import { Icon } from 'ui-icons';

export const TestRunNudge = () => {
const { t } = useTranslation(['scenarios']);

return (
<section className="flex flex-col gap-8">
<h2 className="text-grey-100 text-m font-semibold">
{t('scenarios:home.testrun')}
</h2>
<div className="flex max-w-[500px] flex-row gap-4">
<div className="bg-grey-00 relative flex h-fit flex-col gap-4 rounded-lg border-2 border-purple-50 p-8">
<Nudge
className="absolute -right-3 -top-3 size-6"
content={t('scenarios:testrun.nudge')}
link="https://checkmarble.com/docs"
/>
<CalloutV2>
<div className="flex flex-col gap-4">
<span>{t('scenarios:testrun.description')}</span>
</div>
</CalloutV2>
<Button variant="primary" disabled className="isolate h-10 w-fit">
<Icon icon="plus" className="size-6" aria-hidden />
{t('scenarios:create_testrun.title')}
</Button>
</div>
</div>
</section>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { CalloutV2 } from '@app-builder/components/Callout';
import { Nudge } from '@app-builder/components/Nudge';
import { useTranslation } from 'react-i18next';
import { Button } from 'ui-design-system';
import { Icon } from 'ui-icons';

export const WorkflowNudge = () => {
const { t } = useTranslation(['scenarios', 'workflows']);

return (
<section className="flex flex-col gap-8">
<h2 className="text-grey-100 text-m font-semibold">
{t('scenarios:home.workflow')}
</h2>
<div className="flex max-w-[500px] flex-row gap-4">
<div className="bg-grey-00 relative flex h-fit flex-col gap-4 rounded-lg border-2 border-purple-50 p-8">
<Nudge
className="absolute -right-3 -top-3 size-6"
content={t('workflows:nudge')}
link="https://checkmarble.com/docs"
/>
<CalloutV2>
<div className="flex flex-col gap-4">
<span>{t('scenarios:home.workflow_description')}</span>
</div>
</CalloutV2>
<Button variant="primary" disabled className="isolate h-10 w-fit">
<Icon icon="plus" className="size-6" aria-hidden />
{t('scenarios:home.workflow.create')}
</Button>
</div>
</div>
</section>
);
};
42 changes: 37 additions & 5 deletions packages/app-builder/src/infra/license-api.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,54 @@
import { licenseApi } from 'marble-api';
import {
fetchWithAuthMiddleware,
licenseApi,
type TokenService,
} from 'marble-api';
import * as R from 'remeda';
import { type FunctionKeys } from 'typescript-utils';

//TODO: To remove

export type LicenseApi = {
[P in FunctionKeys<typeof licenseApi>]: (typeof licenseApi)[P];
};

function getLicenseAPIClient({ baseUrl }: { baseUrl: string }): LicenseApi {
export type GetLicenseAPIClientWithAuth = (
tokenService: TokenService<string>,
) => LicenseApi;

function getLicenseAPIClient({
tokenService,
baseUrl,
}: {
baseUrl: string;
tokenService?: TokenService<string>;
}): LicenseApi {
const fetch = tokenService
? fetchWithAuthMiddleware({
tokenService,
getAuthorizationHeader: (token) => ({
name: 'Authorization',
value: `Bearer ${token}`,
}),
})
: undefined;

const { defaults, servers, ...api } = licenseApi;

//@ts-expect-error can't infer args
return R.mapValues(api, (value) => (...args) => {
// @ts-expect-error can't infer args
return value(...args, { baseUrl });
return value(...args, { fetch, baseUrl });
});
}
export function initializeLicenseAPIClient({ baseUrl }: { baseUrl: string }) {

export function initializeLicenseAPIClient({ baseUrl }: { baseUrl: string }): {
licenseApi: LicenseApi;
getLicenseAPIClientWithAuth: GetLicenseAPIClientWithAuth;
} {
return {
licenseAPIClient: getLicenseAPIClient({ baseUrl }),
licenseApi: getLicenseAPIClient({ baseUrl }),
getLicenseAPIClientWithAuth: (tokenService: TokenService<string>) =>
getLicenseAPIClient({ tokenService, baseUrl }),
};
}
1 change: 1 addition & 0 deletions packages/app-builder/src/locales/en/cases.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
"case_detail.add_a_comment.placeholder": "Write a note",
"case_detail.add_a_comment.post": "post a comment",
"case_detail.add_rule_snooze.title": "Snooze a rule",
"case_detail.add_rule_snooze.nudge": "Snooze a rule to disable it temporarily",
"case_detail.add_rule_snooze.callout": "Snoozing a rule for a specific pivot value will prevent it from triggering for a certain amount of time (<DocLink>learn more</DocLink>)",
"case_detail.add_rule_snooze.comment.label": "Comment",
"case_detail.add_rule_snooze.comment.placeholder": "Explain why you are snoozing this rule",
Expand Down
2 changes: 2 additions & 0 deletions packages/app-builder/src/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"false": "False",
"from_to": "From <Date>{{start_date}}</Date> to <Date>{{end_date}}</Date>",
"live": "Live",
"premium": "Premium Feature",
"upgrade": "Upgrade now",
"auth.logout": "Log out",
"search": "Search",
"error_one": "{{count}} error",
Expand Down
Loading
Loading