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

stacklok / codegate-ui / 12949002354

24 Jan 2025 11:46AM UTC coverage: 71.054% (+1.8%) from 69.253%
12949002354

Pull #185

github

web-flow
Merge ce98c6cc8 into 646ed5a6b
Pull Request #185: feat: implement hard delete for workspaces

334 of 576 branches covered (57.99%)

Branch coverage included in aggregate %.

94 of 115 new or added lines in 18 files covered. (81.74%)

2 existing lines in 2 files now uncovered.

724 of 913 relevant lines covered (79.3%)

65.31 hits per line

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

77.61
/src/features/workspace/components/workspace-custom-instructions.tsx
1
import Editor, { type Theme } from "@monaco-editor/react";
2
import {
3
  Button,
4
  Card,
5
  CardBody,
6
  CardFooter,
7
  DarkModeContext,
8
  Loader,
9
  Text,
10
} from "@stacklok/ui-kit";
11
import {
12
  Dispatch,
13
  SetStateAction,
14
  useCallback,
15
  useContext,
16
  useEffect,
17
  useMemo,
18
  useState,
19
} from "react";
20

21
import { twMerge } from "tailwind-merge";
22
import {
23
  V1GetWorkspaceCustomInstructionsData,
24
  V1GetWorkspaceCustomInstructionsResponse,
25
  V1SetWorkspaceCustomInstructionsData,
26
} from "@/api/generated";
27

28
import {
29
  QueryCacheNotifyEvent,
30
  QueryClient,
31
  useQueryClient,
32
} from "@tanstack/react-query";
33
import { v1GetWorkspaceCustomInstructionsQueryKey } from "@/api/generated/@tanstack/react-query.gen";
34
import { useQueryGetWorkspaceCustomInstructions } from "../hooks/use-query-get-workspace-custom-instructions";
35
import { useMutationSetWorkspaceCustomInstructions } from "../hooks/use-mutation-set-workspace-custom-instructions";
36

37
type DarkModeContextValue = {
38
  preference: "dark" | "light" | null;
39
  override: "dark" | "light" | null;
40
};
41

42
function inferDarkMode(
43
  contextValue:
44
    | null
45
    | [DarkModeContextValue, Dispatch<SetStateAction<DarkModeContextValue>>],
46
): Theme {
47
  if (contextValue === null) return "light";
14!
48

49
  // Handle override
50
  if (contextValue[0].override === "dark") return "vs-dark";
14!
51
  if (contextValue[0].override === "light") return "light";
14!
52

53
  // Handle preference
54
  if (contextValue[0].preference === "dark") return "vs-dark";
14!
55
  return "light";
14✔
56
}
57

58
function EditorLoadingUI() {
59
  return (
60
    // arbitrary value to match the monaco editor height
61
    // eslint-disable-next-line tailwindcss/no-unnecessary-arbitrary-value
62
    <div className="min-h-[20rem] w-full flex items-center justify-center">
63
      <Loader className="my-auto" />
64
    </div>
65
  );
66
}
67

68
function isGetWorkspaceCustomInstructionsQuery(
69
  queryKey: unknown,
70
  options: V1GetWorkspaceCustomInstructionsData,
71
): boolean {
72
  return (
7✔
73
    Array.isArray(queryKey) &&
14✔
74
    queryKey[0]._id ===
75
      v1GetWorkspaceCustomInstructionsQueryKey(options)[0]?._id
76
  );
77
}
78

79
function getCustomInstructionsFromEvent(
80
  event: QueryCacheNotifyEvent,
81
): string | null {
82
  if ("action" in event === false || "data" in event.action === false)
2!
83
    return null;
×
84
  return (
2✔
85
    (
2!
86
      event.action.data as
87
        | V1GetWorkspaceCustomInstructionsResponse
88
        | undefined
89
        | null
90
    )?.prompt ?? null
91
  );
92
}
93

94
function useCustomInstructionsValue({
95
  initialValue,
96
  options,
97
  queryClient,
98
}: {
99
  initialValue: string;
100
  options: V1GetWorkspaceCustomInstructionsData;
101
  queryClient: QueryClient;
102
}) {
103
  const [value, setValue] = useState<string>(initialValue);
14✔
104

105
  // Subscribe to changes in the workspace system prompt value in the query cache
106
  useEffect(() => {
14✔
107
    const queryCache = queryClient.getQueryCache();
7✔
108
    const unsubscribe = queryCache.subscribe((event) => {
7✔
109
      if (
66✔
110
        event.type === "updated" &&
98✔
111
        event.action.type === "success" &&
112
        isGetWorkspaceCustomInstructionsQuery(
113
          event.query.options.queryKey,
114
          options,
115
        )
116
      ) {
117
        const prompt: string | null = getCustomInstructionsFromEvent(event);
2✔
118
        if (prompt === value || prompt === null) return;
2!
119

120
        setValue(prompt);
2✔
121
      }
122
    });
123

124
    return () => {
7✔
125
      return unsubscribe();
7✔
126
    };
127
  }, [options, queryClient, value]);
128

129
  return { value, setValue };
14✔
130
}
131

132
export function WorkspaceCustomInstructions({
133
  className,
134
  workspaceName,
135
  isArchived,
136
}: {
137
  className?: string;
138
  workspaceName: string;
139
  isArchived: boolean | undefined;
140
}) {
141
  const context = useContext(DarkModeContext);
14✔
142
  const theme: Theme = inferDarkMode(context);
14✔
143

144
  const options: V1GetWorkspaceCustomInstructionsData &
145
    Omit<V1SetWorkspaceCustomInstructionsData, "body"> = useMemo(
14✔
146
    () => ({
5✔
147
      path: { workspace_name: workspaceName },
148
    }),
149
    [workspaceName],
150
  );
151

152
  const queryClient = useQueryClient();
14✔
153

154
  const {
155
    data: customInstructionsResponse,
156
    isPending: isCustomInstructionsPending,
157
  } = useQueryGetWorkspaceCustomInstructions(options);
14✔
158
  const { mutateAsync, isPending: isMutationPending } =
159
    useMutationSetWorkspaceCustomInstructions(options);
14✔
160

161
  const { setValue, value } = useCustomInstructionsValue({
14✔
162
    initialValue: customInstructionsResponse?.prompt ?? "",
24✔
163
    options,
164
    queryClient,
165
  });
166

167
  const handleSubmit = useCallback(
14✔
168
    (value: string) => {
NEW
169
      mutateAsync(
×
170
        { ...options, body: { prompt: value } },
171
        {
172
          onSuccess: () => {
173
            queryClient.invalidateQueries({
×
174
              queryKey: v1GetWorkspaceCustomInstructionsQueryKey(options),
175
              refetchType: "all",
176
            });
177
          },
178
        },
179
      );
180
    },
181
    [mutateAsync, options, queryClient],
182
  );
183

184
  return (
185
    <Card className={twMerge(className, "shrink-0")}>
186
      <CardBody>
187
        <Text className="text-primary">Custom instructions</Text>
188
        <Text className="text-secondary mb-4">
189
          Pass custom instructions to your LLM to augment it's behavior, and
190
          save time & tokens.
191
        </Text>
192
        <div className="border border-gray-200 rounded overflow-hidden">
193
          {isCustomInstructionsPending ? (
194
            <EditorLoadingUI />
10✔
195
          ) : (
196
            <Editor
197
              options={{
198
                minimap: { enabled: false },
199
                readOnly: isArchived,
200
              }}
201
              value={value}
202
              onChange={(v) => setValue(v ?? "")}
×
203
              height="20rem"
204
              defaultLanguage="Markdown"
205
              theme={theme}
206
              className={twMerge("bg-base", isArchived ? "opacity-25" : "")}
4!
207
            />
208
          )}
209
        </div>
210
      </CardBody>
211
      <CardFooter className="justify-end gap-2">
212
        <Button
213
          isPending={isMutationPending}
214
          isDisabled={Boolean(isArchived ?? isCustomInstructionsPending)}
26✔
215
          onPress={() => handleSubmit(value)}
×
216
          variant="secondary"
217
        >
218
          Save
219
        </Button>
220
      </CardFooter>
221
    </Card>
222
  );
223
}
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