• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

stacklok / codegate-ui / 12805902815

16 Jan 2025 09:28AM UTC coverage: 68.977% (-0.6%) from 69.536%
12805902815

Pull #80

github

web-flow
Merge 94615af54 into ef1b65ca4
Pull Request #80: feat: use @stacklok/ui-kit

208 of 379 branches covered (54.88%)

Branch coverage included in aggregate %.

20 of 26 new or added lines in 8 files covered. (76.92%)

1 existing line in 1 file now uncovered.

399 of 501 relevant lines covered (79.64%)

25.96 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.18
/src/features/dashboard/components/card-codegate-status.tsx
1
import {
2
  Card,
3
  CardBody,
4
  CardFooter,
5
  CardHeader,
6
  CardTitle,
7
  Cell,
8
  Column,
9
  Label,
10
  Row,
11
  Select,
12
  SelectButton,
13
  Table,
14
  TableBody,
15
  TableHeader,
16
  TDropdownItemOrSection,
17
} from "@stacklok/ui-kit";
18

19
import { useQuery } from "@tanstack/react-query";
20
import { format } from "date-fns";
21
import { CheckCircle2, LoaderCircle, XCircle } from "lucide-react";
22
import { Dispatch, SetStateAction, useState } from "react";
23

24
const INTERVAL = {
3✔
25
  "1_SEC": { value: 1_000, name: "1 second" },
26
  "5_SEC": { value: 5_000, name: "5 seconds" },
27
  "10_SEC": { value: 10_000, name: "10 seconds" },
28
  "30_SEC": { value: 30_000, name: "30 seconds" },
29
  "1_MIN": { value: 60_000, name: "1 minute" },
30
  "5_MIN": { value: 300_000, name: "5 minutes" },
31
  "10_MIN": { value: 600_000, name: "10 minutes" },
32
} as const;
33

34
const INTERVAL_SELECT_ITEMS: TDropdownItemOrSection[] = Object.entries(
3✔
35
  INTERVAL,
36
).map(([key, { name }]) => {
37
  return { textValue: name, id: key };
21✔
38
});
39

40
const DEFAULT_INTERVAL: Interval = "5_SEC";
3✔
41

42
type Interval = keyof typeof INTERVAL;
43

44
enum Status {
6✔
45
  HEALTHY = "Healthy",
46
  UNHEALTHY = "Unhealthy",
47
}
48

49
type HealthResp = { status: "healthy" | unknown } | null;
50

51
const getStatus = async (): Promise<Status | null> => {
3✔
52
  const resp = await fetch(
10✔
53
    new URL("/health", import.meta.env.VITE_BASE_API_URL),
54
  );
55
  const data = (await resp.json()) as unknown as HealthResp;
9✔
56

57
  if (data?.status === "healthy") return Status.HEALTHY;
9✔
58
  if (data?.status !== "healthy") return Status.UNHEALTHY;
1!
59

60
  return null;
×
61
};
62

63
const useStatus = (pollingInterval: Interval) =>
3✔
64
  useQuery({
64✔
65
    queryFn: getStatus,
66
    queryKey: ["getStatus", { pollingInterval }],
67
    refetchInterval: INTERVAL[pollingInterval].value,
68
    staleTime: Infinity,
69
    gcTime: Infinity,
70
    refetchIntervalInBackground: true,
71
    refetchOnMount: true,
72
    refetchOnReconnect: true,
73
    refetchOnWindowFocus: true,
74
    retry: false,
75
  });
76

77
const StatusText = ({
3✔
78
  status,
79
  isPending,
80
}: {
81
  status: Status | null;
82
  isPending: boolean;
83
}) => {
84
  if (isPending || status === null) {
63✔
85
    return (
86
      <div className="flex gap-2 items-center text-secondary justify-end overflow-hidden">
87
        Checking <LoaderCircle className="size-4 animate-spin" />
88
      </div>
89
    );
90
  }
91

92
  switch (status) {
46!
93
    case Status.HEALTHY:
94
      return (
95
        <div className="flex gap-2 items-center text-primary justify-end">
96
          {Status.HEALTHY} <CheckCircle2 className="size-4" />
97
        </div>
98
      );
99
    case Status.UNHEALTHY:
100
      return (
101
        <div className="flex gap-2 items-center text-primary justify-end overflow-hidden">
102
          {Status.UNHEALTHY} <XCircle className="size-4" />
103
        </div>
104
      );
105
    default: {
106
      status satisfies never;
×
107
    }
108
  }
109
};
110

111
function ErrorUI() {
112
  return (
113
    <div className="flex flex-col items-center justify-center py-8">
114
      <XCircle className="text-red-600 mb-2 size-8" />
115
      <div className="text-base font-semibold text-secondary text-center">
116
        An error occurred
117
      </div>
118
      <div className="text-sm text-secondary text-center text-balance">
119
        If this issue persists, please reach out to us on{" "}
120
        <a
121
          className="underline text-secondary"
122
          href="https://discord.gg/stacklok"
123
          rel="noopener noreferrer"
124
          target="_blank"
125
        >
126
          Discord
127
        </a>{" "}
128
        or open a new{" "}
129
        <a
130
          className="underline text-secondary"
131
          href="https://github.com/stacklok/codegate/issues/new"
132
          rel="noopener noreferrer"
133
          target="_blank"
134
        >
135
          Github issue
136
        </a>
137
      </div>
138
    </div>
139
  );
140
}
141

142
function PollIntervalControl({
143
  className,
144
  pollingInterval,
145
  setPollingInterval,
146
}: {
147
  className?: string;
148
  pollingInterval: Interval;
149
  setPollingInterval: Dispatch<SetStateAction<Interval>>;
150
}) {
151
  return (
152
    <Select
153
      className={className}
NEW
154
      onSelectionChange={(v) => setPollingInterval(v.toString() as Interval)}
×
155
      items={INTERVAL_SELECT_ITEMS}
156
      defaultSelectedKey={pollingInterval}
157
    >
158
      <Label className="w-full text-right font-semibold text-secondary pr-2 -mb-1">
159
        Check for updates
160
      </Label>
161
      <SelectButton
162
        isBorderless
163
        className="h-7 max-w-36 [&>span>span]:text-right [&>span>span]:justify-end !gap-0 text-secondary"
164
      />
165
    </Select>
166
  );
167
}
168

169
export function InnerContent({
170
  isError,
171
  isPending,
172
  data,
173
}: Pick<ReturnType<typeof useStatus>, "data" | "isPending" | "isError">) {
174
  if (!isPending && isError) {
64✔
175
    return <ErrorUI />;
176
  }
177

178
  return (
179
    <Table className="h-max" aria-label="CodeGate status checks">
180
      <TableHeader className="hidden">
181
        <Column isRowHeader>Name</Column>
182
        <Column>Value</Column>
183
      </TableHeader>
184
      <TableBody>
185
        <Row className="hover:bg-transparent">
186
          <Cell className="pl-0">CodeGate server</Cell>
187
          <Cell className="pr-0 text-end">
188
            <StatusText isPending={isPending} status={data ?? null} />
80✔
189
          </Cell>
190
        </Row>
191
      </TableBody>
192
    </Table>
193
  );
194
}
195

196
export function CardCodegateStatus() {
197
  const [pollingInterval, setPollingInterval] = useState<Interval>(
64✔
198
    () => DEFAULT_INTERVAL,
10✔
199
  );
200
  const { data, dataUpdatedAt, isPending, isError } =
201
    useStatus(pollingInterval);
64✔
202

203
  return (
204
    <Card className="h-full flex flex-col">
205
      <CardHeader>
206
        <CardTitle className="flex justify-between items-center">
207
          <span className="block">CodeGate Status</span>
208
        </CardTitle>
209
      </CardHeader>
210

211
      <CardBody className="h-max">
212
        <InnerContent data={data} isPending={isPending} isError={isError} />
213
      </CardBody>
214

215
      <CardFooter className="items-start border-t border-gray-200 mt-auto py-2">
216
        <div>
217
          <div className="text-sm font-semibold text-secondary">
218
            Last checked
219
          </div>
220
          <div className="text-sm text-secondary">
221
            {format(new Date(dataUpdatedAt), "pp")}
222
          </div>
223
        </div>
224

225
        <PollIntervalControl
226
          className="ml-auto"
227
          pollingInterval={pollingInterval}
228
          setPollingInterval={setPollingInterval}
229
        />
230
      </CardFooter>
231
    </Card>
232
  );
233
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc