Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions app/api/roles.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { describe, expect, it, test } from 'vitest'

import {
allRoles,
byGroupThenName,
deleteRole,
getEffectiveRole,
Expand Down Expand Up @@ -154,3 +155,7 @@ test('byGroupThenName sorts as expected', () => {

expect([c, e, b, d, a].sort(byGroupThenName)).toEqual([a, b, c, d, e])
})

test('allRoles', () => {
expect(allRoles).toEqual(['admin', 'collaborator', 'viewer'])
})
12 changes: 6 additions & 6 deletions app/api/roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@
* it belongs in the API proper.
*/
import { useMemo } from 'react'

import { lowestBy, sortBy } from '~/util/array'
import * as R from 'remeda'
Copy link
Collaborator Author

@david-crespo david-crespo Jun 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It turns out this tree-shakes perfectly well, so we don't need to bother with manual

import { sortBy, firstBy } from 'remeda'


import type { FleetRole, IdentityType, ProjectRole, SiloRole } from './__generated__/Api'
import { usePrefetchedApiQuery } from './client'
Expand All @@ -27,12 +26,13 @@ export type RoleKey = FleetRole | SiloRole | ProjectRole
/** Turn a role order record into a sorted array of strings. */
// used for displaying lists of roles, like in a <select>
const flatRoles = (roleOrder: Record<RoleKey, number>): RoleKey[] =>
sortBy(Object.keys(roleOrder) as RoleKey[], (role) => roleOrder[role])
R.sortBy(Object.keys(roleOrder) as RoleKey[], (role) => roleOrder[role])

// This is a record only to ensure that all RoleKey are covered
// This is a record only to ensure that all RoleKey are covered. weird order
// on purpose so allRoles test can confirm sorting works
export const roleOrder: Record<RoleKey, number> = {
admin: 0,
collaborator: 1,
admin: 0,
viewer: 2,
}

Expand All @@ -41,7 +41,7 @@ export const allRoles = flatRoles(roleOrder)

/** Given a list of roles, get the most permissive one */
export const getEffectiveRole = (roles: RoleKey[]): RoleKey | undefined =>
lowestBy(roles, (role) => roleOrder[role])
R.firstBy(roles, (role) => roleOrder[role])

////////////////////////////
// Policy helpers
Expand Down
94 changes: 46 additions & 48 deletions app/api/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
*
* Copyright Oxide Computer Company
*/
/// Helpers for working with API objects
import { sumBy } from '~/util/array'
import { mapValues, pick } from '~/util/object'

import * as R from 'remeda'

import { bytesToGiB } from '~/util/units'

import type {
Expand Down Expand Up @@ -60,17 +60,16 @@ export function parsePortRange(portRange: string): PortRange | null {
export const firewallRuleGetToPut = (
rule: VpcFirewallRule
): NoExtraKeys<VpcFirewallRuleUpdate, VpcFirewallRule> =>
pick(
rule,
R.pick(rule, [
'name',
'action',
'description',
'direction',
'filters',
'priority',
'status',
'targets'
)
'targets',
])

/**
* Generates a valid name given a list of strings. Must be given at least
Expand All @@ -90,45 +89,44 @@ export const genName = (...parts: [string, ...string[]]) => {
)
}

export const instanceCan = mapValues(
{
start: ['stopped'],
reboot: ['running'],
stop: ['running', 'starting'],
delete: ['stopped', 'failed'],
// https://github.com/oxidecomputer/omicron/blob/9eff6a4/nexus/db-queries/src/db/datastore/disk.rs#L310-L314
detachDisk: ['creating', 'stopped', 'failed'],
// https://github.com/oxidecomputer/omicron/blob/a7c7a67/nexus/db-queries/src/db/datastore/disk.rs#L183-L184
attachDisk: ['creating', 'stopped'],
// https://github.com/oxidecomputer/omicron/blob/8f0cbf0/nexus/db-queries/src/db/datastore/network_interface.rs#L482
updateNic: ['stopped'],
},
// cute way to make it ergonomic to call the test while also making the states
// available directly
(states: InstanceState[]) => {
const test = (i: Instance) => states.includes(i.runState)
test.states = states
return test
}
)

export const diskCan = mapValues(
{
// https://github.com/oxidecomputer/omicron/blob/4970c71e/nexus/db-queries/src/db/datastore/disk.rs#L578-L582.
delete: ['detached', 'creating', 'faulted'],
// TODO: link to API source
snapshot: ['attached', 'detached'],
// https://github.com/oxidecomputer/omicron/blob/4970c71e/nexus/db-queries/src/db/datastore/disk.rs#L169-L172
attach: ['creating', 'detached'],
},
(states: DiskState['state'][]) => {
// only have to Pick because we want this to work for both Disk and
// Json<Disk>, which we pass to it in the MSW handlers
const test = (d: Pick<Disk, 'state'>) => states.includes(d.state.state)
test.states = states
return test
}
)
const instanceActions: Record<string, InstanceState[]> = {
start: ['stopped'],
reboot: ['running'],
stop: ['running', 'starting'],
delete: ['stopped', 'failed'],
// https://github.com/oxidecomputer/omicron/blob/9eff6a4/nexus/db-queries/src/db/datastore/disk.rs#L310-L314
detachDisk: ['creating', 'stopped', 'failed'],
// https://github.com/oxidecomputer/omicron/blob/a7c7a67/nexus/db-queries/src/db/datastore/disk.rs#L183-L184
attachDisk: ['creating', 'stopped'],
// https://github.com/oxidecomputer/omicron/blob/8f0cbf0/nexus/db-queries/src/db/datastore/network_interface.rs#L482
updateNic: ['stopped'],
}

// setting .states is a cute way to make it ergonomic to call the test function
// while also making the states available directly

export const instanceCan = R.mapValues(instanceActions, (states) => {
const test = (i: Instance) => states.includes(i.runState)
test.states = states
return test
})

const diskActions: Record<string, DiskState['state'][]> = {
// https://github.com/oxidecomputer/omicron/blob/4970c71e/nexus/db-queries/src/db/datastore/disk.rs#L578-L582.
delete: ['detached', 'creating', 'faulted'],
// TODO: link to API source
snapshot: ['attached', 'detached'],
// https://github.com/oxidecomputer/omicron/blob/4970c71e/nexus/db-queries/src/db/datastore/disk.rs#L169-L172
attach: ['creating', 'detached'],
}

export const diskCan = R.mapValues(diskActions, (states) => {
// only have to Pick because we want this to work for both Disk and
// Json<Disk>, which we pass to it in the MSW handlers
const test = (d: Pick<Disk, 'state'>) => states.includes(d.state.state)
test.states = states
return test
})

/** Hard coded in the API, so we can hard code it here. */
export const FLEET_ID = '001de000-1334-4000-8000-000000000000'
Expand All @@ -141,8 +139,8 @@ export function totalCapacity(
) {
return {
disk_tib: Math.ceil(FUDGE * sleds.length * 32 * TBtoTiB), // TODO: make more real
ram_gib: Math.ceil(bytesToGiB(FUDGE * sumBy(sleds, (s) => s.usablePhysicalRam))),
cpu: Math.ceil(FUDGE * sumBy(sleds, (s) => s.usableHardwareThreads)),
ram_gib: Math.ceil(bytesToGiB(FUDGE * R.sumBy(sleds, (s) => s.usablePhysicalRam))),
cpu: Math.ceil(FUDGE * R.sumBy(sleds, (s) => s.usableHardwareThreads)),
}
}

Expand Down
4 changes: 2 additions & 2 deletions app/forms/instance-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import * as Accordion from '@radix-ui/react-accordion'
import { useEffect, useMemo, useState } from 'react'
import { useController, useWatch, type Control } from 'react-hook-form'
import { useNavigate, type LoaderFunctionArgs } from 'react-router-dom'
import * as R from 'remeda'
import type { SetRequired } from 'type-fest'

import {
Expand Down Expand Up @@ -72,7 +73,6 @@ import { Slash } from '~/ui/lib/Slash'
import { Tabs } from '~/ui/lib/Tabs'
import { TextInputHint } from '~/ui/lib/TextInput'
import { TipIcon } from '~/ui/lib/TipIcon'
import { isTruthy } from '~/util/array'
import { readBlobAsBase64 } from '~/util/file'
import { docLinks, links } from '~/util/links'
import { nearest10 } from '~/util/math'
Expand Down Expand Up @@ -645,7 +645,7 @@ const AdvancedAccordion = ({
)
const attachedFloatingIpsData = attachedFloatingIps
.map((ip) => attachableFloatingIps.find((fip) => fip.name === ip.floatingIp))
.filter(isTruthy)
.filter(R.isTruthy)

const closeFloatingIpModal = () => {
setFloatingIpModalOpen(false)
Expand Down
9 changes: 7 additions & 2 deletions app/forms/network-interface-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,19 @@
*
* Copyright Oxide Computer Company
*/
import * as R from 'remeda'

import {
useApiMutation,
useApiQueryClient,
type InstanceNetworkInterface,
type InstanceNetworkInterfaceUpdate,
} from '@oxide/api'

import { DescriptionField } from '~/components/form/fields/DescriptionField'
import { NameField } from '~/components/form/fields/NameField'
import { SideModalForm } from '~/components/form/SideModalForm'
import { useForm, useInstanceSelector } from '~/hooks'
import { pick } from '~/util/object'

type EditNetworkInterfaceFormProps = {
editing: InstanceNetworkInterface
Expand All @@ -36,7 +38,10 @@ export function EditNetworkInterfaceForm({
},
})

const defaultValues = pick(editing, 'name', 'description') // satisfies NetworkInterfaceUpdate
const defaultValues = R.pick(editing, [
'name',
'description',
]) satisfies InstanceNetworkInterfaceUpdate

const form = useForm({ defaultValues })

Expand Down
12 changes: 9 additions & 3 deletions app/forms/subnet-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@
*
* Copyright Oxide Computer Company
*/
import { useApiMutation, useApiQueryClient, type VpcSubnet } from '@oxide/api'
import * as R from 'remeda'

import {
useApiMutation,
useApiQueryClient,
type VpcSubnet,
type VpcSubnetUpdate,
} from '@oxide/api'

import { DescriptionField } from '~/components/form/fields/DescriptionField'
import { NameField } from '~/components/form/fields/NameField'
import { SideModalForm } from '~/components/form/SideModalForm'
import { useForm, useVpcSelector } from '~/hooks'
import { pick } from '~/util/object'

type EditSubnetFormProps = {
onDismiss: () => void
Expand All @@ -29,7 +35,7 @@ export function EditSubnetForm({ onDismiss, editing }: EditSubnetFormProps) {
},
})

const defaultValues = pick(editing, 'name', 'description') /* satisfies VpcSubnetUpdate */
const defaultValues = R.pick(editing, ['name', 'description']) satisfies VpcSubnetUpdate

const form = useForm({ defaultValues })

Expand Down
4 changes: 2 additions & 2 deletions app/pages/SiloAccessPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import { EmptyMessage } from '~/ui/lib/EmptyMessage'
import { PageHeader, PageTitle } from '~/ui/lib/PageHeader'
import { TableActions, TableEmptyBox } from '~/ui/lib/Table'
import { identityTypeLabel, roleColor } from '~/util/access'
import { groupBy, isTruthy } from '~/util/array'
import { groupBy } from '~/util/array'
import { docLinks } from '~/util/links'

const EmptyState = ({ onClick }: { onClick: () => void }) => (
Expand Down Expand Up @@ -84,7 +84,7 @@ export function SiloAccessPage() {
.map(([userId, userAssignments]) => {
const siloRole = userAssignments.find((a) => a.roleSource === 'silo')?.roleName

const roles = [siloRole].filter(isTruthy)
const roles = siloRole ? [siloRole] : []

const { name, identityType } = userAssignments[0]

Expand Down
7 changes: 4 additions & 3 deletions app/pages/project/access/ProjectAccessPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import { createColumnHelper, getCoreRowModel, useReactTable } from '@tanstack/react-table'
import { useMemo, useState } from 'react'
import type { LoaderFunctionArgs } from 'react-router-dom'
import * as R from 'remeda'

import {
apiQueryClient,
Expand Down Expand Up @@ -42,7 +43,7 @@ import { PageHeader, PageTitle } from '~/ui/lib/PageHeader'
import { TableActions, TableEmptyBox } from '~/ui/lib/Table'
import { TipIcon } from '~/ui/lib/TipIcon'
import { identityTypeLabel, roleColor } from '~/util/access'
import { groupBy, isTruthy, sortBy } from '~/util/array'
import { groupBy } from '~/util/array'
import { docLinks } from '~/util/links'

const EmptyState = ({ onClick }: { onClick: () => void }) => (
Expand Down Expand Up @@ -100,8 +101,8 @@ export function ProjectAccessPage() {
const siloAccessRow = userAssignments.find((a) => a.roleSource === 'silo')
const projectAccessRow = userAssignments.find((a) => a.roleSource === 'project')

const roleBadges = sortBy(
[siloAccessRow, projectAccessRow].filter(isTruthy),
const roleBadges = R.sortBy(
[siloAccessRow, projectAccessRow].filter(R.isTruthy),
(r) => roleOrder[r.roleName] // sorts strongest role first
)

Expand Down
4 changes: 2 additions & 2 deletions app/pages/project/vpcs/VpcPage/tabs/VpcFirewallRulesTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/
import { createColumnHelper, getCoreRowModel, useReactTable } from '@tanstack/react-table'
import { useMemo, useState } from 'react'
import * as R from 'remeda'

import {
useApiMutation,
Expand All @@ -30,7 +31,6 @@ import { Badge } from '~/ui/lib/Badge'
import { CreateButton } from '~/ui/lib/CreateButton'
import { EmptyMessage } from '~/ui/lib/EmptyMessage'
import { TableEmptyBox } from '~/ui/lib/Table'
import { sortBy } from '~/util/array'
import { titleCase } from '~/util/str'

const colHelper = createColumnHelper<VpcFirewallRule>()
Expand Down Expand Up @@ -103,7 +103,7 @@ export const VpcFirewallRulesTab = () => {
const { data } = usePrefetchedApiQuery('vpcFirewallRulesView', {
query: vpcSelector,
})
const rules = useMemo(() => sortBy(data.rules, (r) => r.priority), [data])
const rules = useMemo(() => R.sortBy(data.rules, (r) => r.priority), [data])

const [createModalOpen, setCreateModalOpen] = useState(false)
const [editing, setEditing] = useState<VpcFirewallRule | null>(null)
Expand Down
6 changes: 3 additions & 3 deletions app/pages/system/inventory/sled/SledInstancesTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/
import { createColumnHelper } from '@tanstack/react-table'
import type { LoaderFunctionArgs } from 'react-router-dom'
import * as R from 'remeda'

import { apiQueryClient, type SledInstance } from '@oxide/api'
import { Instances24Icon } from '@oxide/design-system/icons/react'
Expand All @@ -18,7 +19,6 @@ import { useColsWithActions, type MenuAction } from '~/table/columns/action-col'
import { Columns } from '~/table/columns/common'
import { PAGE_SIZE, useQueryTable } from '~/table/QueryTable'
import { EmptyMessage } from '~/ui/lib/EmptyMessage'
import { pick } from '~/util/object'

const EmptyState = () => {
return (
Expand All @@ -44,7 +44,7 @@ const makeActions = (): MenuAction[] => []

const colHelper = createColumnHelper<SledInstance>()
const staticCols = [
colHelper.accessor((i) => pick(i, 'name', 'siloName', 'projectName'), {
colHelper.accessor((i) => R.pick(i, ['name', 'siloName', 'projectName']), {
header: 'name',
cell: (info) => {
const value = info.getValue()
Expand All @@ -60,7 +60,7 @@ const staticCols = [
header: 'status',
cell: (info) => <InstanceStatusBadge key="run-state" status={info.getValue()} />,
}),
colHelper.accessor((i) => pick(i, 'memory', 'ncpus'), {
colHelper.accessor((i) => R.pick(i, ['memory', 'ncpus']), {
header: 'specs',
cell: (info) => <InstanceResourceCell value={info.getValue()} />,
}),
Expand Down
Loading