Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
7d991b2
feat: init new projects view
ogzhanolguncu Aug 4, 2025
1f3f1c8
fix: typo
ogzhanolguncu Aug 4, 2025
9666438
feat: add dummy controls
ogzhanolguncu Aug 4, 2025
f279a56
feat: add parts of card
ogzhanolguncu Aug 4, 2025
e15cffb
refactor: organize
ogzhanolguncu Aug 4, 2025
c531912
refactor: gradient
ogzhanolguncu Aug 4, 2025
2322c0b
refactor: finalize gradient
ogzhanolguncu Aug 4, 2025
3b1b1d3
feat: add tRPC back
ogzhanolguncu Aug 4, 2025
04f2370
feat: add missing load more and skeleton
ogzhanolguncu Aug 4, 2025
8256fd9
fix: skeleton and layout
ogzhanolguncu Aug 5, 2025
b831ac2
feat: add search
ogzhanolguncu Aug 5, 2025
c998843
fix: add missing empty state
ogzhanolguncu Aug 5, 2025
d3a061f
feat: add create project dialog
ogzhanolguncu Aug 5, 2025
74fcb95
feat: add actinos
ogzhanolguncu Aug 5, 2025
2aa3a02
fix: action skeleton
ogzhanolguncu Aug 5, 2025
13aac18
chore: add back flag check
ogzhanolguncu Aug 5, 2025
60ec968
refactor: create hook
ogzhanolguncu Aug 5, 2025
8b9eb98
chore: remove redundant log
ogzhanolguncu Aug 5, 2025
3f7632f
Merge branch 'main' into new-projects-ui
ogzhanolguncu Aug 5, 2025
2250bfb
fix: coderabbit issues
ogzhanolguncu Aug 7, 2025
2dae742
refactor: redundant check
ogzhanolguncu Aug 7, 2025
c306874
Merge branch 'main' into new-projects-ui
ogzhanolguncu Aug 7, 2025
b0e0fae
fix: add redirection to deployments
ogzhanolguncu Aug 8, 2025
293a40e
fix: comments
ogzhanolguncu Aug 11, 2025
6e63913
feat: init deployments list
ogzhanolguncu Aug 12, 2025
e9b98ea
chore: add missing icons for new ui
ogzhanolguncu Aug 12, 2025
252fd7d
feat:Add status for deployments
ogzhanolguncu Aug 12, 2025
5f6aff2
chore: fmt
ogzhanolguncu Aug 12, 2025
2d036a5
feat: finish columns
ogzhanolguncu Aug 13, 2025
ac58391
feat: add env column
ogzhanolguncu Aug 13, 2025
5410a7c
feat: fix navigation
ogzhanolguncu Aug 13, 2025
c9444e4
fix: skeletons
ogzhanolguncu Aug 13, 2025
e3ab152
fix: zindex
ogzhanolguncu Aug 13, 2025
b66d7bc
feat: add initial filter UI
ogzhanolguncu Aug 14, 2025
3616510
feat: add time filters
ogzhanolguncu Aug 14, 2025
d9a4377
feat: Add llm search
ogzhanolguncu Aug 15, 2025
c1b6e86
fix: repo redirection
ogzhanolguncu Aug 15, 2025
60e5791
Merge branch 'main' of github.com:unkeyed/unkey into new-deployments-…
ogzhanolguncu Aug 15, 2025
39039ba
Merge branch 'main' into new-deployments-list-ui
ogzhanolguncu Aug 18, 2025
bd4b861
fix: coderabbit issues
ogzhanolguncu Aug 18, 2025
e622e48
fix: ui inconsistencies
ogzhanolguncu Aug 18, 2025
59e49c7
fix: add more space between status and env and make sure to have more…
ogzhanolguncu Aug 19, 2025
f45db3b
fix: pr comments
ogzhanolguncu Aug 20, 2025
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

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { HISTORICAL_DATA_WINDOW } from "@/components/logs/constants";
import { ControlCloud } from "@unkey/ui";
import type { DeploymentListFilterField } from "../../filters.schema";
import { useFilters } from "../../hooks/use-filters";

const FIELD_DISPLAY_NAMES: Record<DeploymentListFilterField, string> = {
status: "Status",
environment: "Environment",
branch: "Branch",
startTime: "Start Time",
endTime: "End Time",
since: "Since",
} as const;

const formatFieldName = (field: string): string => {
if (field in FIELD_DISPLAY_NAMES) {
return FIELD_DISPLAY_NAMES[field as DeploymentListFilterField];
}
// Fallback for any missing fields
return field
.replace(/([A-Z])/g, " $1")
.replace(/^./, (str) => str.toUpperCase())
.trim();
};

export const DeploymentsListControlCloud = () => {
const { filters, updateFilters, removeFilter } = useFilters();

return (
<ControlCloud
historicalWindow={HISTORICAL_DATA_WINDOW}
formatFieldName={formatFieldName}
filters={filters}
removeFilter={removeFilter}
updateFilters={updateFilters}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { DatetimePopover } from "@/components/logs/datetime/datetime-popover";
import { cn } from "@/lib/utils";
import { Calendar } from "@unkey/icons";
import { Button } from "@unkey/ui";
import { useState } from "react";
import { useFilters } from "../../../../hooks/use-filters";

export const DeploymentListDatetime = () => {
const [title, setTitle] = useState<string | null>("Last 12 hours");
const { filters, updateFilters } = useFilters();

const timeValues = filters
.filter((f) => ["startTime", "endTime", "since"].includes(f.field))
.reduce(
(acc, f) => ({
// biome-ignore lint/performance/noAccumulatingSpread: it's safe to spread
...acc,
[f.field]: f.value,
}),
{},
);

return (
<DatetimePopover
maxDate={new Date()}
initialTimeValues={timeValues}
onDateTimeChange={(startTime, endTime, since) => {
const activeFilters = filters.filter(
(f) => !["endTime", "startTime", "since"].includes(f.field),
);
if (since !== undefined) {
updateFilters([
...activeFilters,
{
field: "since",
value: since,
id: crypto.randomUUID(),
operator: "is",
},
]);
return;
}
if (since === undefined && startTime) {
activeFilters.push({
field: "startTime",
value: startTime,
id: crypto.randomUUID(),
operator: "is",
});
if (endTime) {
activeFilters.push({
field: "endTime",
value: endTime,
id: crypto.randomUUID(),
operator: "is",
});
}
}
updateFilters(activeFilters);
}}
initialTitle={title ?? ""}
onSuggestionChange={setTitle}
>
<div className="group">
<Button
variant="ghost"
size="md"
className={cn(
"group-data-[state=open]:bg-gray-4 px-2 rounded-lg",
title ? "" : "opacity-50",
title !== "Last 12 hours" ? "bg-gray-4" : "",
)}
aria-label="Filter logs by time"
aria-haspopup="true"
title="Press 'T' to toggle filters"
disabled={!title}
>
<Calendar className="text-gray-9 size-4" />
<span className="text-gray-12 font-medium text-[13px]">{title ?? "Loading..."}</span>
</Button>
</div>
</DatetimePopover>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { FilterCheckbox } from "@/components/logs/checkbox/filter-checkbox";
import type { GroupedDeploymentStatus } from "../../../../../filters.schema";
import { deploymentListFilterFieldConfig } from "../../../../../filters.schema";
import { useFilters } from "../../../../../hooks/use-filters";

type StatusOption = {
id: number;
status: GroupedDeploymentStatus;
display: string;
checked: boolean;
};

const baseOptions: StatusOption[] = [
{
id: 1,
status: "pending",
display: "Pending",
checked: false,
},
{
id: 2,
status: "building",
display: "Building",
checked: false,
},
{
id: 3,
status: "completed",
display: "Ready",
checked: false,
},
{
id: 4,
status: "failed",
display: "Failed",
checked: false,
},
];

export const DeploymentStatusFilter = () => {
const { filters, updateFilters } = useFilters();
const getColorClass = deploymentListFilterFieldConfig.status.getColorClass;

return (
<FilterCheckbox
options={baseOptions}
filterField="status"
checkPath="status"
renderOptionContent={(checkbox) => (
<>
<div className={`size-2 ${getColorClass?.(checkbox.status)} rounded-[2px]`} />
<span className="text-accent-9 text-xs w-16">{checkbox.display}</span>
</>
)}
createFilterValue={(option) => ({
value: option.status,
metadata: {
colorClass: getColorClass?.(option.status),
},
})}
filters={filters}
updateFilters={updateFilters}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { FilterCheckbox } from "@/components/logs/checkbox/filter-checkbox";
import { useFilters } from "../../../../../hooks/use-filters";

type EnvironmentOption = {
id: number;
environment: string;
checked: boolean;
};

const options: EnvironmentOption[] = [
{ id: 1, environment: "production", checked: false },
{ id: 2, environment: "preview", checked: false },
] as const;

export const EnvironmentFilter = () => {
const { filters, updateFilters } = useFilters();

return (
<FilterCheckbox
options={options}
filterField="environment"
checkPath="environment"
selectionMode="single"
renderOptionContent={(checkbox) => (
<div className="text-accent-12 text-xs capitalize">{checkbox.environment}</div>
)}
createFilterValue={(option) => ({
value: option.environment,
})}
filters={filters}
updateFilters={updateFilters}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { FiltersPopover } from "@/components/logs/checkbox/filters-popover";
import { FilterOperatorInput } from "@/components/logs/filter-operator-input";
import { BarsFilter } from "@unkey/icons";
import { Button } from "@unkey/ui";
import { cn } from "@unkey/ui/src/lib/utils";
import {
type DeploymentListFilterField,
deploymentListFilterFieldConfig,
} from "../../../../filters.schema";
import { useFilters } from "../../../../hooks/use-filters";
import { DeploymentStatusFilter } from "./components/deployment-status-filter";
import { EnvironmentFilter } from "./components/environment-filter";

const FIELD_DISPLAY_CONFIG: Record<
Exclude<DeploymentListFilterField, "startTime" | "endTime" | "since">,
{ label: string; shortcut: string }
> = {
status: { label: "Status", shortcut: "s" },
environment: { label: "Environment", shortcut: "e" },
branch: { label: "Branch", shortcut: "b" },
} as const;

const displayableFields: (keyof typeof FIELD_DISPLAY_CONFIG)[] = [
"status",
"environment",
"branch",
];

export const DeploymentListFilters = () => {
const { filters, updateFilters } = useFilters();

// Generate filter items only for displayable fields
const filterItems = displayableFields.map((fieldName) => {
const fieldConfig = deploymentListFilterFieldConfig[fieldName];
const displayConfig = FIELD_DISPLAY_CONFIG[fieldName];

// Use checkbox components for status and environment
if (fieldName === "status") {
return {
id: fieldName,
label: displayConfig.label,
shortcut: displayConfig.shortcut,
component: <DeploymentStatusFilter />,
};
}

if (fieldName === "environment") {
return {
id: fieldName,
label: displayConfig.label,
shortcut: displayConfig.shortcut,
component: <EnvironmentFilter />,
};
}

// Use operator input for other fields (like branch)
const options = fieldConfig.operators.map((op) => ({
id: op,
label: op,
}));

const activeFilter = filters.find((f) => f.field === fieldName);

return {
id: fieldName,
label: displayConfig.label,
shortcut: displayConfig.shortcut,
component: (
<FilterOperatorInput
label={displayConfig.label}
options={options}
defaultOption={activeFilter?.operator}
defaultText={activeFilter?.value as string}
onApply={(operator, text) => {
// Remove existing filters for this field
const filtersWithoutCurrent = filters.filter((f) => f.field !== fieldName);
// Add new filter
updateFilters([
...filtersWithoutCurrent,
{
field: fieldName,
id: crypto.randomUUID(),
operator,
value: text,
},
]);
}}
/>
),
};
});

return (
<FiltersPopover items={filterItems} activeFilters={filters}>
<div className="group">
<Button
variant="ghost"
className={cn(
"group-data-[state=open]:bg-gray-4 px-2 rounded-lg",
filters.length > 0 ? "bg-gray-4" : "",
)}
aria-label="Filter deployments"
aria-haspopup="true"
size="md"
title="Press 'F' to toggle filters"
>
<BarsFilter className="text-accent-9 size-4" />
<span className="text-accent-12 font-medium text-[13px]">Filter</span>
{filters.length > 0 && (
<div className="bg-gray-7 rounded h-4 px-1 text-[11px] font-medium text-accent-12 text-center flex items-center justify-center">
{filters.length}
</div>
)}
</Button>
</div>
</FiltersPopover>
);
};
Loading