Skip to content

Commit 5b75050

Browse files
committed
feat(frontend): add storage unit card
1 parent e86ab0f commit 5b75050

File tree

5 files changed

+222
-16
lines changed

5 files changed

+222
-16
lines changed

frontend/src/components/dropdown.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export const Dropdown: FC<IDropdownProps> = (props) => {
5959
{props.value?.icon} {props.value?.label}
6060
</div>
6161
{cloneElement(Icons.DownCaret, {
62-
className: "w-4 h-4 stroke-gray-600",
62+
className: "w-4 h-4 stroke-neutral-600 dark:stroke-neutral-400",
6363
})}
6464
</div>
6565
<div className={classNames("absolute z-10 divide-y rounded-lg shadow bg-white py-1 border border-gray-200 overflow-y-auto max-h-40 dark:bg-white/10 dark:backdrop-blur-md dark:border-white/20", {

frontend/src/generated/graphql.tsx

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,30 @@ export type LoginCredentials = {
6464

6565
export type Mutation = {
6666
__typename?: 'Mutation';
67+
AddRow: StatusResponse;
68+
AddStorageUnit: StatusResponse;
6769
Login: StatusResponse;
6870
Logout: StatusResponse;
6971
UpdateStorageUnit: StatusResponse;
7072
};
7173

7274

75+
export type MutationAddRowArgs = {
76+
schema: Scalars['String']['input'];
77+
storageUnit: Scalars['String']['input'];
78+
type: DatabaseType;
79+
values: Array<RecordInput>;
80+
};
81+
82+
83+
export type MutationAddStorageUnitArgs = {
84+
fields: Array<RecordInput>;
85+
schema: Scalars['String']['input'];
86+
storageUnit: Scalars['String']['input'];
87+
type: DatabaseType;
88+
};
89+
90+
7391
export type MutationLoginArgs = {
7492
credentials: LoginCredentials;
7593
};
@@ -201,6 +219,16 @@ export type RawExecuteQueryVariables = Exact<{
201219

202220
export type RawExecuteQuery = { __typename?: 'Query', RawExecute: { __typename?: 'RowsResult', Rows: Array<Array<string>>, Columns: Array<{ __typename?: 'Column', Type: string, Name: string }> } };
203221

222+
export type AddStorageUnitMutationVariables = Exact<{
223+
type: DatabaseType;
224+
schema: Scalars['String']['input'];
225+
storageUnit: Scalars['String']['input'];
226+
fields: Array<RecordInput> | RecordInput;
227+
}>;
228+
229+
230+
export type AddStorageUnitMutation = { __typename?: 'Mutation', AddStorageUnit: { __typename?: 'StatusResponse', Status: boolean } };
231+
204232
export type GetStorageUnitRowsQueryVariables = Exact<{
205233
type: DatabaseType;
206234
schema: Scalars['String']['input'];
@@ -469,6 +497,47 @@ export type RawExecuteQueryHookResult = ReturnType<typeof useRawExecuteQuery>;
469497
export type RawExecuteLazyQueryHookResult = ReturnType<typeof useRawExecuteLazyQuery>;
470498
export type RawExecuteSuspenseQueryHookResult = ReturnType<typeof useRawExecuteSuspenseQuery>;
471499
export type RawExecuteQueryResult = Apollo.QueryResult<RawExecuteQuery, RawExecuteQueryVariables>;
500+
export const AddStorageUnitDocument = gql`
501+
mutation AddStorageUnit($type: DatabaseType!, $schema: String!, $storageUnit: String!, $fields: [RecordInput!]!) {
502+
AddStorageUnit(
503+
type: $type
504+
schema: $schema
505+
storageUnit: $storageUnit
506+
fields: $fields
507+
) {
508+
Status
509+
}
510+
}
511+
`;
512+
export type AddStorageUnitMutationFn = Apollo.MutationFunction<AddStorageUnitMutation, AddStorageUnitMutationVariables>;
513+
514+
/**
515+
* __useAddStorageUnitMutation__
516+
*
517+
* To run a mutation, you first call `useAddStorageUnitMutation` within a React component and pass it any options that fit your needs.
518+
* When your component renders, `useAddStorageUnitMutation` returns a tuple that includes:
519+
* - A mutate function that you can call at any time to execute the mutation
520+
* - An object with fields that represent the current status of the mutation's execution
521+
*
522+
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
523+
*
524+
* @example
525+
* const [addStorageUnitMutation, { data, loading, error }] = useAddStorageUnitMutation({
526+
* variables: {
527+
* type: // value for 'type'
528+
* schema: // value for 'schema'
529+
* storageUnit: // value for 'storageUnit'
530+
* fields: // value for 'fields'
531+
* },
532+
* });
533+
*/
534+
export function useAddStorageUnitMutation(baseOptions?: Apollo.MutationHookOptions<AddStorageUnitMutation, AddStorageUnitMutationVariables>) {
535+
const options = {...defaultOptions, ...baseOptions}
536+
return Apollo.useMutation<AddStorageUnitMutation, AddStorageUnitMutationVariables>(AddStorageUnitDocument, options);
537+
}
538+
export type AddStorageUnitMutationHookResult = ReturnType<typeof useAddStorageUnitMutation>;
539+
export type AddStorageUnitMutationResult = Apollo.MutationResult<AddStorageUnitMutation>;
540+
export type AddStorageUnitMutationOptions = Apollo.BaseMutationOptions<AddStorageUnitMutation, AddStorageUnitMutationVariables>;
472541
export const GetStorageUnitRowsDocument = gql`
473542
query GetStorageUnitRows($type: DatabaseType!, $schema: String!, $storageUnit: String!, $where: String!, $pageSize: Int!, $pageOffset: Int!) {
474543
Row(
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
mutation AddStorageUnit($type: DatabaseType!, $schema: String!, $storageUnit: String!, $fields: [RecordInput!]!) {
2+
AddStorageUnit(type: $type, schema: $schema, storageUnit: $storageUnit, fields: $fields) {
3+
Status
4+
}
5+
}

frontend/src/pages/storage-unit/storage-unit.tsx

Lines changed: 141 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
1-
import { useQuery } from "@apollo/client";
1+
import classNames from "classnames";
2+
import { clone } from "lodash";
23
import { FC, useCallback, useMemo, useState } from "react";
34
import { useNavigate } from "react-router-dom";
45
import { Handle, Position } from "reactflow";
5-
import { AnimatedButton } from "../../components/button";
6+
import { ActionButton, AnimatedButton } from "../../components/button";
67
import { Card, ExpandableCard } from "../../components/card";
78
import { EmptyMessage } from "../../components/common";
9+
import { createDropdownItem, Dropdown } from "../../components/dropdown";
810
import { IGraphCardProps } from "../../components/graph/graph";
911
import { Icons } from "../../components/icons";
12+
import { Input, InputWithlabel, Label } from "../../components/input";
1013
import { Loading } from "../../components/loading";
1114
import { InternalPage } from "../../components/page";
1215
import { InternalRoutes } from "../../config/routes";
13-
import { DatabaseType, GetStorageUnitsDocument, GetStorageUnitsQuery, GetStorageUnitsQueryVariables, StorageUnit } from "../../generated/graphql";
16+
import { DatabaseType, RecordInput, StorageUnit, useAddStorageUnitMutation, useGetStorageUnitsQuery } from "../../generated/graphql";
17+
import { notify } from "../../store/function";
1418
import { useAppSelector } from "../../store/hooks";
15-
import { getDatabaseStorageUnitLabel } from "../../utils/functions";
19+
import { getDatabaseStorageUnitLabel, isNoSQL } from "../../utils/functions";
1620

1721
const StorageUnitCard: FC<{ unit: StorageUnit }> = ({ unit }) => {
1822
const [expanded, setExpanded] = useState(false);
@@ -80,9 +84,14 @@ const StorageUnitCard: FC<{ unit: StorageUnit }> = ({ unit }) => {
8084

8185
export const StorageUnitPage: FC = () => {
8286
const navigate = useNavigate();
87+
const [create, setCreate] = useState(false);
88+
const [storageUnitName, setStorageUnitName] = useState("");
89+
const [fields, setFields] = useState<RecordInput[]>([ {Key: "", Value: "" }]);
90+
const [error, setError] = useState<string>();
8391
const schema = useAppSelector(state => state.database.schema);
8492
const current = useAppSelector(state => state.auth.current);
85-
const { loading, data } = useQuery<GetStorageUnitsQuery, GetStorageUnitsQueryVariables>(GetStorageUnitsDocument, {
93+
const [addStorageUnit,] = useAddStorageUnitMutation();
94+
const { loading, data } = useGetStorageUnitsQuery({
8695
variables: {
8796
type: current?.Type as DatabaseType,
8897
schema,
@@ -99,6 +108,82 @@ export const StorageUnitPage: FC = () => {
99108
];
100109
}, [current]);
101110

111+
const handleCreate = useCallback(() => {
112+
setCreate(!create);
113+
}, [create]);
114+
115+
const handleSubmit = useCallback(() => {
116+
if (storageUnitName.length === 0) {
117+
return setError("Name is required");
118+
}
119+
if (fields.some(field => field.Key.length === 0 || field.Value.length === 0)) {
120+
return setError("Fields cannot be empty");
121+
}
122+
setError(undefined);
123+
addStorageUnit({
124+
variables: {
125+
type: current?.Type as DatabaseType,
126+
schema,
127+
storageUnit: storageUnitName,
128+
fields,
129+
},
130+
onCompleted() {
131+
notify(`${getDatabaseStorageUnitLabel(current?.Type, true)} ${storageUnitName} created successfully!`, "success");
132+
setStorageUnitName("");
133+
setFields([]);
134+
},
135+
onError(e) {
136+
notify(e.message, "error");
137+
},
138+
});
139+
}, [addStorageUnit, current?.Type, fields, schema, storageUnitName]);
140+
141+
const handleAddField = useCallback(() => {
142+
setFields(f => [...f, { Key: "", Value: "" }]);
143+
}, []);
144+
145+
const handleFieldValueChange = useCallback((type: "Key" | "Value", index: number, value: string) => {
146+
setFields(f => {
147+
const newF = clone(f);
148+
newF[index][type] = value;
149+
return newF;
150+
});
151+
}, []);
152+
153+
const handleRemove = useCallback((index: number) => {
154+
if (fields.length <= 1) {
155+
return;
156+
}
157+
setFields(f => {
158+
const newF = clone(f);
159+
newF.splice(index, 1);
160+
return newF;
161+
})
162+
}, [fields.length]);
163+
164+
const storageUnitTypesDropdownItems = useMemo(() => {
165+
if (current?.Type == null || isNoSQL(current.Type)) {
166+
return [];
167+
}
168+
let items: string[] = [];
169+
170+
switch(current.Type) {
171+
case DatabaseType.MariaDb:
172+
items = ["VARCHAR", "INT", "TEXT", "DATE", "BOOLEAN"];
173+
break;
174+
case DatabaseType.MySql:
175+
items = ["VARCHAR", "INT", "TEXT", "DATE", "BOOLEAN"];
176+
break;
177+
case DatabaseType.Postgres:
178+
items = ["VARCHAR", "INT", "TEXT", "DATE", "BOOLEAN", "UUID", "JSONB"];
179+
break;
180+
case DatabaseType.Sqlite3:
181+
items = ["TEXT", "INTEGER", "REAL", "BLOB", "NUMERIC"];
182+
break;
183+
}
184+
return items.map(item => createDropdownItem(item));
185+
}, [current?.Type]);
186+
102187
if (loading) {
103188
return <InternalPage routes={routes}>
104189
<Loading />
@@ -112,10 +197,57 @@ export const StorageUnitPage: FC = () => {
112197
{
113198
data != null && (
114199
data.StorageUnit.length === 0
115-
? <EmptyMessage icon={Icons.SadSmile} label={`No ${getDatabaseStorageUnitLabel(current?.Type)} found`} />
116-
: data.StorageUnit.map(unit => (
117-
<StorageUnitCard key={unit.Name} unit={unit} />
118-
))
200+
? <>
201+
<EmptyMessage icon={Icons.SadSmile} label={`No ${getDatabaseStorageUnitLabel(current?.Type)} found`} />
202+
</>
203+
: <>
204+
<ExpandableCard className="overflow-visible" icon={{
205+
bgClassName: "bg-teal-500",
206+
component: Icons.Add,
207+
}} isExpanded={create} tag={<div className="text-red-700 dark:text-red-400 text-xs">
208+
{error}
209+
</div>}>
210+
<div className="flex grow flex-col justify-between my-2 text-neutral-800 dark:text-neutral-100">
211+
Create a {getDatabaseStorageUnitLabel(current?.Type, true)}
212+
<AnimatedButton className="self-end" icon={Icons.Add} label="Create" onClick={handleCreate} />
213+
</div>
214+
<div className="flex grow flex-col justify-between my-2 gap-4">
215+
<div className="flex flex-col gap-2">
216+
<InputWithlabel label="Name" value={storageUnitName} setValue={setStorageUnitName} />
217+
<div className="flex gap-2 justify-between">
218+
<Label label="Field Name" />
219+
<Label label="Value" />
220+
<div className="w-14" />
221+
</div>
222+
{
223+
fields.map((field, index) => (
224+
<div className="flex gap-2">
225+
<Input inputProps={{ className: "w-1/2" }} value={field.Key} setValue={(value) => handleFieldValueChange("Key", index, value)} placeholder="Enter field name" />
226+
<Dropdown className="w-1/2" items={storageUnitTypesDropdownItems} value={createDropdownItem(field.Value)}
227+
onChange={(item) => handleFieldValueChange("Value", index, item.id)} />
228+
<div className="flex items-end mb-2">
229+
<ActionButton disabled={fields.length === 1} containerClassName="w-6 h-6" icon={Icons.Delete} className={classNames({
230+
"stroke-red-500 dark:stroke-red-400": fields.length > 1,
231+
"stroke-neutral-300 dark:stroke-neutral-600": fields.length === 1,
232+
})} onClick={() => handleRemove(index)} />
233+
</div>
234+
</div>
235+
))
236+
}
237+
<AnimatedButton className="self-end" icon={Icons.Add} label="Add field" onClick={handleAddField} />
238+
</div>
239+
<div className="flex items-center justify-between">
240+
<AnimatedButton icon={Icons.Cancel} label="Cancel" onClick={handleCreate} />
241+
<AnimatedButton labelClassName="text-green-600 dark:text-green-300"
242+
iconClassName="stroke-green-600 dark:stroke-green-300" icon={Icons.Add}
243+
label="Submit" onClick={handleSubmit} />
244+
</div>
245+
</div>
246+
</ExpandableCard>
247+
{data.StorageUnit.map(unit => (
248+
<StorageUnitCard key={unit.Name} unit={unit} />
249+
))}
250+
</>
119251
)
120252
}
121253
</InternalPage>

frontend/src/utils/functions.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,18 +44,18 @@ export function isNoSQL(databaseType: string) {
4444
return false;
4545
}
4646

47-
export function getDatabaseStorageUnitLabel(databaseType: string | undefined) {
47+
export function getDatabaseStorageUnitLabel(databaseType: string | undefined, singular: boolean = false) {
4848
switch(databaseType) {
4949
case DatabaseType.ElasticSearch:
50-
return "Indices";
50+
return singular ? "Index" : "Indices";
5151
case DatabaseType.MongoDb:
52-
return "Collections";
52+
return singular ? "Collection" : "Collections";
5353
case DatabaseType.Redis:
54-
return "Keys";
54+
return singular ? "Key" : "Keys";
5555
case DatabaseType.MySql:
5656
case DatabaseType.Postgres:
5757
case DatabaseType.Sqlite3:
58-
return "Tables";
58+
return singular ? "Table" : "Tables";
5959
}
60-
return "Storage Units";
60+
return singular ? "Storage Unit" : "Storage Units";
6161
}

0 commit comments

Comments
 (0)