Skip to content

Commit

Permalink
feat(console): filter logs by a specific resource andor resource type (
Browse files Browse the repository at this point in the history
…#6685)

Resolves #1413

This PR adds:
* Filter logs by Resource Type
* Filter logs by resource Id
* Button to clear filters
* Banner showing the number of filtered logs
  • Loading branch information
polamoros authored Jun 14, 2024
1 parent d5fadbc commit 02b0fd0
Show file tree
Hide file tree
Showing 6 changed files with 525 additions and 120 deletions.
196 changes: 139 additions & 57 deletions apps/wing-console/console/design-system/src/listbox.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { Listbox as HeadlessListbox, Transition } from "@headlessui/react";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/react/20/solid";
import {
CheckIcon,
ChevronUpDownIcon,
MagnifyingGlassIcon,
} from "@heroicons/react/20/solid";
import classNames from "classnames";
import { Fragment, useEffect, useState } from "react";
import type { ForwardRefExoticComponent, SVGProps } from "react";
import { Fragment, useEffect, useMemo, useState } from "react";
import { createPortal } from "react-dom";
import { usePopper } from "react-popper";

Expand All @@ -11,22 +16,28 @@ import { useTheme } from "./theme-provider.js";
export interface ListboxItem {
value: string;
label: string;
icon?: ForwardRefExoticComponent<SVGProps<SVGSVGElement>>;
}

export interface ListboxProps {
label?: string;
icon?: React.ForwardRefExoticComponent<React.SVGProps<SVGSVGElement>>;
renderLabel?: (selected?: string[]) => JSX.Element;
icon?: ForwardRefExoticComponent<SVGProps<SVGSVGElement>>;
className?: string;
items: ListboxItem[];
defaultSelection?: string[];
transparent?: boolean;
selected?: string[];
onChange?: (selected: string[]) => void;
disabled?: boolean;
defaultLabel?: string;
showSearch?: boolean;
notFoundLabel?: string;
}

export const Listbox = ({
label,
renderLabel,
icon,
className,
items,
Expand All @@ -35,6 +46,9 @@ export const Listbox = ({
selected,
onChange,
disabled = false,
defaultLabel = "Default",
showSearch = false,
notFoundLabel = "No results found",
}: ListboxProps) => {
const { theme } = useTheme();

Expand All @@ -57,6 +71,17 @@ export const Listbox = ({
return () => root.remove();
}, [root]);

const [search, setSearch] = useState("");

const filteredItems = useMemo(() => {
if (search === "") {
return items;
}
return items.filter((item) =>
item.label.toLocaleLowerCase().includes(search.toLocaleLowerCase()),
);
}, [search, items]);

return (
<HeadlessListbox
value={selected}
Expand All @@ -77,7 +102,12 @@ export const Listbox = ({
icon={icon}
transparent={transparent}
>
{label && <span className="block truncate">{label}</span>}
{renderLabel && (
<span className="block truncate">{renderLabel(selected)}</span>
)}
{!renderLabel && label && (
<span className="block truncate">{label}</span>
)}
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-1.5">
<ChevronUpDownIcon
className={classNames(theme.textInput, "h-4 w-4")}
Expand All @@ -103,69 +133,121 @@ export const Listbox = ({
<HeadlessListbox.Options
className={classNames(
theme.bgInput,
theme.textInput,
"z-10 m-1 max-h-60 w-full overflow-auto rounded-md",
"z-10 m-1 w-full rounded-md",
"py-1 text-xs shadow-lg ring-1 ring-black ring-opacity-5 outline-none",
)}
>
{defaultSelection && (
<>
{/* TODO: Fix a11y */}
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions */}
<li
className={classNames(
"relative cursor-default select-none py-2 pl-10 pr-4",
)}
onClick={() => onChange?.(defaultSelection)}
>
<span className={`block truncate font-normal`}>
Default
</span>
</li>
{showSearch && (
<div className="pb-1">
<div className="px-1 relative">
<div className="pointer-events-none absolute inset-y-0 left-1 flex items-center pl-2">
<MagnifyingGlassIcon
className={classNames("size-4", theme.text2)}
aria-hidden="true"
/>
</div>
<input
type="text"
className={classNames(
theme.borderInput,
"pl-8",
"inline-flex gap-2 items-center px-2.5 py-1.5 border text-xs rounded",
"outline-none w-full shadow-inner",
theme.bg3,
theme.textInput,
theme.focusInput,
)}
placeholder="Search..."
value={search}
onChange={(event) => setSearch(event.target.value)}
/>
</div>
</div>
)}

<div className="relative">
<div
className="absolute inset-0 flex items-center"
aria-hidden="true"
<div className="overflow-auto max-h-80">
{defaultSelection && (
<>
{/* TODO: Fix a11y */}
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions */}
<li
className={classNames(
"relative cursor-default select-none py-2 pl-10 pr-4",
theme.bgInputHover,
theme.text1,
)}
onClick={() => onChange?.(defaultSelection)}
>
<div
className={classNames(
theme.borderInput,
"w-full border-t",
<span
className={classNames("block truncate font-normal")}
>
{defaultLabel}
</span>
{defaultSelection?.length === 0 &&
selected?.length === 0 && (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-sky-600">
<CheckIcon
className="h-4 w-4"
aria-hidden="true"
/>
</span>
)}
></div>
</div>
</li>
<div
className={classNames(
"border-slate-100 dark:border-slate-700",
"w-full border-t",
)}
/>
</>
)}

{filteredItems.length === 0 && search !== "" && (
<div
className={classNames(
theme.text2,
"py-2 px-4",
"w-full text-center",
)}
>
{notFoundLabel}
</div>
</>
)}
)}

{items.map((item, index) => (
<HeadlessListbox.Option
key={index}
className={({ active }) =>
classNames(
"relative cursor-default select-none py-2 pl-10 pr-4",
active && theme.bgInputHover,
)
}
value={item.value}
>
<span
className={`block truncate ${
selected?.includes(item.value)
? "font-medium"
: "font-normal"
}`}
{filteredItems.map((item, index) => (
<HeadlessListbox.Option
key={index}
className={({ active }) =>
classNames(
"relative cursor-default select-none py-2 pl-10 pr-4",
active && theme.bgInputHover,
)
}
value={item.value}
>
{item.label}
</span>
{selected?.includes(item.value) ? (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-sky-600">
<CheckIcon className="h-4 w-4" aria-hidden="true" />
<span
className={classNames(
"truncate flex items-center gap-2",
selected?.includes(item.value)
? theme.text1
: "text-slate-850 dark:text-slate-300",
)}
>
{item.icon && (
<item.icon
className={classNames("size-4", theme.text2)}
/>
)}
{item.label}
</span>
) : undefined}
</HeadlessListbox.Option>
))}
{selected?.includes(item.value) && (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-sky-600">
<CheckIcon className="size-4" aria-hidden="true" />
</span>
)}
</HeadlessListbox.Option>
))}
</div>
</HeadlessListbox.Options>
</div>
</Transition>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ export const getResourceIconComponent = (
case "@winglang/sdk.cloud.Secret": {
return iconSet.KeyIcon;
}
case "@winglang/sdk.cloud.Endpoint": {
return iconSet.LinkIcon;
}
default: {
return iconSet.CubeIcon;
}
Expand Down
78 changes: 67 additions & 11 deletions apps/wing-console/console/server/src/router/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type {
import { buildConstructTreeNodeMap } from "../utils/constructTreeNodeMap.js";
import type { FileLink } from "../utils/createRouter.js";
import { createProcedure, createRouter } from "../utils/createRouter.js";
import type { IFunctionClient, Simulator } from "../wingsdk.js";
import type { Simulator } from "../wingsdk.js";

const isTest = /(\/test$|\/test:([^/\\])+$)/;
const isTestHandler = /(\/test$|\/test:.*\/Handler$)/;
Expand Down Expand Up @@ -53,6 +53,21 @@ export const createAppRouter = () => {
config: ctx.layoutConfig,
};
}),
"app.logsFilters": createProcedure.query(async ({ ctx }) => {
const simulator = await ctx.simulator();

const resources = simulator.listResources().map((resourceId) => {
const config = simulator.tryGetResourceConfig(resourceId);
return {
id: resourceId,
type: config?.type,
};
});

return {
resources,
};
}),
"app.logs": createProcedure
.input(
z.object({
Expand All @@ -65,20 +80,61 @@ export const createAppRouter = () => {
}),
timestamp: z.number(),
text: z.string(),
resourceIds: z.array(z.string()),
resourceTypes: z.array(z.string()),
}),
}),
)
.query(async ({ ctx, input }) => {
return ctx.logger.messages.filter(
(entry) =>
input.filters.level[entry.level] &&
entry.timestamp &&
entry.timestamp >= input.filters.timestamp &&
(!input.filters.text ||
`${entry.message}${entry.ctx?.sourcePath}`
.toLowerCase()
.includes(input.filters.text.toLowerCase())),
);
const filters = input.filters;
const lowerCaseText = filters.text?.toLowerCase();
let noVerboseLogsCount = 0;

const filteredLogs = ctx.logger.messages.filter((entry) => {
if (entry.level !== "verbose") {
noVerboseLogsCount++;
}

// Filter by timestamp
if (entry.timestamp && entry.timestamp < filters.timestamp) {
return false;
}
// Filter by level
if (!filters.level[entry.level]) {
return false;
}
// Filter by resourceIds
if (
filters.resourceIds.length > 0 &&
(!entry.ctx?.sourcePath ||
!filters.resourceIds.includes(entry.ctx.sourcePath))
) {
return false;
}
// Filter by resourceTypes
if (
filters.resourceTypes.length > 0 &&
(!entry.ctx?.sourceType ||
!filters.resourceTypes.includes(entry.ctx.sourceType))
) {
return false;
}
// Filter by text
if (
lowerCaseText &&
!`${entry.message}${entry.ctx?.sourcePath}`
.toLowerCase()
.includes(lowerCaseText)
) {
return false;
}
return true;
});

return {
logs: filteredLogs,
hiddenLogs: noVerboseLogsCount - filteredLogs.length,
};
}),
"app.error": createProcedure.query(({ ctx }) => {
return ctx.errorMessage();
Expand Down
Loading

0 comments on commit 02b0fd0

Please sign in to comment.