mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore(site): remove new filter from experimental (#8112)
This commit is contained in:
Generated
+2
-4
@@ -7355,13 +7355,11 @@ const docTemplate = `{
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
"moons",
|
"moons",
|
||||||
"workspace_actions",
|
"workspace_actions"
|
||||||
"workspace_filter"
|
|
||||||
],
|
],
|
||||||
"x-enum-varnames": [
|
"x-enum-varnames": [
|
||||||
"ExperimentMoons",
|
"ExperimentMoons",
|
||||||
"ExperimentWorkspaceActions",
|
"ExperimentWorkspaceActions"
|
||||||
"ExperimentWorkspaceFilter"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"codersdk.Feature": {
|
"codersdk.Feature": {
|
||||||
|
|||||||
Generated
+2
-6
@@ -6582,12 +6582,8 @@
|
|||||||
},
|
},
|
||||||
"codersdk.Experiment": {
|
"codersdk.Experiment": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["moons", "workspace_actions", "workspace_filter"],
|
"enum": ["moons", "workspace_actions"],
|
||||||
"x-enum-varnames": [
|
"x-enum-varnames": ["ExperimentMoons", "ExperimentWorkspaceActions"]
|
||||||
"ExperimentMoons",
|
|
||||||
"ExperimentWorkspaceActions",
|
|
||||||
"ExperimentWorkspaceFilter"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"codersdk.Feature": {
|
"codersdk.Feature": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|||||||
@@ -1704,9 +1704,6 @@ const (
|
|||||||
// https://github.com/coder/coder/milestone/19
|
// https://github.com/coder/coder/milestone/19
|
||||||
ExperimentWorkspaceActions Experiment = "workspace_actions"
|
ExperimentWorkspaceActions Experiment = "workspace_actions"
|
||||||
|
|
||||||
// New workspace filter
|
|
||||||
ExperimentWorkspaceFilter Experiment = "workspace_filter"
|
|
||||||
|
|
||||||
// Add new experiments here!
|
// Add new experiments here!
|
||||||
// ExperimentExample Experiment = "example"
|
// ExperimentExample Experiment = "example"
|
||||||
)
|
)
|
||||||
@@ -1715,9 +1712,7 @@ const (
|
|||||||
// users to opt-in to via --experimental='*'.
|
// users to opt-in to via --experimental='*'.
|
||||||
// Experiments that are not ready for consumption by all users should
|
// Experiments that are not ready for consumption by all users should
|
||||||
// not be included here and will be essentially hidden.
|
// not be included here and will be essentially hidden.
|
||||||
var ExperimentsAll = Experiments{
|
var ExperimentsAll = Experiments{}
|
||||||
ExperimentWorkspaceFilter,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Experiments is a list of experiments that are enabled for the deployment.
|
// Experiments is a list of experiments that are enabled for the deployment.
|
||||||
// Multiple experiments may be enabled at the same time.
|
// Multiple experiments may be enabled at the same time.
|
||||||
|
|||||||
@@ -2490,7 +2490,6 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
|||||||
| ------------------- |
|
| ------------------- |
|
||||||
| `moons` |
|
| `moons` |
|
||||||
| `workspace_actions` |
|
| `workspace_actions` |
|
||||||
| `workspace_filter` |
|
|
||||||
|
|
||||||
## codersdk.Feature
|
## codersdk.Feature
|
||||||
|
|
||||||
|
|||||||
@@ -1344,12 +1344,8 @@ export const Entitlements: Entitlement[] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
// From codersdk/deployment.go
|
// From codersdk/deployment.go
|
||||||
export type Experiment = "moons" | "workspace_actions" | "workspace_filter"
|
export type Experiment = "moons" | "workspace_actions"
|
||||||
export const Experiments: Experiment[] = [
|
export const Experiments: Experiment[] = ["moons", "workspace_actions"]
|
||||||
"moons",
|
|
||||||
"workspace_actions",
|
|
||||||
"workspace_filter",
|
|
||||||
]
|
|
||||||
|
|
||||||
// From codersdk/deployment.go
|
// From codersdk/deployment.go
|
||||||
export type FeatureName =
|
export type FeatureName =
|
||||||
|
|||||||
@@ -74,6 +74,8 @@ export const useFilter = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UseFilterResult = ReturnType<typeof useFilter>
|
||||||
|
|
||||||
const parseFilterQuery = (filterQuery: string): FilterValues => {
|
const parseFilterQuery = (filterQuery: string): FilterValues => {
|
||||||
if (filterQuery === "") {
|
if (filterQuery === "") {
|
||||||
return {}
|
return {}
|
||||||
|
|||||||
@@ -101,3 +101,5 @@ export const useFilterMenu = <TOption extends BaseOption = BaseOption>({
|
|||||||
isSearching: searchOptionsQuery.isFetching,
|
isSearching: searchOptionsQuery.isFetching,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UseFilterMenuResult = ReturnType<typeof useFilterMenu>
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { action } from "@storybook/addon-actions"
|
||||||
|
import { UseFilterResult } from "./filter"
|
||||||
|
import { UseFilterMenuResult } from "./menu"
|
||||||
|
|
||||||
|
export const MockMenu: UseFilterMenuResult = {
|
||||||
|
initialOption: undefined,
|
||||||
|
isInitializing: false,
|
||||||
|
isSearching: false,
|
||||||
|
query: "",
|
||||||
|
searchOptions: [],
|
||||||
|
selectedOption: undefined,
|
||||||
|
selectOption: action("selectOption"),
|
||||||
|
setQuery: action("updateQuery"),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDefaultFilterProps = <TFilterProps>({
|
||||||
|
query = "",
|
||||||
|
values,
|
||||||
|
menus,
|
||||||
|
}: {
|
||||||
|
query?: string
|
||||||
|
values: Record<string, string | undefined>
|
||||||
|
menus: Record<string, UseFilterMenuResult>
|
||||||
|
}) =>
|
||||||
|
({
|
||||||
|
filter: {
|
||||||
|
query,
|
||||||
|
update: () => action("update"),
|
||||||
|
debounceUpdate: action("debounce") as UseFilterResult["debounceUpdate"],
|
||||||
|
used: false,
|
||||||
|
values,
|
||||||
|
},
|
||||||
|
menus,
|
||||||
|
} as TFilterProps)
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import { Story } from "@storybook/react"
|
|
||||||
import { userFilterQuery, workspaceFilterQuery } from "../../utils/filters"
|
|
||||||
import {
|
|
||||||
SearchBarWithFilter,
|
|
||||||
SearchBarWithFilterProps,
|
|
||||||
} from "./SearchBarWithFilter"
|
|
||||||
import { mockApiError } from "testHelpers/entities"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: "components/SearchBarWithFilter",
|
|
||||||
component: SearchBarWithFilter,
|
|
||||||
args: {
|
|
||||||
filter: workspaceFilterQuery.me,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const Template: Story<SearchBarWithFilterProps> = (args) => (
|
|
||||||
<SearchBarWithFilter {...args} />
|
|
||||||
)
|
|
||||||
|
|
||||||
export const WithoutPresetFilters = Template.bind({})
|
|
||||||
|
|
||||||
export const WithPresetFilters = Template.bind({})
|
|
||||||
WithPresetFilters.args = {
|
|
||||||
presetFilters: [
|
|
||||||
{ query: workspaceFilterQuery.me, name: "Your workspaces" },
|
|
||||||
{ query: "random query", name: "Random query" },
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
export const WithError = Template.bind({})
|
|
||||||
WithError.args = {
|
|
||||||
filter: "status:inactive",
|
|
||||||
presetFilters: [
|
|
||||||
{ query: userFilterQuery.active, name: "Active users" },
|
|
||||||
{ query: "random query", name: "Random query" },
|
|
||||||
],
|
|
||||||
error: mockApiError({
|
|
||||||
message: "Invalid user search query.",
|
|
||||||
validations: [
|
|
||||||
{
|
|
||||||
field: "status",
|
|
||||||
detail: `Query param "status" has invalid value: "inactive" is not a valid user status`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { screen } from "@testing-library/react"
|
|
||||||
import userEvent from "@testing-library/user-event"
|
|
||||||
import { render } from "../../testHelpers/renderHelpers"
|
|
||||||
import { SearchBarWithFilter } from "./SearchBarWithFilter"
|
|
||||||
|
|
||||||
// mock the debounce utility
|
|
||||||
jest.mock("just-debounce-it", () =>
|
|
||||||
jest.fn((fn) => {
|
|
||||||
fn.cancel = jest.fn()
|
|
||||||
return fn
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
describe("SearchBarWithFilter", () => {
|
|
||||||
it("calls the onFilter handler on keystroke", async () => {
|
|
||||||
// When
|
|
||||||
const onFilter = jest.fn()
|
|
||||||
render(<SearchBarWithFilter onFilter={onFilter} />)
|
|
||||||
|
|
||||||
const searchInput = screen.getByRole("textbox")
|
|
||||||
await userEvent.type(searchInput, "workspace") // 9 characters
|
|
||||||
|
|
||||||
// Then
|
|
||||||
expect(onFilter).toBeCalledTimes(9) // 9 characters
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
import Button from "@mui/material/Button"
|
|
||||||
import Fade from "@mui/material/Fade"
|
|
||||||
import InputAdornment from "@mui/material/InputAdornment"
|
|
||||||
import Menu from "@mui/material/Menu"
|
|
||||||
import MenuItem from "@mui/material/MenuItem"
|
|
||||||
import OutlinedInput from "@mui/material/OutlinedInput"
|
|
||||||
import { makeStyles } from "@mui/styles"
|
|
||||||
import { Theme } from "@mui/material/styles"
|
|
||||||
import SearchIcon from "@mui/icons-material/Search"
|
|
||||||
import debounce from "just-debounce-it"
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react"
|
|
||||||
import { getValidationErrorMessage } from "../../api/errors"
|
|
||||||
import { CloseDropdown, OpenDropdown } from "../DropdownArrows/DropdownArrows"
|
|
||||||
import { Stack } from "../Stack/Stack"
|
|
||||||
|
|
||||||
export const Language = {
|
|
||||||
filterName: "Filters",
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SearchBarWithFilterProps {
|
|
||||||
filter?: string
|
|
||||||
onFilter: (query: string) => void
|
|
||||||
presetFilters?: PresetFilter[]
|
|
||||||
error?: unknown
|
|
||||||
docs?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PresetFilter {
|
|
||||||
name: string
|
|
||||||
query: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SearchBarWithFilter: React.FC<
|
|
||||||
React.PropsWithChildren<SearchBarWithFilterProps>
|
|
||||||
> = ({ filter, onFilter, presetFilters, error, docs }) => {
|
|
||||||
const styles = useStyles({ error: Boolean(error) })
|
|
||||||
const searchInputRef = useRef<HTMLInputElement>(null)
|
|
||||||
const [value, setValue] = useState(filter)
|
|
||||||
useEffect(() => {
|
|
||||||
setValue(filter)
|
|
||||||
}, [filter])
|
|
||||||
// debounce query string entry by user
|
|
||||||
// we want the dependency array empty here
|
|
||||||
// as we don't need to redefine the function
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- see above
|
|
||||||
const debouncedOnFilter = useCallback(
|
|
||||||
debounce((debouncedQueryString: string) => {
|
|
||||||
onFilter(debouncedQueryString)
|
|
||||||
}, 300),
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
|
|
||||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
|
|
||||||
|
|
||||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
||||||
setAnchorEl(event.currentTarget)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
setAnchorEl(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
const setPresetFilter = (query: string) => () => {
|
|
||||||
if (!searchInputRef.current) {
|
|
||||||
throw new Error("Search input not found.")
|
|
||||||
}
|
|
||||||
|
|
||||||
onFilter(query)
|
|
||||||
// Update this to the input directly instead of create a new state and
|
|
||||||
// re-render the component since the onFilter is already calling the
|
|
||||||
// filtering process
|
|
||||||
searchInputRef.current.value = query
|
|
||||||
handleClose()
|
|
||||||
}
|
|
||||||
|
|
||||||
const errorMessage = getValidationErrorMessage(error)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack spacing={1} className={styles.root}>
|
|
||||||
<Stack direction="row" spacing={0}>
|
|
||||||
{presetFilters && presetFilters.length > 0 && (
|
|
||||||
<Button
|
|
||||||
aria-controls="filter-menu"
|
|
||||||
aria-haspopup="true"
|
|
||||||
size="large"
|
|
||||||
onClick={handleClick}
|
|
||||||
className={styles.buttonRoot}
|
|
||||||
>
|
|
||||||
{Language.filterName}{" "}
|
|
||||||
{anchorEl ? <CloseDropdown /> : <OpenDropdown />}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div role="form" className={styles.filterForm}>
|
|
||||||
<OutlinedInput
|
|
||||||
id="query"
|
|
||||||
name="query"
|
|
||||||
error={Boolean(error)}
|
|
||||||
value={value}
|
|
||||||
className={styles.inputStyles}
|
|
||||||
onChange={(event) => {
|
|
||||||
setValue(event.currentTarget.value)
|
|
||||||
debouncedOnFilter(event.currentTarget.value)
|
|
||||||
}}
|
|
||||||
inputRef={searchInputRef}
|
|
||||||
inputProps={{
|
|
||||||
"aria-label": "Filter",
|
|
||||||
}}
|
|
||||||
startAdornment={
|
|
||||||
<InputAdornment position="start" className={styles.searchIcon}>
|
|
||||||
<SearchIcon fontSize="small" />
|
|
||||||
</InputAdornment>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{presetFilters && presetFilters.length > 0 ? (
|
|
||||||
<Menu
|
|
||||||
id="filter-menu"
|
|
||||||
anchorEl={anchorEl}
|
|
||||||
keepMounted
|
|
||||||
open={Boolean(anchorEl)}
|
|
||||||
onClose={handleClose}
|
|
||||||
TransitionComponent={Fade}
|
|
||||||
anchorOrigin={{
|
|
||||||
vertical: "bottom",
|
|
||||||
horizontal: "left",
|
|
||||||
}}
|
|
||||||
transformOrigin={{
|
|
||||||
vertical: "top",
|
|
||||||
horizontal: "left",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{presetFilters.map((presetFilter) => (
|
|
||||||
<MenuItem
|
|
||||||
key={presetFilter.name}
|
|
||||||
onClick={setPresetFilter(presetFilter.query)}
|
|
||||||
>
|
|
||||||
{presetFilter.name}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
{docs && (
|
|
||||||
<MenuItem component="a" href={docs} target="_blank">
|
|
||||||
View advanced filtering
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
|
||||||
</Menu>
|
|
||||||
) : null}
|
|
||||||
</Stack>
|
|
||||||
{errorMessage && (
|
|
||||||
<Stack className={styles.errorRoot}>{errorMessage}</Stack>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StyleProps {
|
|
||||||
error?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const useStyles = makeStyles<Theme, StyleProps>((theme) => ({
|
|
||||||
root: {
|
|
||||||
marginBottom: theme.spacing(2),
|
|
||||||
|
|
||||||
"&:has(button) .MuiInputBase-root": {
|
|
||||||
borderTopLeftRadius: 0,
|
|
||||||
borderBottomLeftRadius: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// necessary to expand the textField
|
|
||||||
// the length of the page (within the bordered filterContainer)
|
|
||||||
filterForm: {
|
|
||||||
width: "100%",
|
|
||||||
},
|
|
||||||
buttonRoot: {
|
|
||||||
border: `1px solid ${theme.palette.divider}`,
|
|
||||||
borderRadius: `${theme.shape.borderRadius}px 0px 0px ${theme.shape.borderRadius}px`,
|
|
||||||
flexShrink: 0,
|
|
||||||
marginRight: -1,
|
|
||||||
|
|
||||||
"&:hover": {
|
|
||||||
zIndex: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
errorRoot: {
|
|
||||||
color: theme.palette.error.main,
|
|
||||||
whiteSpace: "pre-wrap",
|
|
||||||
},
|
|
||||||
inputStyles: {
|
|
||||||
height: "100%",
|
|
||||||
width: "100%",
|
|
||||||
color: theme.palette.primary.contrastText,
|
|
||||||
backgroundColor: theme.palette.background.paper,
|
|
||||||
|
|
||||||
"& fieldset": {
|
|
||||||
borderColor: theme.palette.divider,
|
|
||||||
"&MuiOutlinedInput-root:hover, &MuiOutlinedInput-notchedOutline": {
|
|
||||||
borderColor: (props) => props.error && theme.palette.error.contrastText,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
"& .MuiInputBase-input": {
|
|
||||||
paddingTop: "inherit",
|
|
||||||
paddingBottom: "inherit",
|
|
||||||
// The same as the button
|
|
||||||
minHeight: 40,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
searchIcon: {
|
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
@@ -7,14 +7,12 @@ import { pageTitle } from "utils/page"
|
|||||||
import { AuditPageView } from "./AuditPageView"
|
import { AuditPageView } from "./AuditPageView"
|
||||||
import { useUserFilterMenu } from "components/Filter/UserFilter"
|
import { useUserFilterMenu } from "components/Filter/UserFilter"
|
||||||
import { useFilter } from "components/Filter/filter"
|
import { useFilter } from "components/Filter/filter"
|
||||||
import { useDashboard } from "components/Dashboard/DashboardProvider"
|
|
||||||
import { usePagination } from "hooks"
|
import { usePagination } from "hooks"
|
||||||
import { useQuery } from "@tanstack/react-query"
|
import { useQuery } from "@tanstack/react-query"
|
||||||
import { getAuditLogs } from "api/api"
|
import { getAuditLogs } from "api/api"
|
||||||
import { useActionFilterMenu, useResourceTypeFilterMenu } from "./AuditFilter"
|
import { useActionFilterMenu, useResourceTypeFilterMenu } from "./AuditFilter"
|
||||||
|
|
||||||
const AuditPage: FC = () => {
|
const AuditPage: FC = () => {
|
||||||
const dashboard = useDashboard()
|
|
||||||
const searchParamsResult = useSearchParams()
|
const searchParamsResult = useSearchParams()
|
||||||
const pagination = usePagination({ searchParamsResult })
|
const pagination = usePagination({ searchParamsResult })
|
||||||
const filter = useFilter({
|
const filter = useFilter({
|
||||||
@@ -73,22 +71,15 @@ const AuditPage: FC = () => {
|
|||||||
isNonInitialPage={nonInitialPage(searchParamsResult[0])}
|
isNonInitialPage={nonInitialPage(searchParamsResult[0])}
|
||||||
isAuditLogVisible={isAuditLogVisible}
|
isAuditLogVisible={isAuditLogVisible}
|
||||||
error={error}
|
error={error}
|
||||||
filterProps={
|
filterProps={{
|
||||||
dashboard.experiments.includes("workspace_filter")
|
filter,
|
||||||
? {
|
error,
|
||||||
filter,
|
menus: {
|
||||||
error,
|
user: userMenu,
|
||||||
menus: {
|
action: actionMenu,
|
||||||
user: userMenu,
|
resourceType: resourceTypeMenu,
|
||||||
action: actionMenu,
|
},
|
||||||
resourceType: resourceTypeMenu,
|
}}
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
filter: filter.query,
|
|
||||||
onFilter: filter.update,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,41 +1,25 @@
|
|||||||
/* eslint-disable eslint-comments/disable-enable-pair -- ignore */
|
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any -- We don't care about any here */
|
|
||||||
import { Meta, StoryObj } from "@storybook/react"
|
import { Meta, StoryObj } from "@storybook/react"
|
||||||
import { MockAuditLog, MockAuditLog2, MockUser } from "testHelpers/entities"
|
import { MockAuditLog, MockAuditLog2, MockUser } from "testHelpers/entities"
|
||||||
import { AuditPageView } from "./AuditPageView"
|
import { AuditPageView } from "./AuditPageView"
|
||||||
import { action } from "@storybook/addon-actions"
|
|
||||||
import { WorkspacesPageView } from "pages/WorkspacesPage/WorkspacesPageView"
|
import { WorkspacesPageView } from "pages/WorkspacesPage/WorkspacesPageView"
|
||||||
import { ComponentProps } from "react"
|
import { ComponentProps } from "react"
|
||||||
|
import { MockMenu, getDefaultFilterProps } from "components/Filter/storyHelpers"
|
||||||
|
|
||||||
const mockMenu = {
|
type FilterProps = ComponentProps<typeof AuditPageView>["filterProps"]
|
||||||
initialOption: undefined,
|
|
||||||
isInitializing: false,
|
|
||||||
isSearching: false,
|
|
||||||
query: "",
|
|
||||||
searchOptions: [],
|
|
||||||
selectedOption: undefined,
|
|
||||||
selectOption: action("selectOption"),
|
|
||||||
setQuery: action("updateQuery"),
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultFilterProps = {
|
const defaultFilterProps = getDefaultFilterProps<FilterProps>({
|
||||||
filter: {
|
query: `owner:me`,
|
||||||
query: `owner:me`,
|
values: {
|
||||||
update: () => action("update"),
|
username: MockUser.username,
|
||||||
debounceUpdate: action("debounce") as any,
|
action: undefined,
|
||||||
used: false,
|
resource_type: undefined,
|
||||||
values: {
|
|
||||||
username: MockUser.username,
|
|
||||||
action: undefined,
|
|
||||||
resource_type: undefined,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
menus: {
|
menus: {
|
||||||
user: mockMenu,
|
user: MockMenu,
|
||||||
action: mockMenu,
|
action: MockMenu,
|
||||||
resourceType: mockMenu,
|
resourceType: MockMenu,
|
||||||
},
|
},
|
||||||
} as ComponentProps<typeof AuditPageView>["filterProps"]
|
})
|
||||||
|
|
||||||
const meta: Meta<typeof AuditPageView> = {
|
const meta: Meta<typeof AuditPageView> = {
|
||||||
title: "pages/AuditPageView",
|
title: "pages/AuditPageView",
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
PageHeaderSubtitle,
|
PageHeaderSubtitle,
|
||||||
PageHeaderTitle,
|
PageHeaderTitle,
|
||||||
} from "components/PageHeader/PageHeader"
|
} from "components/PageHeader/PageHeader"
|
||||||
import { SearchBarWithFilter } from "components/SearchBarWithFilter/SearchBarWithFilter"
|
|
||||||
import { Stack } from "components/Stack/Stack"
|
import { Stack } from "components/Stack/Stack"
|
||||||
import { TableLoader } from "components/TableLoader/TableLoader"
|
import { TableLoader } from "components/TableLoader/TableLoader"
|
||||||
import { Timeline } from "components/Timeline/Timeline"
|
import { Timeline } from "components/Timeline/Timeline"
|
||||||
@@ -30,23 +29,6 @@ export const Language = {
|
|||||||
subtitle: "View events in your audit log.",
|
subtitle: "View events in your audit log.",
|
||||||
}
|
}
|
||||||
|
|
||||||
const presetFilters = [
|
|
||||||
{
|
|
||||||
query: "resource_type:workspace action:create",
|
|
||||||
name: "Created workspaces",
|
|
||||||
},
|
|
||||||
{ query: "resource_type:template action:create", name: "Added templates" },
|
|
||||||
{ query: "resource_type:user action:delete", name: "Deleted users" },
|
|
||||||
{
|
|
||||||
query: "resource_type:workspace_build action:start build_reason:initiator",
|
|
||||||
name: "Builds started by a user",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
query: "resource_type:api_key action:login",
|
|
||||||
name: "User logins",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export interface AuditPageViewProps {
|
export interface AuditPageViewProps {
|
||||||
auditLogs?: AuditLog[]
|
auditLogs?: AuditLog[]
|
||||||
count?: number
|
count?: number
|
||||||
@@ -56,9 +38,7 @@ export interface AuditPageViewProps {
|
|||||||
isNonInitialPage: boolean
|
isNonInitialPage: boolean
|
||||||
isAuditLogVisible: boolean
|
isAuditLogVisible: boolean
|
||||||
error?: Error | unknown
|
error?: Error | unknown
|
||||||
filterProps:
|
filterProps: ComponentProps<typeof AuditFilter>
|
||||||
| ComponentProps<typeof SearchBarWithFilter>
|
|
||||||
| ComponentProps<typeof AuditFilter>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AuditPageView: FC<AuditPageViewProps> = ({
|
export const AuditPageView: FC<AuditPageViewProps> = ({
|
||||||
@@ -91,16 +71,7 @@ export const AuditPageView: FC<AuditPageViewProps> = ({
|
|||||||
|
|
||||||
<ChooseOne>
|
<ChooseOne>
|
||||||
<Cond condition={isAuditLogVisible}>
|
<Cond condition={isAuditLogVisible}>
|
||||||
{"onFilter" in filterProps ? (
|
<AuditFilter {...filterProps} />
|
||||||
<SearchBarWithFilter
|
|
||||||
{...filterProps}
|
|
||||||
docs="https://coder.com/docs/coder-oss/latest/admin/audit-logs#filtering-logs"
|
|
||||||
presetFilters={presetFilters}
|
|
||||||
error={error}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<AuditFilter {...filterProps} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<PaginationStatus
|
<PaginationStatus
|
||||||
isLoading={Boolean(isLoading)}
|
isLoading={Boolean(isLoading)}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import { ResetPasswordDialog } from "../../components/Dialogs/ResetPasswordDialo
|
|||||||
import { pageTitle } from "../../utils/page"
|
import { pageTitle } from "../../utils/page"
|
||||||
import { UsersPageView } from "./UsersPageView"
|
import { UsersPageView } from "./UsersPageView"
|
||||||
import { useStatusFilterMenu } from "./UsersFilter"
|
import { useStatusFilterMenu } from "./UsersFilter"
|
||||||
import { useDashboard } from "components/Dashboard/DashboardProvider"
|
|
||||||
import { useFilter } from "components/Filter/filter"
|
import { useFilter } from "components/Filter/filter"
|
||||||
|
|
||||||
export const Language = {
|
export const Language = {
|
||||||
@@ -77,8 +76,6 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
|
|||||||
|
|
||||||
const me = useMe()
|
const me = useMe()
|
||||||
|
|
||||||
// New filter
|
|
||||||
const dashboard = useDashboard()
|
|
||||||
const useFilterResult = useFilter({
|
const useFilterResult = useFilter({
|
||||||
searchParamsResult,
|
searchParamsResult,
|
||||||
onUpdate: () => {
|
onUpdate: () => {
|
||||||
@@ -148,29 +145,19 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
|
|||||||
roles,
|
roles,
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
error={getUsersError}
|
|
||||||
isUpdatingUserRoles={usersState.matches("updatingUserRoles")}
|
isUpdatingUserRoles={usersState.matches("updatingUserRoles")}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
canEditUsers={canEditUsers}
|
canEditUsers={canEditUsers}
|
||||||
paginationRef={paginationRef}
|
paginationRef={paginationRef}
|
||||||
isNonInitialPage={nonInitialPage(searchParams)}
|
isNonInitialPage={nonInitialPage(searchParams)}
|
||||||
actorID={me.id}
|
actorID={me.id}
|
||||||
filterProps={
|
filterProps={{
|
||||||
dashboard.experiments.includes("workspace_filter")
|
filter: useFilterResult,
|
||||||
? {
|
error: getUsersError,
|
||||||
filter: useFilterResult,
|
menus: {
|
||||||
error: getUsersError,
|
status: statusMenu,
|
||||||
menus: {
|
},
|
||||||
status: statusMenu,
|
}}
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
filter: usersState.context.filter,
|
|
||||||
onFilter: (query) => {
|
|
||||||
usersSend({ type: "UPDATE_FILTER", query })
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DeleteDialog
|
<DeleteDialog
|
||||||
|
|||||||
@@ -7,7 +7,20 @@ import {
|
|||||||
mockApiError,
|
mockApiError,
|
||||||
} from "testHelpers/entities"
|
} from "testHelpers/entities"
|
||||||
import { UsersPageView } from "./UsersPageView"
|
import { UsersPageView } from "./UsersPageView"
|
||||||
import { action } from "@storybook/addon-actions"
|
import { ComponentProps } from "react"
|
||||||
|
import { MockMenu, getDefaultFilterProps } from "components/Filter/storyHelpers"
|
||||||
|
|
||||||
|
type FilterProps = ComponentProps<typeof UsersPageView>["filterProps"]
|
||||||
|
|
||||||
|
const defaultFilterProps = getDefaultFilterProps<FilterProps>({
|
||||||
|
query: "owner:me",
|
||||||
|
menus: {
|
||||||
|
status: MockMenu,
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
status: "active",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const meta: Meta<typeof UsersPageView> = {
|
const meta: Meta<typeof UsersPageView> = {
|
||||||
title: "pages/UsersPageView",
|
title: "pages/UsersPageView",
|
||||||
@@ -19,10 +32,7 @@ const meta: Meta<typeof UsersPageView> = {
|
|||||||
roles: MockAssignableSiteRoles,
|
roles: MockAssignableSiteRoles,
|
||||||
count: 2,
|
count: 2,
|
||||||
canEditUsers: true,
|
canEditUsers: true,
|
||||||
filterProps: {
|
filterProps: defaultFilterProps,
|
||||||
onFilter: action("onFilter"),
|
|
||||||
filter: "",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,14 +66,18 @@ export const EmptyPage = {
|
|||||||
export const Error = {
|
export const Error = {
|
||||||
args: {
|
args: {
|
||||||
users: undefined,
|
users: undefined,
|
||||||
error: mockApiError({
|
count: 0,
|
||||||
message: "Invalid user search query.",
|
filterProps: {
|
||||||
validations: [
|
...defaultFilterProps,
|
||||||
{
|
error: mockApiError({
|
||||||
field: "status",
|
message: "Invalid user search query.",
|
||||||
detail: `Query param "status" has invalid value: "inactive" is not a valid user status`,
|
validations: [
|
||||||
},
|
{
|
||||||
],
|
field: "status",
|
||||||
}),
|
detail: `Query param "status" has invalid value: "inactive" is not a valid user status`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ import { PaginationWidget } from "components/PaginationWidget/PaginationWidget"
|
|||||||
import { ComponentProps, FC } from "react"
|
import { ComponentProps, FC } from "react"
|
||||||
import { PaginationMachineRef } from "xServices/pagination/paginationXService"
|
import { PaginationMachineRef } from "xServices/pagination/paginationXService"
|
||||||
import * as TypesGen from "../../api/typesGenerated"
|
import * as TypesGen from "../../api/typesGenerated"
|
||||||
import { SearchBarWithFilter } from "../../components/SearchBarWithFilter/SearchBarWithFilter"
|
|
||||||
import { UsersTable } from "../../components/UsersTable/UsersTable"
|
import { UsersTable } from "../../components/UsersTable/UsersTable"
|
||||||
import { userFilterQuery } from "../../utils/filters"
|
|
||||||
import { UsersFilter } from "./UsersFilter"
|
import { UsersFilter } from "./UsersFilter"
|
||||||
import { PaginationStatus } from "components/PaginationStatus/PaginationStatus"
|
import { PaginationStatus } from "components/PaginationStatus/PaginationStatus"
|
||||||
|
|
||||||
@@ -16,7 +14,6 @@ export interface UsersPageViewProps {
|
|||||||
users?: TypesGen.User[]
|
users?: TypesGen.User[]
|
||||||
count?: number
|
count?: number
|
||||||
roles?: TypesGen.AssignableRoles[]
|
roles?: TypesGen.AssignableRoles[]
|
||||||
error?: unknown
|
|
||||||
isUpdatingUserRoles?: boolean
|
isUpdatingUserRoles?: boolean
|
||||||
canEditUsers?: boolean
|
canEditUsers?: boolean
|
||||||
isLoading?: boolean
|
isLoading?: boolean
|
||||||
@@ -30,9 +27,7 @@ export interface UsersPageViewProps {
|
|||||||
user: TypesGen.User,
|
user: TypesGen.User,
|
||||||
roles: TypesGen.Role["name"][],
|
roles: TypesGen.Role["name"][],
|
||||||
) => void
|
) => void
|
||||||
filterProps:
|
filterProps: ComponentProps<typeof UsersFilter>
|
||||||
| ComponentProps<typeof SearchBarWithFilter>
|
|
||||||
| ComponentProps<typeof UsersFilter>
|
|
||||||
paginationRef: PaginationMachineRef
|
paginationRef: PaginationMachineRef
|
||||||
isNonInitialPage: boolean
|
isNonInitialPage: boolean
|
||||||
actorID: string
|
actorID: string
|
||||||
@@ -49,7 +44,6 @@ export const UsersPageView: FC<React.PropsWithChildren<UsersPageViewProps>> = ({
|
|||||||
onActivateUser,
|
onActivateUser,
|
||||||
onResetUserPassword,
|
onResetUserPassword,
|
||||||
onUpdateUserRoles,
|
onUpdateUserRoles,
|
||||||
error,
|
|
||||||
isUpdatingUserRoles,
|
isUpdatingUserRoles,
|
||||||
canEditUsers,
|
canEditUsers,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -58,22 +52,9 @@ export const UsersPageView: FC<React.PropsWithChildren<UsersPageViewProps>> = ({
|
|||||||
isNonInitialPage,
|
isNonInitialPage,
|
||||||
actorID,
|
actorID,
|
||||||
}) => {
|
}) => {
|
||||||
const presetFilters = [
|
|
||||||
{ query: userFilterQuery.active, name: Language.activeUsersFilterName },
|
|
||||||
{ query: userFilterQuery.all, name: Language.allUsersFilterName },
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{"onFilter" in filterProps ? (
|
<UsersFilter {...filterProps} />
|
||||||
<SearchBarWithFilter
|
|
||||||
{...filterProps}
|
|
||||||
presetFilters={presetFilters}
|
|
||||||
error={error}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<UsersFilter {...filterProps} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<PaginationStatus
|
<PaginationStatus
|
||||||
isLoading={Boolean(isLoading)}
|
isLoading={Boolean(isLoading)}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { WorkspacesPageView } from "./WorkspacesPageView"
|
|||||||
import { useOrganizationId, usePermissions } from "hooks"
|
import { useOrganizationId, usePermissions } from "hooks"
|
||||||
import { useTemplateFilterMenu, useStatusFilterMenu } from "./filter/menus"
|
import { useTemplateFilterMenu, useStatusFilterMenu } from "./filter/menus"
|
||||||
import { useSearchParams } from "react-router-dom"
|
import { useSearchParams } from "react-router-dom"
|
||||||
import { useDashboard } from "components/Dashboard/DashboardProvider"
|
|
||||||
import { useFilter } from "components/Filter/filter"
|
import { useFilter } from "components/Filter/filter"
|
||||||
import { useUserFilterMenu } from "components/Filter/UserFilter"
|
import { useUserFilterMenu } from "components/Filter/UserFilter"
|
||||||
|
|
||||||
@@ -49,7 +48,6 @@ const WorkspacesPage: FC = () => {
|
|||||||
onChange: (option) =>
|
onChange: (option) =>
|
||||||
filter.update({ ...filter.values, status: option?.value }),
|
filter.update({ ...filter.values, status: option?.value }),
|
||||||
})
|
})
|
||||||
const dashboard = useDashboard()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -58,7 +56,6 @@ const WorkspacesPage: FC = () => {
|
|||||||
</Helmet>
|
</Helmet>
|
||||||
|
|
||||||
<WorkspacesPageView
|
<WorkspacesPageView
|
||||||
useNewFilter={dashboard.experiments.includes("workspace_filter")}
|
|
||||||
workspaces={data?.workspaces}
|
workspaces={data?.workspaces}
|
||||||
error={error}
|
error={error}
|
||||||
count={data?.count}
|
count={data?.count}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
/* eslint-disable eslint-comments/disable-enable-pair -- ignore */
|
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any -- We don't care about any here */
|
|
||||||
import { Meta, StoryObj } from "@storybook/react"
|
import { Meta, StoryObj } from "@storybook/react"
|
||||||
import { DEFAULT_RECORDS_PER_PAGE } from "components/PaginationWidget/utils"
|
import { DEFAULT_RECORDS_PER_PAGE } from "components/PaginationWidget/utils"
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
@@ -15,13 +13,13 @@ import {
|
|||||||
MockBuildInfo,
|
MockBuildInfo,
|
||||||
MockEntitlementsWithScheduling,
|
MockEntitlementsWithScheduling,
|
||||||
MockExperiments,
|
MockExperiments,
|
||||||
MockUser,
|
|
||||||
mockApiError,
|
mockApiError,
|
||||||
|
MockUser,
|
||||||
} from "testHelpers/entities"
|
} from "testHelpers/entities"
|
||||||
import { WorkspacesPageView } from "./WorkspacesPageView"
|
import { WorkspacesPageView } from "./WorkspacesPageView"
|
||||||
import { DashboardProviderContext } from "components/Dashboard/DashboardProvider"
|
import { DashboardProviderContext } from "components/Dashboard/DashboardProvider"
|
||||||
import { action } from "@storybook/addon-actions"
|
|
||||||
import { ComponentProps } from "react"
|
import { ComponentProps } from "react"
|
||||||
|
import { MockMenu, getDefaultFilterProps } from "components/Filter/storyHelpers"
|
||||||
|
|
||||||
const createWorkspace = (
|
const createWorkspace = (
|
||||||
status: WorkspaceStatus,
|
status: WorkspaceStatus,
|
||||||
@@ -71,35 +69,21 @@ const MockedAppearance = {
|
|||||||
save: () => null,
|
save: () => null,
|
||||||
}
|
}
|
||||||
|
|
||||||
const mockMenu = {
|
type FilterProps = ComponentProps<typeof WorkspacesPageView>["filterProps"]
|
||||||
initialOption: undefined,
|
|
||||||
isInitializing: false,
|
|
||||||
isSearching: false,
|
|
||||||
query: "",
|
|
||||||
searchOptions: [],
|
|
||||||
selectedOption: undefined,
|
|
||||||
selectOption: action("selectOption"),
|
|
||||||
setQuery: action("updateQuery"),
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultFilterProps = {
|
const defaultFilterProps = getDefaultFilterProps<FilterProps>({
|
||||||
filter: {
|
query: "owner:me",
|
||||||
query: `owner:me`,
|
|
||||||
update: () => action("update"),
|
|
||||||
debounceUpdate: action("debounce") as any,
|
|
||||||
used: false,
|
|
||||||
values: {
|
|
||||||
owner: MockUser.username,
|
|
||||||
template: undefined,
|
|
||||||
status: undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
menus: {
|
menus: {
|
||||||
user: mockMenu,
|
user: MockMenu,
|
||||||
template: mockMenu,
|
template: MockMenu,
|
||||||
status: mockMenu,
|
status: MockMenu,
|
||||||
},
|
},
|
||||||
} as ComponentProps<typeof WorkspacesPageView>["filterProps"]
|
values: {
|
||||||
|
owner: MockUser.username,
|
||||||
|
template: undefined,
|
||||||
|
status: undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const meta: Meta<typeof WorkspacesPageView> = {
|
const meta: Meta<typeof WorkspacesPageView> = {
|
||||||
title: "pages/WorkspacesPageView",
|
title: "pages/WorkspacesPageView",
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ import { ImpendingDeletionBanner, Count } from "components/WorkspaceDeletion"
|
|||||||
import { ErrorAlert } from "components/Alert/ErrorAlert"
|
import { ErrorAlert } from "components/Alert/ErrorAlert"
|
||||||
import { WorkspacesFilter } from "./filter/filter"
|
import { WorkspacesFilter } from "./filter/filter"
|
||||||
import { hasError, isApiValidationError } from "api/errors"
|
import { hasError, isApiValidationError } from "api/errors"
|
||||||
import { workspaceFilterQuery } from "utils/filters"
|
|
||||||
import { SearchBarWithFilter } from "components/SearchBarWithFilter/SearchBarWithFilter"
|
|
||||||
import { PaginationStatus } from "components/PaginationStatus/PaginationStatus"
|
import { PaginationStatus } from "components/PaginationStatus/PaginationStatus"
|
||||||
|
|
||||||
export const Language = {
|
export const Language = {
|
||||||
@@ -32,24 +30,10 @@ export const Language = {
|
|||||||
template: "Template",
|
template: "Template",
|
||||||
}
|
}
|
||||||
|
|
||||||
const presetFilters = [
|
|
||||||
{ query: workspaceFilterQuery.me, name: Language.yourWorkspacesButton },
|
|
||||||
{ query: workspaceFilterQuery.all, name: Language.allWorkspacesButton },
|
|
||||||
{
|
|
||||||
query: workspaceFilterQuery.running,
|
|
||||||
name: Language.runningWorkspacesButton,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
query: workspaceFilterQuery.failed,
|
|
||||||
name: "Failed workspaces",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export interface WorkspacesPageViewProps {
|
export interface WorkspacesPageViewProps {
|
||||||
error: unknown
|
error: unknown
|
||||||
workspaces?: Workspace[]
|
workspaces?: Workspace[]
|
||||||
count?: number
|
count?: number
|
||||||
useNewFilter?: boolean
|
|
||||||
filterProps: ComponentProps<typeof WorkspacesFilter>
|
filterProps: ComponentProps<typeof WorkspacesFilter>
|
||||||
page: number
|
page: number
|
||||||
limit: number
|
limit: number
|
||||||
@@ -67,7 +51,6 @@ export const WorkspacesPageView: FC<
|
|||||||
filterProps,
|
filterProps,
|
||||||
onPageChange,
|
onPageChange,
|
||||||
onUpdateWorkspace,
|
onUpdateWorkspace,
|
||||||
useNewFilter,
|
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const { saveLocal, getLocal } = useLocalStorage()
|
const { saveLocal, getLocal } = useLocalStorage()
|
||||||
@@ -133,16 +116,7 @@ export const WorkspacesPageView: FC<
|
|||||||
count={Count.Multiple}
|
count={Count.Multiple}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{useNewFilter ? (
|
<WorkspacesFilter error={error} {...filterProps} />
|
||||||
<WorkspacesFilter error={error} {...filterProps} />
|
|
||||||
) : (
|
|
||||||
<SearchBarWithFilter
|
|
||||||
filter={filterProps.filter.query}
|
|
||||||
onFilter={filterProps.filter.debounceUpdate}
|
|
||||||
presetFilters={presetFilters}
|
|
||||||
error={error}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<PaginationStatus
|
<PaginationStatus
|
||||||
|
|||||||
@@ -1405,7 +1405,6 @@ export const MockEntitlementsWithScheduling: TypesGen.Entitlements = {
|
|||||||
export const MockExperiments: TypesGen.Experiment[] = [
|
export const MockExperiments: TypesGen.Experiment[] = [
|
||||||
"workspace_actions",
|
"workspace_actions",
|
||||||
"moons",
|
"moons",
|
||||||
"workspace_filter",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
export const MockAuditLog: TypesGen.AuditLog = {
|
export const MockAuditLog: TypesGen.AuditLog = {
|
||||||
|
|||||||
Reference in New Issue
Block a user