Skip to content

Commit 2367196

Browse files
authored
Merge branch 'main' into run-store-write-adapter
2 parents 3b809b9 + a6400f9 commit 2367196

136 files changed

Lines changed: 7987 additions & 2690 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: improvement
4+
---
5+
6+
Add a currency unit to the agent dashboard "LLM spend" chart label, so it now reads "LLM spend ($)".
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: feature
4+
---
5+
6+
Enforce role-based permissions across the dashboard and API. New permission boundaries cover: runs (cancel, replay, bulk actions), deployments (rollback, promote, cancel), prompt versions, organization members (invite, resend, revoke), billing and seat purchases, integrations (GitHub and Vercel), and environment variables and API keys (restricted by environment tier). Roles without access can no longer read or change these, gated controls are disabled with a tooltip, and gated pages show a permission-denied panel instead of redirecting away. Behaviour is unchanged in the default configuration, where permissions stay permissive.

.server-changes/sso.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: feature
4+
---
5+
6+
SAML/OIDC single sign-on: SSO login with optional per-domain enforcement, JIT provisioning, and periodic re-validation against the IdP.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: improvement
4+
---
5+
6+
Replace the Task type filter on the Tasks page with a segmented control: "All" plus icon-only Agent, Standard, and Scheduled segments (each with a tooltip showing its label and number-key shortcut). Filtering is now single-select (one task type at a time) instead of multi-select. Shortcut keys 0–3 select each segment.

apps/webapp/app/components/ErrorDisplay.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { HomeIcon } from "@heroicons/react/20/solid";
22
import { isRouteErrorResponse, useRouteError } from "@remix-run/react";
33
import { friendlyErrorDisplay } from "~/utils/httpErrors";
4+
import { permissionDeniedMessage } from "~/utils/permissionDenied";
45
import { LinkButton } from "./primitives/Buttons";
56
import { Header1 } from "./primitives/Headers";
67
import { Paragraph } from "./primitives/Paragraph";
8+
import { PermissionDenied } from "./PermissionDenied";
79
import { TriggerRotatingLogo } from "./TriggerRotatingLogo";
810
import { type ReactNode } from "react";
911

@@ -17,6 +19,21 @@ type ErrorDisplayOptions = {
1719
export function RouteErrorDisplay(options?: ErrorDisplayOptions) {
1820
const error = useRouteError();
1921

22+
// A failed `authorization` check (or `throwPermissionDenied`) throws a 403
23+
// that bubbles to the nearest route ErrorBoundary. Every layout boundary
24+
// renders through here, so handling it once means a gated route only has to
25+
// declare `authorization` to get the permission panel: no per-route boundary.
26+
const permission = isRouteErrorResponse(error) ? permissionDeniedMessage(error.data) : null;
27+
if (permission) {
28+
return (
29+
<div className="flex min-h-screen w-full items-center justify-center p-4">
30+
<div className="w-full max-w-md">
31+
<PermissionDenied message={permission} />
32+
</div>
33+
</div>
34+
);
35+
}
36+
2037
return (
2138
<>
2239
{isRouteErrorResponse(error) ? (
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { NoSymbolIcon } from "@heroicons/react/20/solid";
2+
import React from "react";
3+
import { useOptionalOrganization } from "~/hooks/useOrganizations";
4+
import { organizationRolesPath } from "~/utils/pathBuilder";
5+
import { LinkButton } from "./primitives/Buttons";
6+
import { InfoPanel } from "./primitives/InfoPanel";
7+
8+
export function PermissionDenied({ message }: { message: React.ReactNode }) {
9+
const organization = useOptionalOrganization();
10+
11+
return (
12+
<InfoPanel
13+
icon={NoSymbolIcon}
14+
iconClassName="text-text-dimmed"
15+
title="Permission denied"
16+
accessory={
17+
organization ? (
18+
<LinkButton to={organizationRolesPath(organization)} variant="secondary/small">
19+
View roles
20+
</LinkButton>
21+
) : undefined
22+
}
23+
>
24+
{message}
25+
</InfoPanel>
26+
);
27+
}

apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import { ArrowLeftIcon } from "@heroicons/react/24/solid";
1+
import { ArrowLeftIcon, LinkIcon } from "@heroicons/react/24/solid";
22
import { BellIcon } from "~/assets/icons/BellIcon";
33
import { CreditCardIcon } from "~/assets/icons/CreditCardIcon";
44
import { PadlockIcon } from "~/assets/icons/PadlockIcon";
55
import { UsageIcon } from "~/assets/icons/UsageIcon";
66
import { RolesIcon } from "~/assets/icons/RolesIcon";
7-
import { ShieldLockIcon } from "~/assets/icons/ShieldLockIcon";
87
import { SlackIcon } from "~/assets/icons/SlackIcon";
98
import { SlidersIcon } from "~/assets/icons/SlidersIcon";
109
import { UserGroupIcon } from "~/assets/icons/UserGroupIcon";
@@ -17,6 +16,7 @@ import {
1716
organizationRolesPath,
1817
organizationSettingsPath,
1918
organizationSlackIntegrationPath,
19+
organizationSsoPath,
2020
organizationTeamPath,
2121
organizationVercelIntegrationPath,
2222
rootPath,
@@ -48,10 +48,12 @@ export function OrganizationSettingsSideMenu({
4848
organization,
4949
buildInfo,
5050
isUsingPlugin,
51+
isSsoUsingPlugin,
5152
}: {
5253
organization: MatchedOrganization;
5354
buildInfo: BuildInfo;
5455
isUsingPlugin: boolean;
56+
isSsoUsingPlugin: boolean;
5557
}) {
5658
const { isManagedCloud } = useFeatures();
5759
const featureFlags = useFeatureFlags();
@@ -128,7 +130,7 @@ export function OrganizationSettingsSideMenu({
128130
{featureFlags.hasPrivateConnections && (
129131
<SideMenuItem
130132
name="Private Connections"
131-
icon={PadlockIcon}
133+
icon={LinkIcon}
132134
activeIconColor="text-text-bright"
133135
inactiveIconColor="text-text-dimmed"
134136
to={v3PrivateConnectionsPath(organization)}
@@ -145,6 +147,21 @@ export function OrganizationSettingsSideMenu({
145147
data-action="roles"
146148
/>
147149
)}
150+
{isManagedCloud && isSsoUsingPlugin && (
151+
<SideMenuItem
152+
name="SSO"
153+
icon={PadlockIcon}
154+
activeIconColor="text-text-bright"
155+
inactiveIconColor="text-text-dimmed"
156+
to={organizationSsoPath(organization)}
157+
data-action="sso"
158+
badge={
159+
currentPlan?.v3Subscription?.plan?.code === "enterprise" ? undefined : (
160+
<Badge variant="extra-small">Enterprise</Badge>
161+
)
162+
}
163+
/>
164+
)}
148165
<SideMenuItem
149166
name="Settings"
150167
icon={SlidersIcon}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { forwardRef, type ReactNode } from "react";
2+
import { Button } from "./Buttons";
3+
4+
export const DEFAULT_NO_PERMISSION_TOOLTIP = "You don't have permission to do this";
5+
6+
type PermissionButtonProps = React.ComponentProps<typeof Button> & {
7+
/** Server-computed flag (see `checkPermissions`). When false the button is disabled with a tooltip. */
8+
hasPermission: boolean;
9+
noPermissionTooltip?: ReactNode;
10+
};
11+
12+
/**
13+
* A `Button` that disables itself and shows an explanatory tooltip when the
14+
* user lacks permission. Display only — the server route builder's
15+
* `authorization` block is the real gate. `Button` already renders its
16+
* `tooltip` while disabled (it wraps the disabled button in a hoverable span),
17+
* so we reuse that path.
18+
*/
19+
export const PermissionButton = forwardRef<HTMLButtonElement, PermissionButtonProps>(
20+
({ hasPermission, noPermissionTooltip, disabled, tooltip, ...props }, ref) => {
21+
if (hasPermission) {
22+
return <Button ref={ref} disabled={disabled} tooltip={tooltip} {...props} />;
23+
}
24+
25+
return (
26+
<Button
27+
ref={ref}
28+
{...props}
29+
disabled
30+
tooltip={noPermissionTooltip ?? DEFAULT_NO_PERMISSION_TOOLTIP}
31+
/>
32+
);
33+
}
34+
);
35+
36+
PermissionButton.displayName = "PermissionButton";
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { type ReactNode } from "react";
2+
import { cn } from "~/utils/cn";
3+
import { ButtonContent, type ButtonContentPropsType, LinkButton } from "./Buttons";
4+
import { SimpleTooltip } from "./Tooltip";
5+
import { DEFAULT_NO_PERMISSION_TOOLTIP } from "./PermissionButton";
6+
7+
type PermissionLinkProps = React.ComponentProps<typeof LinkButton> & {
8+
/** Server-computed flag (see `checkPermissions`). When false the link is disabled with a tooltip. */
9+
hasPermission: boolean;
10+
noPermissionTooltip?: ReactNode;
11+
};
12+
13+
/**
14+
* A `LinkButton` that disables itself and shows an explanatory tooltip when the
15+
* user lacks permission. Display only — the server route builder's
16+
* `authorization` block is the real gate. Unlike `Button`, `LinkButton` has no
17+
* tooltip support and renders a `pointer-events-none` element when disabled
18+
* (which can't be hovered), so the denied state renders a `SimpleTooltip`
19+
* around a non-interactive `ButtonContent` instead — the same pattern the team
20+
* settings page uses for its gated controls.
21+
*/
22+
export function PermissionLink({
23+
hasPermission,
24+
noPermissionTooltip,
25+
...props
26+
}: PermissionLinkProps) {
27+
if (hasPermission) {
28+
return <LinkButton {...props} />;
29+
}
30+
31+
return (
32+
<SimpleTooltip
33+
button={
34+
<ButtonContent
35+
{...(props as ButtonContentPropsType)}
36+
className={cn(props.className, "cursor-not-allowed opacity-50")}
37+
/>
38+
}
39+
content={noPermissionTooltip ?? DEFAULT_NO_PERMISSION_TOOLTIP}
40+
disableHoverableContent
41+
/>
42+
);
43+
}

0 commit comments

Comments
 (0)