From e8bb034d78aeac96caf3c5daa809f75cb8c17eb3 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Fri, 22 Sep 2023 19:14:28 +0000 Subject: [PATCH 1/5] better story for resource metadata designs --- .../Resources/ResourceCard.stories.tsx | 95 +++++------ .../Resources/Resources.stories.tsx | 148 ++++++++++++++++++ site/src/testHelpers/entities.ts | 51 +++--- 3 files changed, 202 insertions(+), 92 deletions(-) create mode 100644 site/src/components/Resources/Resources.stories.tsx diff --git a/site/src/components/Resources/ResourceCard.stories.tsx b/site/src/components/Resources/ResourceCard.stories.tsx index d1be96424275a..2759ab4bd5230 100644 --- a/site/src/components/Resources/ResourceCard.stories.tsx +++ b/site/src/components/Resources/ResourceCard.stories.tsx @@ -8,41 +8,14 @@ import { AgentRow } from "./AgentRow"; import { ResourceCard } from "./ResourceCard"; import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext"; import type { Meta, StoryObj } from "@storybook/react"; +import { type WorkspaceAgent } from "api/typesGenerated"; const meta: Meta = { - title: "components/ResourceCard", + title: "components/Resources/ResourceCard", component: ResourceCard, args: { resource: MockWorkspaceResource, - agentRow: (agent) => ( - { - return; - }, - clearProxy: () => { - return; - }, - refetchProxyLatencies: (): Date => { - return new Date(); - }, - }} - > - - - ), + agentRow: getAgentRow, }, }; @@ -96,34 +69,38 @@ export const BunchOfMetadata: Story = { }, ], }, - agentRow: (agent) => ( - { - return; - }, - clearProxy: () => { - return; - }, - refetchProxyLatencies: (): Date => { - return new Date(); - }, - }} - > - - - ), + agentRow: getAgentRow, }, }; + +function getAgentRow(agent: WorkspaceAgent): JSX.Element { + return ( + { + return; + }, + clearProxy: () => { + return; + }, + refetchProxyLatencies: (): Date => { + return new Date(); + }, + }} + > + + + ); +} diff --git a/site/src/components/Resources/Resources.stories.tsx b/site/src/components/Resources/Resources.stories.tsx new file mode 100644 index 0000000000000..6a48d1b906fa7 --- /dev/null +++ b/site/src/components/Resources/Resources.stories.tsx @@ -0,0 +1,148 @@ +import { action } from "@storybook/addon-actions"; +import { + MockProxyLatencies, + MockWorkspace, + MockWorkspaceResource, +} from "testHelpers/entities"; +import { AgentRow } from "./AgentRow"; +import { Resources } from "./Resources"; +import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext"; +import type { Meta, StoryObj } from "@storybook/react"; +import { type WorkspaceAgent } from "api/typesGenerated"; + +const meta: Meta = { + title: "components/Resources/Resources", + component: Resources, + args: { + resources: [MockWorkspaceResource], + agentRow: getAgentRow, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Example: Story = {}; + +const nullDevice = { + created_at: "", + job_id: "", + workspace_transition: "start", + type: "null_resource", + hide: false, + icon: "", + daily_cost: 0, +} as const; + +const short = { + key: "Short", + value: "Hi!", + sensitive: false, +}; +const long = { + key: "Long", + value: "The quick brown fox jumped over the lazy dog", + sensitive: false, +}; +const reallyLong = { + key: "Really long", + value: + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + sensitive: false, +}; + +export const BunchOfDevicesWithMetadata: Story = { + args: { + resources: [ + MockWorkspaceResource, + { + ...nullDevice, + id: "e8c846da", + name: "device1", + metadata: [short], + }, + { + ...nullDevice, + id: "a1b11343", + name: "device3", + metadata: [long], + }, + { + ...nullDevice, + id: "09ab7e8c", + name: "device3", + metadata: [reallyLong], + }, + { + ...nullDevice, + id: "0a09fa91", + name: "device2", + metadata: Array.from({ length: 8 }, (_, i) => ({ + ...short, + key: `Short ${i}`, + })), + }, + { + ...nullDevice, + id: "d0b9eb9d", + name: "device4", + metadata: Array.from({ length: 4 }, (_, i) => ({ + ...long, + key: `Short ${i}`, + })), + }, + { + ...nullDevice, + id: "a6c69587", + name: "device5", + metadata: Array.from({ length: 8 }, (_, i) => + i % 2 === 0 + ? { ...short, key: `Short ${i}` } + : { ...long, key: `Long ${i}` }, + ), + }, + { + ...nullDevice, + id: "3af84e31", + name: "device6", + metadata: Array.from({ length: 8 }, (_, i) => ({ + ...reallyLong, + key: `Really long ${i}`, + })), + }, + ], + agentRow: getAgentRow, + }, +}; + +function getAgentRow(agent: WorkspaceAgent): JSX.Element { + return ( + { + return; + }, + clearProxy: () => { + return; + }, + refetchProxyLatencies: (): Date => { + return new Date(); + }, + }} + > + + + ); +} diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index ace4333128026..46f94fe84b4cc 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -718,33 +718,11 @@ export const MockWorkspaceAgentOff: TypesGen.WorkspaceAgent = { }; export const MockWorkspaceResource: TypesGen.WorkspaceResource = { - agents: [ - MockWorkspaceAgent, - MockWorkspaceAgentConnecting, - MockWorkspaceAgentOutdated, - ], - created_at: "", id: "test-workspace-resource", - job_id: "", name: "a-workspace-resource", - type: "google_compute_disk", - workspace_transition: "start", - hide: false, - icon: "", - metadata: [{ key: "api_key", value: "12345678", sensitive: true }], - daily_cost: 10, -}; - -export const MockWorkspaceResource2: TypesGen.WorkspaceResource = { - agents: [ - MockWorkspaceAgent, - MockWorkspaceAgentDisconnected, - MockWorkspaceAgentOutdated, - ], + agents: [MockWorkspaceAgent], created_at: "", - id: "test-workspace-resource-2", job_id: "", - name: "another-workspace-resource", type: "google_compute_disk", workspace_transition: "start", hide: false, @@ -753,22 +731,29 @@ export const MockWorkspaceResource2: TypesGen.WorkspaceResource = { daily_cost: 10, }; -export const MockWorkspaceResource3: TypesGen.WorkspaceResource = { +export const MockWorkspaceResourceSensitive: TypesGen.WorkspaceResource = { + ...MockWorkspaceResource, + id: "test-workspace-resource-sensitive", + name: "workspace-resource-sensitive", + metadata: [{ key: "api_key", value: "12345678", sensitive: true }], +}; + +export const MockWorkspaceResourceMultipleAgents: TypesGen.WorkspaceResource = { + ...MockWorkspaceResource, + id: "test-workspace-resource-multiple-agents", + name: "workspace-resource-multiple-agents", agents: [ MockWorkspaceAgent, MockWorkspaceAgentDisconnected, MockWorkspaceAgentOutdated, ], - created_at: "", - id: "test-workspace-resource-3", - job_id: "", - name: "another-workspace-resource", - type: "google_compute_disk", - workspace_transition: "start", +}; + +export const MockWorkspaceResourceHidden: TypesGen.WorkspaceResource = { + ...MockWorkspaceResource, + id: "test-workspace-resource-hidden", + name: "workspace-resource-hidden", hide: true, - icon: "", - metadata: [{ key: "size", value: "32GB", sensitive: false }], - daily_cost: 20, }; export const MockWorkspaceAutostartDisabled: TypesGen.UpdateWorkspaceAutostartRequest = From 185b8b0b5887aa02c6e57b615b7c0d48ade4b73f Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Fri, 22 Sep 2023 20:45:28 +0000 Subject: [PATCH 2/5] allow single metadata items to fill entire grid width --- .../src/components/Resources/ResourceCard.tsx | 104 +++++++++--------- 1 file changed, 51 insertions(+), 53 deletions(-) diff --git a/site/src/components/Resources/ResourceCard.tsx b/site/src/components/Resources/ResourceCard.tsx index f6cec21e5669e..1f2f71532cd8b 100644 --- a/site/src/components/Resources/ResourceCard.tsx +++ b/site/src/components/Resources/ResourceCard.tsx @@ -10,8 +10,8 @@ import { } from "components/DropdownArrows/DropdownArrows"; import IconButton from "@mui/material/IconButton"; import Tooltip from "@mui/material/Tooltip"; -import { Maybe } from "components/Conditionals/Maybe"; import { CopyableValue } from "components/CopyableValue/CopyableValue"; +import { type Theme } from "@mui/material"; export interface ResourceCardProps { resource: WorkspaceResource; @@ -21,7 +21,7 @@ export interface ResourceCardProps { export const ResourceCard: FC = ({ resource, agentRow }) => { const [shouldDisplayAllMetadata, setShouldDisplayAllMetadata] = useState(false); - const styles = useStyles(); + const styles = useStyles({ metadataLength: resource.metadata?.length ?? 0 }); const metadataToDisplay = resource.metadata ?? []; const visibleMetadata = shouldDisplayAllMetadata ? metadataToDisplay @@ -49,57 +49,52 @@ export const ResourceCard: FC = ({ resource, agentRow }) => { - -
- {resource.daily_cost > 0 && ( -
-
- cost -
+
+ {resource.daily_cost > 0 && ( +
+
+ cost +
+
{resource.daily_cost}
+
+ )} + {visibleMetadata.map((meta) => { + return ( +
+
{meta.key}
- {resource.daily_cost} + {meta.sensitive ? ( + + ) : ( + + {meta.value} + + )}
- )} - {visibleMetadata.map((meta) => { - return ( -
-
{meta.key}
-
- {meta.sensitive ? ( - - ) : ( - - {meta.value} - - )} -
-
- ); - })} -
- - 4}> - + {metadataToDisplay.length > 4 && ( + + { + setShouldDisplayAllMetadata((value) => !value); + }} + size="large" > - { - setShouldDisplayAllMetadata((value) => !value); - }} - size="large" - > - {shouldDisplayAllMetadata ? ( - - ) : ( - - )} - - - - + {shouldDisplayAllMetadata ? ( + + ) : ( + + )} + + + )} {resource.agents && resource.agents.length > 0 && ( @@ -109,7 +104,7 @@ export const ResourceCard: FC = ({ resource, agentRow }) => { ); }; -const useStyles = makeStyles((theme) => ({ +const useStyles = makeStyles((theme) => ({ resourceCard: { background: theme.palette.background.paper, borderRadius: theme.shape.borderRadius, @@ -141,12 +136,15 @@ const useStyles = makeStyles((theme) => ({ }, }, - metadataHeader: { + metadataHeader: (props) => ({ + flexGrow: 2, display: "grid", - gridTemplateColumns: "repeat(4, minmax(0, 1fr))", + gridTemplateColumns: `repeat(${ + props.metadataLength === 1 ? 1 : 4 + }, minmax(0, 1fr))`, gap: theme.spacing(5), rowGap: theme.spacing(3), - }, + }), metadata: { ...theme.typography.body2, From 05d2c0a172af8f0572561e240d8a0c6ea426f590 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Fri, 22 Sep 2023 20:55:16 +0000 Subject: [PATCH 3/5] minor tweaks to `ResourceCard` layout --- .../src/components/Resources/ResourceCard.tsx | 12 ++++++++-- .../Resources/Resources.stories.tsx | 23 ++++++++++++------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/site/src/components/Resources/ResourceCard.tsx b/site/src/components/Resources/ResourceCard.tsx index 1f2f71532cd8b..4d58ace7e204f 100644 --- a/site/src/components/Resources/ResourceCard.tsx +++ b/site/src/components/Resources/ResourceCard.tsx @@ -21,12 +21,20 @@ export interface ResourceCardProps { export const ResourceCard: FC = ({ resource, agentRow }) => { const [shouldDisplayAllMetadata, setShouldDisplayAllMetadata] = useState(false); - const styles = useStyles({ metadataLength: resource.metadata?.length ?? 0 }); const metadataToDisplay = resource.metadata ?? []; const visibleMetadata = shouldDisplayAllMetadata ? metadataToDisplay : metadataToDisplay.slice(0, 4); + // Add one to `metadataLength` if the resource has a cost, and hide one + // additional metadata item, because cost is displayed in the same grid. + let metadataLength = resource.metadata?.length ?? 0; + if (resource.daily_cost > 0) { + metadataLength += 1; + visibleMetadata.pop(); + } + const styles = useStyles({ metadataLength }); + return (
= ({ resource, agentRow }) => { ); })}
- {metadataToDisplay.length > 4 && ( + {metadataLength > 4 && ( ; export const Example: Story = {}; +export const MultipleAgents: Story = { + args: { + resources: [MockWorkspaceResourceMultipleAgents], + }, +}; + const nullDevice = { created_at: "", job_id: "", @@ -58,25 +65,25 @@ export const BunchOfDevicesWithMetadata: Story = { { ...nullDevice, id: "e8c846da", - name: "device1", + name: "Short", metadata: [short], }, { ...nullDevice, id: "a1b11343", - name: "device3", + name: "Long", metadata: [long], }, { ...nullDevice, id: "09ab7e8c", - name: "device3", + name: "Really long", metadata: [reallyLong], }, { ...nullDevice, id: "0a09fa91", - name: "device2", + name: "Many short", metadata: Array.from({ length: 8 }, (_, i) => ({ ...short, key: `Short ${i}`, @@ -85,16 +92,16 @@ export const BunchOfDevicesWithMetadata: Story = { { ...nullDevice, id: "d0b9eb9d", - name: "device4", + name: "Many long", metadata: Array.from({ length: 4 }, (_, i) => ({ ...long, - key: `Short ${i}`, + key: `Long ${i}`, })), }, { ...nullDevice, id: "a6c69587", - name: "device5", + name: "Short and long", metadata: Array.from({ length: 8 }, (_, i) => i % 2 === 0 ? { ...short, key: `Short ${i}` } @@ -104,7 +111,7 @@ export const BunchOfDevicesWithMetadata: Story = { { ...nullDevice, id: "3af84e31", - name: "device6", + name: "Many really long", metadata: Array.from({ length: 8 }, (_, i) => ({ ...reallyLong, key: `Really long ${i}`, From fe794e35f7aa3dbf390b6a490e7dbcf0ddaf8193 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Fri, 22 Sep 2023 21:28:11 +0000 Subject: [PATCH 4/5] fix tests and stories --- .../TemplateSummaryPageView.stories.tsx | 10 +++--- .../TemplateVersionEditor.stories.tsx | 14 +++++--- .../pages/WorkspacePage/Workspace.stories.tsx | 7 ++-- site/src/testHelpers/entities.ts | 36 +++++++++++++++++++ site/src/testHelpers/handlers.ts | 14 ++++++-- 5 files changed, 67 insertions(+), 14 deletions(-) diff --git a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.stories.tsx b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.stories.tsx index 46d2e8adc7c19..1cca2f6a7ac9d 100644 --- a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.stories.tsx +++ b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.stories.tsx @@ -4,7 +4,7 @@ import { MockTemplateVersion, MockTemplateVersion3, MockWorkspaceResource, - MockWorkspaceResource2, + MockWorkspaceVolumeResource, } from "testHelpers/entities"; import { TemplateSummaryPageView } from "./TemplateSummaryPageView"; @@ -20,7 +20,7 @@ export const Example: Story = { args: { template: MockTemplate, activeVersion: MockTemplateVersion, - resources: [MockWorkspaceResource, MockWorkspaceResource2], + resources: [MockWorkspaceResource, MockWorkspaceVolumeResource], }, }; @@ -28,7 +28,7 @@ export const NoIcon: Story = { args: { template: { ...MockTemplate, icon: "" }, activeVersion: MockTemplateVersion, - resources: [MockWorkspaceResource, MockWorkspaceResource2], + resources: [MockWorkspaceResource, MockWorkspaceVolumeResource], }, }; @@ -49,7 +49,7 @@ export const SmallViewport: Story = { \`\`\` `, }, - resources: [MockWorkspaceResource, MockWorkspaceResource2], + resources: [MockWorkspaceResource, MockWorkspaceVolumeResource], }, }; @@ -61,6 +61,6 @@ export const WithDeprecatedParameters: Story = { args: { template: MockTemplate, activeVersion: MockTemplateVersion3, - resources: [MockWorkspaceResource, MockWorkspaceResource2], + resources: [MockWorkspaceResource, MockWorkspaceVolumeResource], }, }; diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.stories.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.stories.tsx index 0789e985faef3..bc40bf5cd38ef 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.stories.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.stories.tsx @@ -3,10 +3,13 @@ import { MockTemplateVersion, MockTemplateVersionFileTree, MockWorkspaceBuildLogs, + MockWorkspaceContainerResource, MockWorkspaceExtendedBuildLogs, + MockWorkspaceImageResource, MockWorkspaceResource, - MockWorkspaceResource2, - MockWorkspaceResource3, + MockWorkspaceResourceMultipleAgents, + MockWorkspaceResourceSensitive, + MockWorkspaceVolumeResource, } from "testHelpers/entities"; import { TemplateVersionEditor } from "./TemplateVersionEditor"; import type { Meta, StoryObj } from "@storybook/react"; @@ -40,8 +43,11 @@ export const Resources: Story = { buildLogs: MockWorkspaceBuildLogs, resources: [ MockWorkspaceResource, - MockWorkspaceResource2, - MockWorkspaceResource3, + MockWorkspaceResourceSensitive, + MockWorkspaceResourceMultipleAgents, + MockWorkspaceVolumeResource, + MockWorkspaceImageResource, + MockWorkspaceContainerResource, ], }, }; diff --git a/site/src/pages/WorkspacePage/Workspace.stories.tsx b/site/src/pages/WorkspacePage/Workspace.stories.tsx index dddbf55a2d2b5..046daf7cb788d 100644 --- a/site/src/pages/WorkspacePage/Workspace.stories.tsx +++ b/site/src/pages/WorkspacePage/Workspace.stories.tsx @@ -80,9 +80,10 @@ export const Running: Story = { handleStart: action("start"), handleStop: action("stop"), resources: [ - Mocks.MockWorkspaceResource, - Mocks.MockWorkspaceResource2, - Mocks.MockWorkspaceResource3, + Mocks.MockWorkspaceResourceMultipleAgents, + Mocks.MockWorkspaceVolumeResource, + Mocks.MockWorkspaceImageResource, + Mocks.MockWorkspaceContainerResource, ], builds: [Mocks.MockWorkspaceBuild], canUpdateWorkspace: true, diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 46f94fe84b4cc..6e048e22bd735 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -756,6 +756,42 @@ export const MockWorkspaceResourceHidden: TypesGen.WorkspaceResource = { hide: true, }; +export const MockWorkspaceVolumeResource: TypesGen.WorkspaceResource = { + id: "test-workspace-volume-resource", + created_at: "", + job_id: "", + workspace_transition: "start", + type: "docker_volume", + name: "home_volume", + hide: false, + icon: "", + daily_cost: 0, +}; + +export const MockWorkspaceImageResource: TypesGen.WorkspaceResource = { + id: "test-workspace-image-resource", + created_at: "", + job_id: "", + workspace_transition: "start", + type: "docker_image", + name: "main", + hide: false, + icon: "", + daily_cost: 0, +}; + +export const MockWorkspaceContainerResource: TypesGen.WorkspaceResource = { + id: "test-workspace-container-resource", + created_at: "", + job_id: "", + workspace_transition: "start", + type: "docker_container", + name: "workspace", + hide: false, + icon: "", + daily_cost: 0, +}; + export const MockWorkspaceAutostartDisabled: TypesGen.UpdateWorkspaceAutostartRequest = { schedule: "", diff --git a/site/src/testHelpers/handlers.ts b/site/src/testHelpers/handlers.ts index 7b3cde88d0ca4..21e580faa2458 100644 --- a/site/src/testHelpers/handlers.ts +++ b/site/src/testHelpers/handlers.ts @@ -96,7 +96,12 @@ export const handlers = [ async (req, res, ctx) => { return res( ctx.status(200), - ctx.json([M.MockWorkspaceResource, M.MockWorkspaceResource2]), + ctx.json([ + M.MockWorkspaceResource, + M.MockWorkspaceVolumeResource, + M.MockWorkspaceImageResource, + M.MockWorkspaceContainerResource, + ]), ); }, ), @@ -254,7 +259,12 @@ export const handlers = [ (req, res, ctx) => { return res( ctx.status(200), - ctx.json([M.MockWorkspaceResource, M.MockWorkspaceResource2]), + ctx.json([ + M.MockWorkspaceResource, + M.MockWorkspaceVolumeResource, + M.MockWorkspaceImageResource, + M.MockWorkspaceContainerResource, + ]), ); }, ), From b449f4e74177b978946661e5b1c2b8ade9cd5f55 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Fri, 22 Sep 2023 21:36:25 +0000 Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/Resources/ResourceCard.tsx | 2 +- .../Resources/Resources.stories.tsx | 27 ++++++++++++------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/site/src/components/Resources/ResourceCard.tsx b/site/src/components/Resources/ResourceCard.tsx index 4d58ace7e204f..44dcf881462fa 100644 --- a/site/src/components/Resources/ResourceCard.tsx +++ b/site/src/components/Resources/ResourceCard.tsx @@ -11,7 +11,7 @@ import { import IconButton from "@mui/material/IconButton"; import Tooltip from "@mui/material/Tooltip"; import { CopyableValue } from "components/CopyableValue/CopyableValue"; -import { type Theme } from "@mui/material"; +import { type Theme } from "@mui/material/styles"; export interface ResourceCardProps { resource: WorkspaceResource; diff --git a/site/src/components/Resources/Resources.stories.tsx b/site/src/components/Resources/Resources.stories.tsx index ae9b264a16015..7e107b79ad315 100644 --- a/site/src/components/Resources/Resources.stories.tsx +++ b/site/src/components/Resources/Resources.stories.tsx @@ -98,6 +98,24 @@ export const BunchOfDevicesWithMetadata: Story = { key: `Long ${i}`, })), }, + { + ...nullDevice, + id: "3af84e31", + name: "Many really long", + metadata: Array.from({ length: 8 }, (_, i) => ({ + ...reallyLong, + key: `Really long ${i}`, + })), + }, + { + ...nullDevice, + id: "d0b9eb9d", + name: "Couple long", + metadata: Array.from({ length: 2 }, (_, i) => ({ + ...long, + key: `Long ${i}`, + })), + }, { ...nullDevice, id: "a6c69587", @@ -108,15 +126,6 @@ export const BunchOfDevicesWithMetadata: Story = { : { ...long, key: `Long ${i}` }, ), }, - { - ...nullDevice, - id: "3af84e31", - name: "Many really long", - metadata: Array.from({ length: 8 }, (_, i) => ({ - ...reallyLong, - key: `Really long ${i}`, - })), - }, ], agentRow: getAgentRow, },