Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 453a8ac15d | |||
| 9aac7ecfc5 | |||
| 6e08d9c5a2 | |||
| d75fefba27 | |||
| a72a0cd29c | |||
| 1f0280e6fb | |||
| b7d8918d60 | |||
| e7dbbcde87 | |||
| bbf7b137da |
@@ -61,10 +61,10 @@ func NewWithCommand(
|
||||
t testing.TB, cmd *serpent.Command, args ...string,
|
||||
) (*serpent.Invocation, config.Root) {
|
||||
configDir := config.Root(t.TempDir())
|
||||
// Keyring usage is disabled here because many existing tests expect the session token
|
||||
// to be stored on disk and is not properly instrumented for parallel testing against
|
||||
// the actual operating system keyring.
|
||||
invArgs := append([]string{"--global-config", string(configDir), "--use-keyring=false"}, args...)
|
||||
// Keyring usage is disabled here when --global-config is set because many existing
|
||||
// tests expect the session token to be stored on disk and is not properly instrumented
|
||||
// for parallel testing against the actual operating system keyring.
|
||||
invArgs := append([]string{"--global-config", string(configDir)}, args...)
|
||||
return setupInvocation(t, cmd, invArgs...), configDir
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ func setupKeyringTestEnv(t *testing.T, clientURL string, args ...string) keyring
|
||||
|
||||
serviceName := keyringTestServiceName(t)
|
||||
root.WithKeyringServiceName(serviceName)
|
||||
root.UseKeyringWithGlobalConfig()
|
||||
|
||||
inv, cfg := clitest.NewWithDefaultKeyringCommand(t, cmd, args...)
|
||||
|
||||
@@ -169,6 +170,7 @@ func TestUseKeyring(t *testing.T) {
|
||||
logoutCmd, err := logoutRoot.Command(logoutRoot.AGPL())
|
||||
require.NoError(t, err)
|
||||
logoutRoot.WithKeyringServiceName(env.serviceName)
|
||||
logoutRoot.UseKeyringWithGlobalConfig()
|
||||
|
||||
logoutInv, _ := clitest.NewWithDefaultKeyringCommand(t, logoutCmd,
|
||||
"logout",
|
||||
|
||||
+23
-9
@@ -483,9 +483,9 @@ func (r *RootCmd) Command(subcommands []*serpent.Command) (*serpent.Command, err
|
||||
Flag: varUseKeyring,
|
||||
Env: envUseKeyring,
|
||||
Description: "Store and retrieve session tokens using the operating system " +
|
||||
"keyring. Enabled by default. If the keyring is not supported on the " +
|
||||
"current platform, file-based storage is used automatically. Set to " +
|
||||
"false to force file-based storage.",
|
||||
"keyring. This flag is ignored and file-based storage is used when " +
|
||||
"--global-config is set or keyring usage is not supported on the current " +
|
||||
"platform. Set to false to force file-based storage on supported platforms.",
|
||||
Default: "true",
|
||||
Value: serpent.BoolOf(&r.useKeyring),
|
||||
Group: globalGroup,
|
||||
@@ -536,11 +536,12 @@ type RootCmd struct {
|
||||
disableDirect bool
|
||||
debugHTTP bool
|
||||
|
||||
disableNetworkTelemetry bool
|
||||
noVersionCheck bool
|
||||
noFeatureWarning bool
|
||||
useKeyring bool
|
||||
keyringServiceName string
|
||||
disableNetworkTelemetry bool
|
||||
noVersionCheck bool
|
||||
noFeatureWarning bool
|
||||
useKeyring bool
|
||||
keyringServiceName string
|
||||
useKeyringWithGlobalConfig bool
|
||||
}
|
||||
|
||||
// InitClient creates and configures a new client with authentication, telemetry,
|
||||
@@ -721,8 +722,14 @@ func (r *RootCmd) createUnauthenticatedClient(ctx context.Context, serverURL *ur
|
||||
// flag.
|
||||
func (r *RootCmd) ensureTokenBackend() sessionstore.Backend {
|
||||
if r.tokenBackend == nil {
|
||||
// Checking for the --global-config directory being set is a bit wonky but necessary
|
||||
// to allow extensions that invoke the CLI with this flag (e.g. VS code) to continue
|
||||
// working without modification. In the future we should modify these extensions to
|
||||
// either access the credential in the keyring (like Coder Desktop) or some other
|
||||
// approach that doesn't rely on the session token being stored on disk.
|
||||
assumeExtensionInUse := r.globalConfig != config.DefaultDir() && !r.useKeyringWithGlobalConfig
|
||||
keyringSupported := runtime.GOOS == "windows" || runtime.GOOS == "darwin"
|
||||
if r.useKeyring && keyringSupported {
|
||||
if r.useKeyring && !assumeExtensionInUse && keyringSupported {
|
||||
serviceName := sessionstore.DefaultServiceName
|
||||
if r.keyringServiceName != "" {
|
||||
serviceName = r.keyringServiceName
|
||||
@@ -742,6 +749,13 @@ func (r *RootCmd) WithKeyringServiceName(serviceName string) {
|
||||
r.keyringServiceName = serviceName
|
||||
}
|
||||
|
||||
// UseKeyringWithGlobalConfig enables the use of the keyring storage backend
|
||||
// when the --global-config directory is set. This is only intended as an override
|
||||
// for tests, which require specifying the global config directory for test isolation.
|
||||
func (r *RootCmd) UseKeyringWithGlobalConfig() {
|
||||
r.useKeyringWithGlobalConfig = true
|
||||
}
|
||||
|
||||
type AgentAuth struct {
|
||||
// Agent Client config
|
||||
agentToken string
|
||||
|
||||
Vendored
+4
-3
@@ -111,9 +111,10 @@ variables or flags.
|
||||
|
||||
--use-keyring bool, $CODER_USE_KEYRING (default: true)
|
||||
Store and retrieve session tokens using the operating system keyring.
|
||||
Enabled by default. If the keyring is not supported on the current
|
||||
platform, file-based storage is used automatically. Set to false to
|
||||
force file-based storage.
|
||||
This flag is ignored and file-based storage is used when
|
||||
--global-config is set or keyring usage is not supported on the
|
||||
current platform. Set to false to force file-based storage on
|
||||
supported platforms.
|
||||
|
||||
-v, --verbose bool, $CODER_VERBOSE
|
||||
Enable verbose output.
|
||||
|
||||
@@ -23751,6 +23751,7 @@ SET
|
||||
WHERE
|
||||
template_id = $3
|
||||
AND dormant_at IS NOT NULL
|
||||
AND deleted = false
|
||||
-- Prebuilt workspaces (identified by having the prebuilds system user as owner_id)
|
||||
-- should not have their dormant or deleting at set, as these are handled by the
|
||||
-- prebuilds reconciliation loop.
|
||||
|
||||
@@ -846,6 +846,7 @@ SET
|
||||
WHERE
|
||||
template_id = @template_id
|
||||
AND dormant_at IS NOT NULL
|
||||
AND deleted = false
|
||||
-- Prebuilt workspaces (identified by having the prebuilds system user as owner_id)
|
||||
-- should not have their dormant or deleting at set, as these are handled by the
|
||||
-- prebuilds reconciliation loop.
|
||||
|
||||
Generated
+1
-1
@@ -179,7 +179,7 @@ Disable network telemetry. Network telemetry is collected when connecting to wor
|
||||
| Environment | <code>$CODER_USE_KEYRING</code> |
|
||||
| Default | <code>true</code> |
|
||||
|
||||
Store and retrieve session tokens using the operating system keyring. Enabled by default. If the keyring is not supported on the current platform, file-based storage is used automatically. Set to false to force file-based storage.
|
||||
Store and retrieve session tokens using the operating system keyring. This flag is ignored and file-based storage is used when --global-config is set or keyring usage is not supported on the current platform. Set to false to force file-based storage on supported platforms.
|
||||
|
||||
### --global-config
|
||||
|
||||
|
||||
+4
-3
@@ -70,9 +70,10 @@ variables or flags.
|
||||
|
||||
--use-keyring bool, $CODER_USE_KEYRING (default: true)
|
||||
Store and retrieve session tokens using the operating system keyring.
|
||||
Enabled by default. If the keyring is not supported on the current
|
||||
platform, file-based storage is used automatically. Set to false to
|
||||
force file-based storage.
|
||||
This flag is ignored and file-based storage is used when
|
||||
--global-config is set or keyring usage is not supported on the
|
||||
current platform. Set to false to force file-based storage on
|
||||
supported platforms.
|
||||
|
||||
-v, --verbose bool, $CODER_VERBOSE
|
||||
Enable verbose output.
|
||||
|
||||
@@ -737,6 +737,105 @@ func TestNotifications(t *testing.T) {
|
||||
require.Contains(t, sent[i].Targets, dormantWs.OwnerID)
|
||||
}
|
||||
})
|
||||
|
||||
// Regression test for https://github.com/coder/coder/issues/20913
|
||||
// Deleted workspaces should not receive dormancy notifications.
|
||||
t.Run("DeletedWorkspacesNotNotified", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
db, _ = dbtestutil.NewDB(t)
|
||||
ctx = testutil.Context(t, testutil.WaitLong)
|
||||
user = dbgen.User(t, db, database.User{})
|
||||
file = dbgen.File(t, db, database.File{
|
||||
CreatedBy: user.ID,
|
||||
})
|
||||
templateJob = dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
|
||||
FileID: file.ID,
|
||||
InitiatorID: user.ID,
|
||||
Tags: database.StringMap{
|
||||
"foo": "bar",
|
||||
},
|
||||
})
|
||||
timeTilDormant = time.Minute * 2
|
||||
templateVersion = dbgen.TemplateVersion(t, db, database.TemplateVersion{
|
||||
CreatedBy: user.ID,
|
||||
JobID: templateJob.ID,
|
||||
OrganizationID: templateJob.OrganizationID,
|
||||
})
|
||||
template = dbgen.Template(t, db, database.Template{
|
||||
ActiveVersionID: templateVersion.ID,
|
||||
CreatedBy: user.ID,
|
||||
OrganizationID: templateJob.OrganizationID,
|
||||
TimeTilDormant: int64(timeTilDormant),
|
||||
TimeTilDormantAutoDelete: int64(timeTilDormant),
|
||||
})
|
||||
)
|
||||
|
||||
// Create a dormant workspace that is NOT deleted.
|
||||
activeDormantWorkspace := dbgen.Workspace(t, db, database.WorkspaceTable{
|
||||
OwnerID: user.ID,
|
||||
TemplateID: template.ID,
|
||||
OrganizationID: templateJob.OrganizationID,
|
||||
LastUsedAt: time.Now().Add(-time.Hour),
|
||||
})
|
||||
_, err := db.UpdateWorkspaceDormantDeletingAt(ctx, database.UpdateWorkspaceDormantDeletingAtParams{
|
||||
ID: activeDormantWorkspace.ID,
|
||||
DormantAt: sql.NullTime{
|
||||
Time: activeDormantWorkspace.LastUsedAt.Add(timeTilDormant),
|
||||
Valid: true,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a dormant workspace that IS deleted.
|
||||
deletedDormantWorkspace := dbgen.Workspace(t, db, database.WorkspaceTable{
|
||||
OwnerID: user.ID,
|
||||
TemplateID: template.ID,
|
||||
OrganizationID: templateJob.OrganizationID,
|
||||
LastUsedAt: time.Now().Add(-time.Hour),
|
||||
Deleted: true, // Mark as deleted
|
||||
})
|
||||
_, err = db.UpdateWorkspaceDormantDeletingAt(ctx, database.UpdateWorkspaceDormantDeletingAtParams{
|
||||
ID: deletedDormantWorkspace.ID,
|
||||
DormantAt: sql.NullTime{
|
||||
Time: deletedDormantWorkspace.LastUsedAt.Add(timeTilDormant),
|
||||
Valid: true,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Setup dependencies
|
||||
notifyEnq := notificationstest.NewFakeEnqueuer()
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
|
||||
const userQuietHoursSchedule = "CRON_TZ=UTC 0 0 * * *" // midnight UTC
|
||||
userQuietHoursStore, err := schedule.NewEnterpriseUserQuietHoursScheduleStore(userQuietHoursSchedule, true)
|
||||
require.NoError(t, err)
|
||||
userQuietHoursStorePtr := &atomic.Pointer[agplschedule.UserQuietHoursScheduleStore]{}
|
||||
userQuietHoursStorePtr.Store(&userQuietHoursStore)
|
||||
templateScheduleStore := schedule.NewEnterpriseTemplateScheduleStore(userQuietHoursStorePtr, notifyEnq, logger, nil)
|
||||
|
||||
// Lower the dormancy TTL to ensure the schedule recalculates deadlines and
|
||||
// triggers notifications.
|
||||
_, err = templateScheduleStore.Set(dbauthz.AsNotifier(ctx), db, template, agplschedule.TemplateScheduleOptions{
|
||||
TimeTilDormant: timeTilDormant / 2,
|
||||
TimeTilDormantAutoDelete: timeTilDormant / 2,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// We should only receive a notification for the non-deleted dormant workspace.
|
||||
sent := notifyEnq.Sent()
|
||||
require.Len(t, sent, 1, "expected exactly 1 notification for the non-deleted workspace")
|
||||
require.Equal(t, sent[0].UserID, activeDormantWorkspace.OwnerID)
|
||||
require.Equal(t, sent[0].TemplateID, notifications.TemplateWorkspaceMarkedForDeletion)
|
||||
require.Contains(t, sent[0].Targets, activeDormantWorkspace.ID)
|
||||
|
||||
// Ensure the deleted workspace was NOT notified
|
||||
for _, notification := range sent {
|
||||
require.NotContains(t, notification.Targets, deletedDormantWorkspace.ID,
|
||||
"deleted workspace should not receive notifications")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestTemplateTTL(t *testing.T) {
|
||||
|
||||
+124
-22
@@ -1,5 +1,17 @@
|
||||
# Frontend Development Guidelines
|
||||
|
||||
## Critical Pattern Migrations (MUST FOLLOW)
|
||||
|
||||
The following patterns are actively being migrated and have **STRICT policies**:
|
||||
|
||||
1. **Emotion → Tailwind**: "No new emotion styles, full stop" - Always use Tailwind CSS
|
||||
2. **MUI Components → Custom/Radix Components**: Replace MUI components (Tooltips, Tables, Buttons) with custom/shadcn equivalents
|
||||
3. **MUI Icons → lucide-react**: All icons must use lucide-react, never MUI icons
|
||||
4. **spyOn → queries parameter**: Use `queries` in story parameters for GET endpoint mocks
|
||||
5. **localStorage → user_configs**: Store user preferences in backend, not browser storage
|
||||
|
||||
When touching existing code, **"leave the campsite better than you found it"** - refactor old patterns to new ones even if not directly related to your changes.
|
||||
|
||||
## TypeScript LSP Navigation (USE FIRST)
|
||||
|
||||
When investigating or editing TypeScript/React code, always use the TypeScript language server tools for accurate navigation:
|
||||
@@ -26,25 +38,84 @@ When investigating or editing TypeScript/React code, always use the TypeScript l
|
||||
|
||||
## Components
|
||||
|
||||
- MUI components are deprecated - migrate away from these when encountered
|
||||
- Use shadcn/ui components first - check `site/src/components` for existing implementations.
|
||||
- **MUI components are deprecated** - migrate away from these when encountered
|
||||
- Replace `@mui/material/Tooltip` with custom `Tooltip` component (Radix-based)
|
||||
- Default 100ms delay via global tooltip provider
|
||||
- Use `delayDuration={0}` when immediate tooltip needed
|
||||
- Replace MUI Tables with custom table components
|
||||
- Replace MUI Buttons with shadcn Button components
|
||||
- Systematically replace MUI components with custom/shadcn equivalents
|
||||
- Use shadcn/ui components first - check `site/src/components` for existing implementations
|
||||
- Do not use shadcn CLI - manually add components to maintain consistency
|
||||
- The modules folder should contain components with business logic specific to the codebase.
|
||||
- The modules folder should contain components with business logic specific to the codebase
|
||||
- Create custom components only when shadcn alternatives don't exist
|
||||
|
||||
### Icon Migration: MUI Icons → lucide-react
|
||||
|
||||
Never import from `@mui/icons-material`. Use `lucide-react` instead.
|
||||
|
||||
```tsx
|
||||
import { Building2Icon, UsersIcon, GlobeIcon, UserIcon } from "lucide-react";
|
||||
```
|
||||
|
||||
**Common replacements:** `BusinessIcon` → `Building2Icon`, `GroupIcon` → `UsersIcon`, `PublicIcon` → `GlobeIcon`, `PersonIcon` → `UserIcon`
|
||||
|
||||
### MUI → Radix Component Prop Naming
|
||||
|
||||
When migrating from MUI to Radix components, use Radix naming conventions:
|
||||
|
||||
```tsx
|
||||
<Tooltip side="top">
|
||||
<TooltipTrigger>Hover me</TooltipTrigger>
|
||||
<TooltipContent>Tooltip text</TooltipContent>
|
||||
</Tooltip>
|
||||
```
|
||||
|
||||
**Prop changes from MUI:** Use `side` instead of `placement`, remove `PopperProps`, and use `TooltipContent` children instead of `title` prop.
|
||||
|
||||
## Styling
|
||||
|
||||
- Emotion CSS is deprecated. Use Tailwind CSS instead.
|
||||
- Use custom Tailwind classes in tailwind.config.js.
|
||||
- **Emotion CSS is STRICTLY DEPRECATED: "no new emotion styles, full stop"**
|
||||
- Never use `@emotion/react`, `css` prop, `useTheme()`, or emotion styled components
|
||||
- Always use Tailwind CSS utility classes instead
|
||||
- Use custom Tailwind classes in tailwind.config.js
|
||||
- Tailwind CSS reset is currently not used to maintain compatibility with MUI
|
||||
- Responsive design - use Tailwind's responsive prefixes (sm:, md:, lg:, xl:)
|
||||
- Do not use `dark:` prefix for dark mode
|
||||
|
||||
### Common Emotion → Tailwind Migrations
|
||||
|
||||
Use Tailwind CSS utility classes:
|
||||
|
||||
```tsx
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-6">
|
||||
<span className="font-medium text-content-primary">
|
||||
Content here
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Common replacements:**
|
||||
|
||||
- Replace `css={visuallyHidden}` with `className="sr-only"`
|
||||
- Replace `Stack` component with Tailwind flex (`flex`, `flex-col`, `gap-*`)
|
||||
- Replace emotion `css` prop and theme colors with Tailwind utilities (`text-content-primary`, `bg-surface-secondary`, `border-border-default`)
|
||||
- Use lucide-react icons with `size-icon-sm`, `size-icon-xs` classes
|
||||
|
||||
## Tailwind Best Practices
|
||||
|
||||
- Group related classes
|
||||
- Use semantic color names from the theme inside `tailwind.config.js` including `content`, `surface`, `border`, `highlight` semantic tokens
|
||||
- Prefer Tailwind utilities over custom CSS when possible
|
||||
- For conditional classes, use the `cn()` utility (from `utils/cn`) which combines `clsx` and `tailwind-merge`
|
||||
|
||||
```tsx
|
||||
import { cn } from "utils/cn";
|
||||
|
||||
<div className={cn("base-classes", condition && "conditional-classes", className)} />
|
||||
```
|
||||
|
||||
## General Code style
|
||||
|
||||
@@ -53,6 +124,54 @@ When investigating or editing TypeScript/React code, always use the TypeScript l
|
||||
- Prefer `for...of` over `forEach` for iteration
|
||||
- **Biome** handles both linting and formatting (not ESLint/Prettier)
|
||||
|
||||
## Testing Patterns
|
||||
|
||||
### Storybook: queries parameter for GET endpoint mocks
|
||||
|
||||
**PREFERRED PATTERN**: For GET endpoint mocks in Storybook stories, use `queries` parameter instead of `spyOn`.
|
||||
|
||||
```tsx
|
||||
// Use queries parameter pattern
|
||||
parameters: {
|
||||
queries: [
|
||||
{
|
||||
key: usersKey({ q: "" }),
|
||||
data: {
|
||||
users: MockUsers,
|
||||
count: MockUsers.length,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: getTemplatesQueryKey({ q: "has-ai-task:true" }),
|
||||
data: [MockTemplate],
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
**Important notes:**
|
||||
|
||||
- This applies specifically to GET endpoint mocks in Storybook stories
|
||||
- `spyOn` is still appropriate for other mock types (POST, PUT, DELETE, non-GET endpoints)
|
||||
- Must import the correct query key functions (e.g., `usersKey`, `getTemplatesQueryKey`)
|
||||
|
||||
### Chromatic/Storybook Testing Best Practices
|
||||
|
||||
- Prefer visual validation through Chromatic snapshots over programmatic assertions
|
||||
- Remove redundant assertions that duplicate snapshot validation
|
||||
- Stories are snapshot tests - rely on screenshots to verify correctness
|
||||
|
||||
## State Storage
|
||||
|
||||
### localStorage vs user_configs table
|
||||
|
||||
**IMPORTANT**: Use `user_configs` table for user preferences, NOT `localStorage`.
|
||||
|
||||
- localStorage is browser-specific; user preferences should persist across devices
|
||||
- Follow `theme_preference` as reference implementation
|
||||
- Use localStorage only for truly transient UI state
|
||||
- **Key principle**: User preferences should be tied to their account, not their browser
|
||||
|
||||
## Workflow
|
||||
|
||||
- Be sure to typecheck when you're done making a series of code changes
|
||||
@@ -69,23 +188,6 @@ When investigating or editing TypeScript/React code, always use the TypeScript l
|
||||
4. `pnpm test` - Run affected unit tests
|
||||
5. Visual check in Storybook if component changes
|
||||
|
||||
## Migration (MUI → shadcn) (Emotion → Tailwind)
|
||||
|
||||
### Migration Strategy
|
||||
|
||||
- Identify MUI components in current feature
|
||||
- Find shadcn equivalent in existing components
|
||||
- Create wrapper if needed for missing functionality
|
||||
- Update tests to reflect new component structure
|
||||
- Remove MUI imports once migration complete
|
||||
|
||||
### Migration Guidelines
|
||||
|
||||
- Use Tailwind classes for all new styling
|
||||
- Replace Emotion `css` prop with Tailwind classes
|
||||
- Leverage custom color tokens: `content-primary`, `surface-secondary`, etc.
|
||||
- Use `className` with `clsx` for conditional styling
|
||||
|
||||
## React Rules
|
||||
|
||||
### 1. Purity & Immutability
|
||||
|
||||
@@ -10,7 +10,7 @@ import { withAuthProvider, withProxyProvider } from "testHelpers/storybook";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { API } from "api/api";
|
||||
import { MockUsers } from "pages/UsersPage/storybookData/users";
|
||||
import { expect, spyOn, userEvent, within } from "storybook/test";
|
||||
import { expect, spyOn, userEvent, waitFor, within } from "storybook/test";
|
||||
import { getTemplatesQueryKey } from "../../api/queries/templates";
|
||||
import TasksPage from "./TasksPage";
|
||||
|
||||
@@ -145,6 +145,29 @@ export const LoadedTasksWaitingForInputTab: Story = {
|
||||
spyOn(API, "getTasks").mockResolvedValue([
|
||||
{
|
||||
...firstTask,
|
||||
id: "active-idle-task",
|
||||
display_name: "Active Idle Task",
|
||||
status: "active",
|
||||
current_state: {
|
||||
...firstTask.current_state,
|
||||
state: "idle",
|
||||
},
|
||||
},
|
||||
{
|
||||
...firstTask,
|
||||
id: "paused-idle-task",
|
||||
display_name: "Paused Idle Task",
|
||||
status: "paused",
|
||||
current_state: {
|
||||
...firstTask.current_state,
|
||||
state: "idle",
|
||||
},
|
||||
},
|
||||
{
|
||||
...firstTask,
|
||||
id: "error-idle-task",
|
||||
display_name: "Error Idle Task",
|
||||
status: "error",
|
||||
current_state: {
|
||||
...firstTask.current_state,
|
||||
state: "idle",
|
||||
@@ -161,6 +184,23 @@ export const LoadedTasksWaitingForInputTab: Story = {
|
||||
name: /waiting for input/i,
|
||||
});
|
||||
await userEvent.click(waitingForInputTab);
|
||||
|
||||
// Wait for the table to update after tab switch
|
||||
await waitFor(async () => {
|
||||
const table = canvas.getByRole("table");
|
||||
const tableContent = within(table);
|
||||
|
||||
// Active idle task should be visible
|
||||
expect(tableContent.getByText("Active Idle Task")).toBeInTheDocument();
|
||||
|
||||
// Only active idle tasks should be visible in the table
|
||||
expect(
|
||||
tableContent.queryByText("Paused Idle Task"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
tableContent.queryByText("Error Idle Task"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -44,7 +44,7 @@ const TasksPage: FC = () => {
|
||||
refetchInterval: 10_000,
|
||||
});
|
||||
const idleTasks = tasksQuery.data?.filter(
|
||||
(task) => task.current_state?.state === "idle",
|
||||
(task) => task.status === "active" && task.current_state?.state === "idle",
|
||||
);
|
||||
const displayedTasks =
|
||||
tab.value === "waiting-for-input" ? idleTasks : tasksQuery.data;
|
||||
|
||||
Reference in New Issue
Block a user