From 8e899dad7802b676ab57212cf16ddbe96057c546 Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Tue, 22 Nov 2022 03:04:26 +0800 Subject: [PATCH 01/29] enable derivatives logging in code editor --- src/components/CodeEditor/CodeEditorHelper.tsx | 4 ++++ src/components/fields/Derivative/Settings.tsx | 4 ++-- src/components/fields/Derivative/derivative.d.ts | 5 +++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/components/CodeEditor/CodeEditorHelper.tsx b/src/components/CodeEditor/CodeEditorHelper.tsx index 299c2134..04326e3c 100644 --- a/src/components/CodeEditor/CodeEditorHelper.tsx +++ b/src/components/CodeEditor/CodeEditorHelper.tsx @@ -38,6 +38,10 @@ export default function CodeEditorHelper({ key: "rowy", description: `rowy provides a set of functions that are commonly used, such as easy file uploads & access to GCP Secret Manager`, }, + { + key: "logging", + description: `logging.log is encouraged to replace console.log`, + }, ]; return ( diff --git a/src/components/fields/Derivative/Settings.tsx b/src/components/fields/Derivative/Settings.tsx index 32ce65dc..7fe36869 100644 --- a/src/components/fields/Derivative/Settings.tsx +++ b/src/components/fields/Derivative/Settings.tsx @@ -65,10 +65,10 @@ export default function Settings({ : config.derivativeFn ? config.derivativeFn : config?.script - ? `const derivative:Derivative = async ({row,ref,db,storage,auth})=>{ + ? `const derivative:Derivative = async ({row,ref,db,storage,auth,logging})=>{ ${config.script.replace(/utilFns.getSecret/g, "rowy.secrets.get")} }` - : `const derivative:Derivative = async ({row,ref,db,storage,auth})=>{ + : `const derivative:Derivative = async ({row,ref,db,storage,auth,logging})=>{ // Write your derivative code here // for example: // const sum = row.a + row.b; diff --git a/src/components/fields/Derivative/derivative.d.ts b/src/components/fields/Derivative/derivative.d.ts index 3af58e11..9d01f97c 100644 --- a/src/components/fields/Derivative/derivative.d.ts +++ b/src/components/fields/Derivative/derivative.d.ts @@ -5,6 +5,11 @@ type DerivativeContext = { db: FirebaseFirestore.Firestore; auth: firebaseauth.BaseAuth; change: any; + logging: { + log: (payload: any) => void; + warn: (payload: any) => void; + error: (payload: any) => void; + }; }; type Derivative = (context: DerivativeContext) => "PLACEHOLDER_OUTPUT_TYPE"; From 5baa6386917360273d2424280dd301ecf45821e8 Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Wed, 23 Nov 2022 21:26:36 +0800 Subject: [PATCH 02/29] enable actions logging in code editor --- src/components/fields/Action/Settings.tsx | 82 ++++++++++--------- src/components/fields/Action/action.d.ts | 3 + src/components/fields/Action/templates.ts | 4 +- .../fields/Derivative/derivative.d.ts | 8 +- src/components/fields/types.ts | 9 ++ 5 files changed, 59 insertions(+), 47 deletions(-) diff --git a/src/components/fields/Action/Settings.tsx b/src/components/fields/Action/Settings.tsx index 9c2562a1..735423ef 100644 --- a/src/components/fields/Action/Settings.tsx +++ b/src/components/fields/Action/Settings.tsx @@ -130,7 +130,7 @@ const Settings = ({ config, onChange, fieldName }: ISettingsProps) => { : config?.runFn ? config.runFn : config?.script - ? `const action:Action = async ({row,ref,db,storage,auth,actionParams,user}) => { + ? `const action:Action = async ({row,ref,db,storage,auth,actionParams,user,logging}) => { ${config.script.replace(/utilFns.getSecret/g, "rowy.secrets.get")} }` : RUN_ACTION_TEMPLATE; @@ -140,7 +140,7 @@ const Settings = ({ config, onChange, fieldName }: ISettingsProps) => { : config.undoFn ? config.undoFn : get(config, "undo.script") - ? `const action : Action = async ({row,ref,db,storage,auth,actionParams,user}) => { + ? `const action : Action = async ({row,ref,db,storage,auth,actionParams,user,logging}) => { ${get(config, "undo.script")} }` : UNDO_ACTION_TEMPLATE; @@ -303,7 +303,9 @@ const Settings = ({ config, onChange, fieldName }: ISettingsProps) => { aria-label="Action will run" name="isActionScript" value={ - config.isActionScript !== false ? "actionScript" : "cloudFunction" + config.isActionScript !== false + ? "actionScript" + : "cloudFunction" } onChange={(e) => onChange("isActionScript")( @@ -559,45 +561,45 @@ const Settings = ({ config, onChange, fieldName }: ISettingsProps) => { title: "Customization", content: ( <> - - + + onChange("customName.enabled")(e.target.checked) + } + name="customName.enabled" + /> + } + label="Customize label for action" + style={{ marginLeft: -11 }} + /> + {config.customName?.enabled && ( + - onChange("customName.enabled")(e.target.checked) + onChange("customName.actionName")(e.target.value) } - name="customName.enabled" - /> - } - label="Customize label for action" - style={{ marginLeft: -11 }} - /> - {config.customName?.enabled && ( - - onChange("customName.actionName")(e.target.value) - } - label="Action name:" - className="labelHorizontal" - inputProps={{ style: { width: "10ch" } }} - > - )} - - onChange("customIcons.enabled")(e.target.checked) - } - name="customIcons.enabled" - /> - } - label="Customize button icons with emoji" - style={{ marginLeft: -11 }} - /> + label="Action name:" + className="labelHorizontal" + inputProps={{ style: { width: "10ch" } }} + > + )} + + onChange("customIcons.enabled")(e.target.checked) + } + name="customIcons.enabled" + /> + } + label="Customize button icons with emoji" + style={{ marginLeft: -11 }} + /> {config.customIcons?.enabled && ( diff --git a/src/components/fields/Action/action.d.ts b/src/components/fields/Action/action.d.ts index a4339978..933e7dbc 100644 --- a/src/components/fields/Action/action.d.ts +++ b/src/components/fields/Action/action.d.ts @@ -1,3 +1,5 @@ +import { RowyLogging } from "@src/components/fields/types"; + type ActionUser = { timestamp: Date; displayName: string; @@ -15,6 +17,7 @@ type ActionContext = { auth: firebaseauth.BaseAuth; actionParams: actionParams; user: ActionUser; + logging: RowyLogging; }; type ActionResult = { diff --git a/src/components/fields/Action/templates.ts b/src/components/fields/Action/templates.ts index 5701ef24..04b3ab23 100644 --- a/src/components/fields/Action/templates.ts +++ b/src/components/fields/Action/templates.ts @@ -1,4 +1,4 @@ -export const RUN_ACTION_TEMPLATE = `const action:Action = async ({row,ref,db,storage,auth,actionParams,user}) => { +export const RUN_ACTION_TEMPLATE = `const action:Action = async ({row,ref,db,storage,auth,actionParams,user,logging}) => { // Write your action code here // for example: // const authToken = await rowy.secrets.get("service") @@ -26,7 +26,7 @@ export const RUN_ACTION_TEMPLATE = `const action:Action = async ({row,ref,db,sto // checkout the documentation for more info: https://docs.rowy.io/field-types/action#script }`; -export const UNDO_ACTION_TEMPLATE = `const action : Action = async ({row,ref,db,storage,auth,actionParams,user}) => { +export const UNDO_ACTION_TEMPLATE = `const action : Action = async ({row,ref,db,storage,auth,actionParams,user,logging}) => { // Write your undo code here // for example: // const authToken = await rowy.secrets.get("service") diff --git a/src/components/fields/Derivative/derivative.d.ts b/src/components/fields/Derivative/derivative.d.ts index 9d01f97c..d210d497 100644 --- a/src/components/fields/Derivative/derivative.d.ts +++ b/src/components/fields/Derivative/derivative.d.ts @@ -1,3 +1,5 @@ +import { RowyLogging } from "@src/components/fields/types"; + type DerivativeContext = { row: Row; ref: FirebaseFirestore.DocumentReference; @@ -5,11 +7,7 @@ type DerivativeContext = { db: FirebaseFirestore.Firestore; auth: firebaseauth.BaseAuth; change: any; - logging: { - log: (payload: any) => void; - warn: (payload: any) => void; - error: (payload: any) => void; - }; + logging: RowyLogging; }; type Derivative = (context: DerivativeContext) => "PLACEHOLDER_OUTPUT_TYPE"; diff --git a/src/components/fields/types.ts b/src/components/fields/types.ts index 66890472..a0ee73d9 100644 --- a/src/components/fields/types.ts +++ b/src/components/fields/types.ts @@ -49,6 +49,7 @@ export interface IBasicCellProps { type: FieldType; name: string; } + export interface IHeavyCellProps extends IBasicCellProps, FormatterProps { @@ -61,6 +62,7 @@ export interface IHeavyCellProps export interface IPopoverInlineCellProps extends IHeavyCellProps { showPopoverCell: React.Dispatch>; } + export interface IPopoverCellProps extends IPopoverInlineCellProps { parentRef: PopoverProps["anchorEl"]; } @@ -110,5 +112,12 @@ export interface IFilterOperator { export interface IFilterCustomInputProps { onChange: (value: any) => void; operator: TableFilter["operator"]; + [key: string]: any; } + +export interface RowyLogging { + log: (payload: any) => void; + warn: (payload: any) => void; + error: (payload: any) => void; +} From 74852a8a3dc008f01cfa94ca23603c9b3ef9d542 Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Wed, 23 Nov 2022 21:32:20 +0800 Subject: [PATCH 03/29] fix action/derivative type hint issue --- src/components/fields/Action/action.d.ts | 8 +++++--- src/components/fields/Derivative/derivative.d.ts | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/components/fields/Action/action.d.ts b/src/components/fields/Action/action.d.ts index 933e7dbc..33fc7489 100644 --- a/src/components/fields/Action/action.d.ts +++ b/src/components/fields/Action/action.d.ts @@ -1,5 +1,3 @@ -import { RowyLogging } from "@src/components/fields/types"; - type ActionUser = { timestamp: Date; displayName: string; @@ -17,7 +15,11 @@ type ActionContext = { auth: firebaseauth.BaseAuth; actionParams: actionParams; user: ActionUser; - logging: RowyLogging; + logging: { + log: (payload: any) => void; + warn: (payload: any) => void; + error: (payload: any) => void; + }; }; type ActionResult = { diff --git a/src/components/fields/Derivative/derivative.d.ts b/src/components/fields/Derivative/derivative.d.ts index d210d497..9d01f97c 100644 --- a/src/components/fields/Derivative/derivative.d.ts +++ b/src/components/fields/Derivative/derivative.d.ts @@ -1,5 +1,3 @@ -import { RowyLogging } from "@src/components/fields/types"; - type DerivativeContext = { row: Row; ref: FirebaseFirestore.DocumentReference; @@ -7,7 +5,11 @@ type DerivativeContext = { db: FirebaseFirestore.Firestore; auth: firebaseauth.BaseAuth; change: any; - logging: RowyLogging; + logging: { + log: (payload: any) => void; + warn: (payload: any) => void; + error: (payload: any) => void; + }; }; type Derivative = (context: DerivativeContext) => "PLACEHOLDER_OUTPUT_TYPE"; From cc2b86027d0cb413c257eb136d2fc40c22158a72 Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Wed, 23 Nov 2022 22:08:35 +0800 Subject: [PATCH 04/29] enable connectors logging in code editor --- src/components/fields/Connector/connector.d.ts | 5 +++++ src/components/fields/Connector/utils.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/fields/Connector/connector.d.ts b/src/components/fields/Connector/connector.d.ts index d031d64b..d12041d4 100644 --- a/src/components/fields/Connector/connector.d.ts +++ b/src/components/fields/Connector/connector.d.ts @@ -15,6 +15,11 @@ type ConnectorContext = { auth: firebaseauth.BaseAuth; query: string; user: ConnectorUser; + logging: { + log: (payload: any) => void; + warn: (payload: any) => void; + error: (payload: any) => void; + }; }; type ConnectorResult = any[]; type Connector = ( diff --git a/src/components/fields/Connector/utils.ts b/src/components/fields/Connector/utils.ts index 17687fe7..88959895 100644 --- a/src/components/fields/Connector/utils.ts +++ b/src/components/fields/Connector/utils.ts @@ -11,7 +11,7 @@ export const replacer = (data: any) => (m: string, key: string) => { return get(data, objKey, defaultValue); }; -export const baseFunction = `const connectorFn: Connector = async ({query, row, user}) => { +export const baseFunction = `const connectorFn: Connector = async ({query, row, user, logging}) => { // TODO: Implement your service function here return []; };`; From 35a173fba0e29926a9cd247d032f3d6cf105135d Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Thu, 24 Nov 2022 06:25:23 +0800 Subject: [PATCH 05/29] enable webhooks logging in code editor --- .../WebhooksModal/Schemas/basic.tsx | 89 ++++++++++++------- .../WebhooksModal/Schemas/sendgrid.tsx | 4 +- .../WebhooksModal/Schemas/stripe.tsx | 4 +- .../WebhooksModal/Schemas/typeform.tsx | 4 +- .../WebhooksModal/Schemas/webform.tsx | 4 +- .../TableModals/WebhooksModal/utils.tsx | 40 +++++++-- .../TableModals/WebhooksModal/webhooks.d.ts | 10 +++ 7 files changed, 106 insertions(+), 49 deletions(-) diff --git a/src/components/TableModals/WebhooksModal/Schemas/basic.tsx b/src/components/TableModals/WebhooksModal/Schemas/basic.tsx index 7f9e4d11..54075d43 100644 --- a/src/components/TableModals/WebhooksModal/Schemas/basic.tsx +++ b/src/components/TableModals/WebhooksModal/Schemas/basic.tsx @@ -21,17 +21,41 @@ const requestType = [ export const parserExtraLibs = [ requestType, - `type Parser = (args:{req:WebHookRequest,db: FirebaseFirestore.Firestore,ref: FirebaseFirestore.CollectionReference,res:{ - send:(v:any)=>void - sendStatus:(status:number)=>void - }}) => Promise;`, + `type Parser = ( + args: { + req: WebHookRequest; + db: FirebaseFirestore.Firestore; + ref: FirebaseFirestore.CollectionReference; + res: { + send: (v:any)=>void; + sendStatus: (status:number)=>void + }; + logging: { + log: (payload: any) => void; + warn: (payload: any) => void; + error: (payload: any) => void; + }; + } + ) => Promise;`, ]; export const conditionExtraLibs = [ requestType, - `type Condition = (args:{req:WebHookRequest,db: FirebaseFirestore.Firestore,ref: FirebaseFirestore.CollectionReference,res:{ - send:(v:any)=>void - sendStatus:(status:number)=>void - }}) => Promise;`, + `type Condition = ( + args: { + req: WebHookRequest; + db: FirebaseFirestore.Firestore; + ref: FirebaseFirestore.CollectionReference; + res: { + send: (v:any)=>void; + sendStatus: (status:number)=>void; + }; + logging: { + log: (payload: any) => void; + warn: (payload: any) => void; + error: (payload: any) => void; + }; + } + ) => Promise;`, ]; const additionalVariables = [ @@ -48,30 +72,29 @@ export const webhookBasic = { extraLibs: parserExtraLibs, template: ( table: TableSettings - ) => `const basicParser: Parser = async({req, db,ref}) => { - // request is the request object from the webhook - // db is the database object - // ref is the reference to collection of the table - // the returned object will be added as a new row to the table - // eg: adding the webhook body as row - const {body} = req; - ${ - table.audit !== false - ? ` - // auditField - const ${ - table.auditFieldCreatedBy ?? "_createdBy" - } = await rowy.metadata.serviceAccountUser() - return { - ...body, - ${table.auditFieldCreatedBy ?? "_createdBy"} - } - ` - : ` - return body; - ` - } - + ) => `const basicParser: Parser = async({req, db, ref, logging}) => { + // request is the request object from the webhook + // db is the database object + // ref is the reference to collection of the table + // the returned object will be added as a new row to the table + // eg: adding the webhook body as row + const {body} = req; + ${ + table.audit !== false + ? ` + // auditField + const ${ + table.auditFieldCreatedBy ?? "_createdBy" + } = await rowy.metadata.serviceAccountUser() + return { + ...body, + ${table.auditFieldCreatedBy ?? "_createdBy"} + } + ` + : ` + return body; + ` + } }`, }, condition: { @@ -79,7 +102,7 @@ export const webhookBasic = { extraLibs: conditionExtraLibs, template: ( table: TableSettings - ) => `const condition: Condition = async({ref,req,db}) => { + ) => `const condition: Condition = async({ref, req, db, logging}) => { // feel free to add your own code logic here return true; }`, diff --git a/src/components/TableModals/WebhooksModal/Schemas/sendgrid.tsx b/src/components/TableModals/WebhooksModal/Schemas/sendgrid.tsx index b557dd78..b251697f 100644 --- a/src/components/TableModals/WebhooksModal/Schemas/sendgrid.tsx +++ b/src/components/TableModals/WebhooksModal/Schemas/sendgrid.tsx @@ -13,7 +13,7 @@ export const webhookSendgrid = { extraLibs: null, template: ( table: TableSettings - ) => `const sendgridParser: Parser = async ({ req, db, ref }) => { + ) => `const sendgridParser: Parser = async ({ req, db, ref, logging }) => { const { body } = req const eventHandler = async (sgEvent) => { // Event handlers can be modiefed to preform different actions based on the sendgrid event @@ -35,7 +35,7 @@ export const webhookSendgrid = { extraLibs: null, template: ( table: TableSettings - ) => `const condition: Condition = async({ref,req,db}) => { + ) => `const condition: Condition = async({ref, req, db, logging}) => { // feel free to add your own code logic here return true; }`, diff --git a/src/components/TableModals/WebhooksModal/Schemas/stripe.tsx b/src/components/TableModals/WebhooksModal/Schemas/stripe.tsx index 31eb4aff..acfb2dfe 100644 --- a/src/components/TableModals/WebhooksModal/Schemas/stripe.tsx +++ b/src/components/TableModals/WebhooksModal/Schemas/stripe.tsx @@ -17,7 +17,7 @@ export const webhookStripe = { extraLibs: null, template: ( table: TableSettings - ) => `const sendgridParser: Parser = async ({ req, db, ref }) => { + ) => `const sendgridParser: Parser = async ({ req, db, ref, logging }) => { const event = req.body switch (event.type) { case "payment_intent.succeeded": @@ -34,7 +34,7 @@ export const webhookStripe = { extraLibs: null, template: ( table: TableSettings - ) => `const condition: Condition = async({ref,req,db}) => { + ) => `const condition: Condition = async({ref, req, db, logging}) => { // feel free to add your own code logic here return true; }`, diff --git a/src/components/TableModals/WebhooksModal/Schemas/typeform.tsx b/src/components/TableModals/WebhooksModal/Schemas/typeform.tsx index 5fd4ce7d..9c509efd 100644 --- a/src/components/TableModals/WebhooksModal/Schemas/typeform.tsx +++ b/src/components/TableModals/WebhooksModal/Schemas/typeform.tsx @@ -13,7 +13,7 @@ export const webhookTypeform = { extraLibs: null, template: ( table: TableSettings - ) => `const typeformParser: Parser = async({req, db,ref}) =>{ + ) => `const typeformParser: Parser = async({req, db, ref, logging}) =>{ // this reduces the form submission into a single object of key value pairs // eg: {name: "John", age: 20} // ⚠️ ensure that you have assigned ref values of the fields @@ -73,7 +73,7 @@ export const webhookTypeform = { extraLibs: null, template: ( table: TableSettings - ) => `const condition: Condition = async({ref,req,db}) => { + ) => `const condition: Condition = async({ref, req, db, logging}) => { // feel free to add your own code logic here return true; }`, diff --git a/src/components/TableModals/WebhooksModal/Schemas/webform.tsx b/src/components/TableModals/WebhooksModal/Schemas/webform.tsx index bf5e8cda..7bed061d 100644 --- a/src/components/TableModals/WebhooksModal/Schemas/webform.tsx +++ b/src/components/TableModals/WebhooksModal/Schemas/webform.tsx @@ -14,7 +14,7 @@ export const webhook = { extraLibs: null, template: ( table: TableSettings - ) => `const formParser: Parser = async({req, db,ref}) => { + ) => `const formParser: Parser = async({req, db, ref, logging}) => { // request is the request object from the webhook // db is the database object // ref is the reference to collection of the table @@ -45,7 +45,7 @@ export const webhook = { extraLibs: null, template: ( table: TableSettings - ) => `const condition: Condition = async({ref,req,db}) => { + ) => `const condition: Condition = async({ref, req, db, logging}) => { // feel free to add your own code logic here return true; }`, diff --git a/src/components/TableModals/WebhooksModal/utils.tsx b/src/components/TableModals/WebhooksModal/utils.tsx index 7c338bab..b1ba8601 100644 --- a/src/components/TableModals/WebhooksModal/utils.tsx +++ b/src/components/TableModals/WebhooksModal/utils.tsx @@ -26,17 +26,41 @@ const requestType = [ export const parserExtraLibs = [ requestType, - `type Parser = (args:{req:WebHookRequest,db: FirebaseFirestore.Firestore,ref: FirebaseFirestore.CollectionReference,res:{ - send:(v:any)=>void - sendStatus:(status:number)=>void - }}) => Promise;`, + `type Parser = ( + args: { + req: WebHookRequest; + db: FirebaseFirestore.Firestore; + ref: FirebaseFirestore.CollectionReference; + res: { + send: (v:any)=>void; + sendStatus: (status:number)=>void + }; + logging: { + log: (payload: any) => void; + warn: (payload: any) => void; + error: (payload: any) => void; + }; + } + ) => Promise;`, ]; export const conditionExtraLibs = [ requestType, - `type Condition = (args:{req:WebHookRequest,db: FirebaseFirestore.Firestore,ref: FirebaseFirestore.CollectionReference,res:{ - send:(v:any)=>void - sendStatus:(status:number)=>void - }}) => Promise;`, + `type Condition = ( + args: { + req:WebHookRequest, + db: FirebaseFirestore.Firestore, + ref: FirebaseFirestore.CollectionReference, + res: { + send: (v:any)=>void + sendStatus: (status:number)=>void + }; + logging: { + log: (payload: any) => void; + warn: (payload: any) => void; + error: (payload: any) => void; + }; + } + ) => Promise;`, ]; const additionalVariables = [ diff --git a/src/components/TableModals/WebhooksModal/webhooks.d.ts b/src/components/TableModals/WebhooksModal/webhooks.d.ts index 4a8715f8..fa98341f 100644 --- a/src/components/TableModals/WebhooksModal/webhooks.d.ts +++ b/src/components/TableModals/WebhooksModal/webhooks.d.ts @@ -3,10 +3,20 @@ type Condition = (args: { db: FirebaseFirestore.Firestore; ref: FirebaseFirestore.CollectionReference; res: Response; + logging: { + log: (payload: any) => void; + warn: (payload: any) => void; + error: (payload: any) => void; + }; }) => Promise; type Parser = (args: { req: WebHookRequest; db: FirebaseFirestore.Firestore; ref: FirebaseFirestore.CollectionReference; + logging: { + log: (payload: any) => void; + warn: (payload: any) => void; + error: (payload: any) => void; + }; }) => Promise; From 3a57d6e28a914b4579f7a73f77b7c10bd9a0a6f8 Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Fri, 25 Nov 2022 15:19:51 +0000 Subject: [PATCH 06/29] added reordering feature to the hidden fields menu --- src/components/TableToolbar/HiddenFields.tsx | 183 ++++++++++++++----- 1 file changed, 138 insertions(+), 45 deletions(-) diff --git a/src/components/TableToolbar/HiddenFields.tsx b/src/components/TableToolbar/HiddenFields.tsx index 8015f6f5..a761c4de 100644 --- a/src/components/TableToolbar/HiddenFields.tsx +++ b/src/components/TableToolbar/HiddenFields.tsx @@ -1,12 +1,26 @@ -import { useEffect, useRef, useMemo, useState } from "react"; -import { useAtom } from "jotai"; +import { + useEffect, + useRef, + useMemo, + useState, + forwardRef, + ChangeEvent, +} from "react"; +import { useAtom, useSetAtom } from "jotai"; import { isEqual } from "lodash-es"; import { colord } from "colord"; +import { + DragDropContext, + Droppable, + Draggable, + DropResult, +} from "react-beautiful-dnd"; import { Box, AutocompleteProps, Theme } from "@mui/material"; import VisibilityIcon from "@mui/icons-material/VisibilityOutlined"; import VisibilityOffIcon from "@mui/icons-material/VisibilityOffOutlined"; import IconSlash from "@src/components/IconSlash"; +import DragIndicatorOutlinedIcon from "@mui/icons-material/DragIndicatorOutlined"; import ColumnSelect, { ColumnItem } from "@src/components/Table/ColumnSelect"; import ButtonWithStatus from "@src/components/ButtonWithStatus"; @@ -16,10 +30,14 @@ import { userSettingsAtom, updateUserSettingsAtom, } from "@src/atoms/projectScope"; -import { tableScope, tableIdAtom } from "@src/atoms/tableScope"; +import { + tableScope, + tableIdAtom, + updateColumnAtom, +} from "@src/atoms/tableScope"; import { formatSubTableName } from "@src/utils/table"; -export default function HiddenFields() { +export default function HiddenFields2() { const buttonRef = useRef(null); const [userSettings] = useAtom(userSettingsAtom, projectScope); @@ -57,6 +75,9 @@ export default function HiddenFields() { } setOpen(false); }; + + // disable drag if search box is not empty + const [disableDrag, setDisableDrag] = useState(false); const renderOption: AutocompleteProps< any, true, @@ -67,49 +88,112 @@ export default function HiddenFields() { colord(theme.palette.background.paper) .mix("#fff", theme.palette.mode === "dark" ? 0.16 : 0) .alpha(1); - return ( -
  • - - - - slashColor(theme).toHslString(), - }, - ".Mui-focused & .icon-slash-mask": { - stroke: (theme) => - slashColor(theme) - .mix( - theme.palette.primary.main, - theme.palette.action.selectedOpacity + - theme.palette.action.hoverOpacity - ) - .alpha(1) - .toHslString(), - }, - }, - selected ? { strokeDashoffset: 0 } : {}, - ]} - /> - - -
  • + + {(provided) => ( +
  • + + + + + + + slashColor(theme).toHslString(), + }, + ".Mui-focused & .icon-slash-mask": { + stroke: (theme) => + slashColor(theme) + .mix( + theme.palette.primary.main, + theme.palette.action.selectedOpacity + + theme.palette.action.hoverOpacity + ) + .alpha(1) + .toHslString(), + }, + }, + selected ? { strokeDashoffset: 0 } : {}, + ]} + /> + + +
  • + )} +
    ); }; + const updateColumn = useSetAtom(updateColumnAtom, tableScope); + + // updates column on drag end + function handleOnDragEnd(result: DropResult) { + if (!result.destination) return; + updateColumn({ + key: result.draggableId, + config: {}, + index: result.destination.index, + }); + } + + function checkToDisableDrag(e: ChangeEvent) { + setDisableDrag(e.target.value !== ""); + } + + const ListboxComponent = forwardRef(function ListboxComponent( + props: React.HTMLAttributes, + ulRef: any /*React.ForwardedRef*/ + ) { + const { children, ...other } = props; + + return ( + + + {(provided) => ( +
      { + provided.innerRef(ref); + if (ulRef !== null) { + ulRef(ref); + } + }} + > + {props.children} + {provided.placeholder} +
    + )} +
    +
    + ); + }); + return ( <> { + setHiddenFields(updates); + setDisableDrag(false); + }} onClose={handleSave} clearText="Show all" selectAllText="Hide all" From 9c312bb53b9ab6c94c569e338c9caa5e0ee7bfdf Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Fri, 25 Nov 2022 15:21:38 +0000 Subject: [PATCH 07/29] added comment --- src/components/TableToolbar/HiddenFields.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/TableToolbar/HiddenFields.tsx b/src/components/TableToolbar/HiddenFields.tsx index a761c4de..e147accb 100644 --- a/src/components/TableToolbar/HiddenFields.tsx +++ b/src/components/TableToolbar/HiddenFields.tsx @@ -161,6 +161,7 @@ export default function HiddenFields2() { }); } + // checks whether to disable reordering when search filter is applied function checkToDisableDrag(e: ChangeEvent) { setDisableDrag(e.target.value !== ""); } From 9bfe324362b9a8173c684984b313f610a357f8ae Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Fri, 25 Nov 2022 16:44:45 +0000 Subject: [PATCH 08/29] HiddenFields2 to HiddenFields --- src/components/TableToolbar/HiddenFields.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/TableToolbar/HiddenFields.tsx b/src/components/TableToolbar/HiddenFields.tsx index e147accb..8a33d3ed 100644 --- a/src/components/TableToolbar/HiddenFields.tsx +++ b/src/components/TableToolbar/HiddenFields.tsx @@ -37,7 +37,7 @@ import { } from "@src/atoms/tableScope"; import { formatSubTableName } from "@src/utils/table"; -export default function HiddenFields2() { +export default function HiddenFields() { const buttonRef = useRef(null); const [userSettings] = useAtom(userSettingsAtom, projectScope); From 2c206d2e721da9a603fdfb9171b2bf2933f10d84 Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Sun, 27 Nov 2022 20:45:12 +0800 Subject: [PATCH 09/29] enable logging in extensions code editor --- src/components/CodeEditor/extensions.d.ts | 5 ++++ .../TableModals/ExtensionsModal/utils.ts | 25 ++++++++++--------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/components/CodeEditor/extensions.d.ts b/src/components/CodeEditor/extensions.d.ts index dd592d05..3da39ae9 100644 --- a/src/components/CodeEditor/extensions.d.ts +++ b/src/components/CodeEditor/extensions.d.ts @@ -26,6 +26,11 @@ type ExtensionContext = { extensionBody: any; }; RULES_UTILS: any; + logging: { + log: (payload: any) => void; + warn: (payload: any) => void; + error: (payload: any) => void; + }; }; // extension body definition diff --git a/src/components/TableModals/ExtensionsModal/utils.ts b/src/components/TableModals/ExtensionsModal/utils.ts index 28d24fda..48de1a92 100644 --- a/src/components/TableModals/ExtensionsModal/utils.ts +++ b/src/components/TableModals/ExtensionsModal/utils.ts @@ -61,7 +61,7 @@ export interface IRuntimeOptions { export const triggerTypes: ExtensionTrigger[] = ["create", "update", "delete"]; const extensionBodyTemplate = { - task: `const extensionBody: TaskBody = async({row, db, change, ref}) => { + task: `const extensionBody: TaskBody = async({row, db, change, ref, logging}) => { // task extensions are very flexible you can do anything from updating other documents in your database, to making an api request to 3rd party service. // example: @@ -87,7 +87,7 @@ const extensionBodyTemplate = { }) */ }`, - docSync: `const extensionBody: DocSyncBody = async({row, db, change, ref}) => { + docSync: `const extensionBody: DocSyncBody = async({row, db, change, ref, logging}) => { // feel free to add your own code logic here return ({ @@ -96,7 +96,7 @@ const extensionBodyTemplate = { targetPath: "", // fill in the path here }) }`, - historySnapshot: `const extensionBody: HistorySnapshotBody = async({row, db, change, ref}) => { + historySnapshot: `const extensionBody: HistorySnapshotBody = async({row, db, change, ref, logging}) => { // feel free to add your own code logic here return ({ @@ -104,7 +104,7 @@ const extensionBodyTemplate = { collectionId: "historySnapshots", // optionally change the sub-collection id of where the history snapshots are stored }) }`, - algoliaIndex: `const extensionBody: AlgoliaIndexBody = async({row, db, change, ref}) => { + algoliaIndex: `const extensionBody: AlgoliaIndexBody = async({row, db, change, ref, logging}) => { // feel free to add your own code logic here return ({ @@ -114,7 +114,7 @@ const extensionBodyTemplate = { objectID: ref.id, // algolia object ID, ref.id is one possible choice }) }`, - meiliIndex: `const extensionBody: MeiliIndexBody = async({row, db, change, ref}) => { + meiliIndex: `const extensionBody: MeiliIndexBody = async({row, db, change, ref, logging}) => { // feel free to add your own code logic here return({ @@ -124,7 +124,7 @@ const extensionBodyTemplate = { objectID: ref.id, // algolia object ID, ref.id is one possible choice }) }`, - bigqueryIndex: `const extensionBody: BigqueryIndexBody = async({row, db, change, ref}) => { + bigqueryIndex: `const extensionBody: BigqueryIndexBody = async({row, db, change, ref, logging}) => { // feel free to add your own code logic here return ({ @@ -134,7 +134,7 @@ const extensionBodyTemplate = { objectID: ref.id, // algolia object ID, ref.id is one possible choice }) }`, - slackMessage: `const extensionBody: SlackMessageBody = async({row, db, change, ref}) => { + slackMessage: `const extensionBody: SlackMessageBody = async({row, db, change, ref, logging}) => { // feel free to add your own code logic here return ({ @@ -144,7 +144,7 @@ const extensionBodyTemplate = { attachments: [], // the attachments parameter to pass in to slack api }) }`, - sendgridEmail: `const extensionBody: SendgridEmailBody = async({row, db, change, ref}) => { + sendgridEmail: `const extensionBody: SendgridEmailBody = async({row, db, change, ref, logging}) => { // feel free to add your own code logic here return ({ @@ -164,7 +164,7 @@ const extensionBodyTemplate = { }, }) }`, - apiCall: `const extensionBody: ApiCallBody = async({row, db, change, ref}) => { + apiCall: `const extensionBody: ApiCallBody = async({row, db, change, ref, logging}) => { // feel free to add your own code logic here return ({ @@ -174,7 +174,7 @@ const extensionBodyTemplate = { callback: ()=>{}, }) }`, - twilioMessage: `const extensionBody: TwilioMessageBody = async({row, db, change, ref}) => { + twilioMessage: `const extensionBody: TwilioMessageBody = async({row, db, change, ref, logging}) => { /** * * Setup twilio secret key: https://docs.rowy.io/extensions/twilio-message#secret-manager-setup @@ -190,7 +190,7 @@ const extensionBodyTemplate = { body: "Hi there!" // message text }) }`, - pushNotification: `const extensionBody: PushNotificationBody = async({row, db, change, ref}) => { + pushNotification: `const extensionBody: PushNotificationBody = async({row, db, change, ref, logging}) => { // you can FCM token from the row or from the user document in the database // const FCMtoken = row.FCMtoken // or push through topic @@ -238,13 +238,14 @@ export function emptyExtensionObject( extensionBody: extensionBodyTemplate[type] ?? extensionBodyTemplate["task"], requiredFields: [], trackedFields: [], - conditions: `const condition: Condition = async({row, change}) => { + conditions: `const condition: Condition = async({row, change, logging}) => { // feel free to add your own code logic here return true; }`, lastEditor: user, }; } + export function sparkToExtensionObjects( sparkConfig: string, user: IExtensionEditor From 2fc323ecd5958796cb263c2a877088c80c6cf093 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Mon, 28 Nov 2022 16:29:30 +1100 Subject: [PATCH 10/29] Update src/components/TableToolbar/HiddenFields.tsx --- src/components/TableToolbar/HiddenFields.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/TableToolbar/HiddenFields.tsx b/src/components/TableToolbar/HiddenFields.tsx index 8a33d3ed..0a537713 100644 --- a/src/components/TableToolbar/HiddenFields.tsx +++ b/src/components/TableToolbar/HiddenFields.tsx @@ -102,7 +102,13 @@ export default function HiddenFields() { > + disableDrag ? theme.palette.action.disabledOpacity : 1, + }, + ]} /> From 065484aeb21384bb6e0f15fa02a5240988d23f26 Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Fri, 9 Dec 2022 03:45:58 +0800 Subject: [PATCH 11/29] add rowy loggin tab --- src/atoms/tableScope/ui.ts | 2 +- .../ColumnModals/ColumnConfigModal/DefaultValueInput.tsx | 4 ++-- .../ColumnModals/ColumnConfigModal/defaultValue.d.ts | 5 +++++ src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx | 2 ++ src/components/TableModals/CloudLogsModal/utils.ts | 3 +++ 5 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/atoms/tableScope/ui.ts b/src/atoms/tableScope/ui.ts index 3d11a081..47bdd828 100644 --- a/src/atoms/tableScope/ui.ts +++ b/src/atoms/tableScope/ui.ts @@ -142,7 +142,7 @@ export const selectedCellAtom = atom(null); export const contextMenuTargetAtom = atom(null); export type CloudLogFilters = { - type: "webhook" | "functions" | "audit" | "build"; + type: "rowy" | "webhook" | "functions" | "audit" | "build"; timeRange: | { type: "seconds" | "minutes" | "hours" | "days"; value: number } | { type: "range"; start: Date; end: Date }; diff --git a/src/components/ColumnModals/ColumnConfigModal/DefaultValueInput.tsx b/src/components/ColumnModals/ColumnConfigModal/DefaultValueInput.tsx index 5213eb8a..3cc78e2a 100644 --- a/src/components/ColumnModals/ColumnConfigModal/DefaultValueInput.tsx +++ b/src/components/ColumnModals/ColumnConfigModal/DefaultValueInput.tsx @@ -52,11 +52,11 @@ function CodeEditor({ type, column, handleChange }: ICodeEditorProps) { } else if (column.config?.defaultValue?.dynamicValueFn) { dynamicValueFn = column.config?.defaultValue?.dynamicValueFn; } else if (column.config?.defaultValue?.script) { - dynamicValueFn = `const dynamicValueFn : DefaultValue = async ({row,ref,db,storage,auth})=>{ + dynamicValueFn = `const dynamicValueFn : DefaultValue = async ({row,ref,db,storage,auth,logging})=>{ ${column.config?.defaultValue.script} }`; } else { - dynamicValueFn = `const dynamicValueFn : DefaultValue = async ({row,ref,db,storage,auth})=>{ + dynamicValueFn = `const dynamicValueFn : DefaultValue = async ({row,ref,db,storage,auth,logging})=>{ // Write your default value code here // for example: // generate random hex color diff --git a/src/components/ColumnModals/ColumnConfigModal/defaultValue.d.ts b/src/components/ColumnModals/ColumnConfigModal/defaultValue.d.ts index 0e50bb2d..e3b90e13 100644 --- a/src/components/ColumnModals/ColumnConfigModal/defaultValue.d.ts +++ b/src/components/ColumnModals/ColumnConfigModal/defaultValue.d.ts @@ -4,5 +4,10 @@ type DefaultValueContext = { storage: firebasestorage.Storage; db: FirebaseFirestore.Firestore; auth: firebaseauth.BaseAuth; + logging: { + log: (payload: any) => void; + warn: (payload: any) => void; + error: (payload: any) => void; + }; }; type DefaultValue = (context: DefaultValueContext) => "PLACEHOLDER_OUTPUT_TYPE"; diff --git a/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx b/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx index e69acc44..217b5929 100644 --- a/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx +++ b/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx @@ -118,6 +118,7 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) { } aria-label="Filter by log type" > + Rowy Logging Webhooks Functions Audit @@ -139,6 +140,7 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) { )} + {cloudLogFilters.type === "rowy" && <>} {cloudLogFilters.type === "webhook" && ( Date: Sat, 10 Dec 2022 17:19:53 +0800 Subject: [PATCH 12/29] rowy logging ui implementation --- src/atoms/tableScope/ui.ts | 14 +- src/components/ColumnMenu/ColumnMenu.tsx | 18 + .../CloudLogsModal/CloudLogItem.tsx | 40 +- .../CloudLogsModal/CloudLogList.tsx | 7 + .../CloudLogsModal/CloudLogSeverityIcon.tsx | 6 + .../CloudLogsModal/CloudLogsModal.tsx | 477 +++++++++++++----- .../TableModals/CloudLogsModal/utils.ts | 58 ++- .../ExtensionsModal/ExtensionList.tsx | 26 + .../TableModals/WebhooksModal/WebhookList.tsx | 3 +- 9 files changed, 503 insertions(+), 146 deletions(-) diff --git a/src/atoms/tableScope/ui.ts b/src/atoms/tableScope/ui.ts index 47bdd828..44afee10 100644 --- a/src/atoms/tableScope/ui.ts +++ b/src/atoms/tableScope/ui.ts @@ -142,14 +142,26 @@ export const selectedCellAtom = atom(null); export const contextMenuTargetAtom = atom(null); export type CloudLogFilters = { - type: "rowy" | "webhook" | "functions" | "audit" | "build"; + type: "rowy" | "audit" | "build"; timeRange: | { type: "seconds" | "minutes" | "hours" | "days"; value: number } | { type: "range"; start: Date; end: Date }; severity?: Array; webhook?: string[]; + extension?: string[]; + column?: string[]; auditRowId?: string; buildLogExpanded?: number; + functionType?: ( + | "connector" + | "derivative-script" + | "action" + | "derivative-function" + | "extension" + | "defaultValue" + | "hooks" + )[]; + loggingSource?: ("backend-scripts" | "backend-function" | "hooks")[]; }; /** Store cloud log modal filters in URL */ export const cloudLogFiltersAtom = atomWithHash( diff --git a/src/components/ColumnMenu/ColumnMenu.tsx b/src/components/ColumnMenu/ColumnMenu.tsx index cf025217..a5f76a67 100644 --- a/src/components/ColumnMenu/ColumnMenu.tsx +++ b/src/components/ColumnMenu/ColumnMenu.tsx @@ -20,6 +20,7 @@ import { ColumnPlusBefore as ColumnPlusBeforeIcon, ColumnPlusAfter as ColumnPlusAfterIcon, ColumnRemove as ColumnRemoveIcon, + CloudLogs as LogsIcon, } from "@src/assets/icons"; import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward"; import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward"; @@ -51,6 +52,8 @@ import { tableFiltersPopoverAtom, tableNextPageAtom, tableSchemaAtom, + cloudLogFiltersAtom, + tableModalAtom, } from "@src/atoms/tableScope"; import { FieldType } from "@src/constants/fields"; import { getFieldProp } from "@src/components/fields"; @@ -107,6 +110,8 @@ export default function ColumnMenu({ ); const [tableNextPage] = useAtom(tableNextPageAtom, tableScope); const [tableSchema] = useAtom(tableSchemaAtom, tableScope); + const setModal = useSetAtom(tableModalAtom, tableScope); + const setCloudLogFilters = useSetAtom(cloudLogFiltersAtom, tableScope); const snackLogContext = useSnackLogContext(); const [altPress] = useAtom(altPressAtom, projectScope); @@ -383,6 +388,19 @@ export default function ColumnMenu({ confirm: "Evaluate", }), }, + { + key: "logs", + label: altPress ? "Logs" : "Logs…", + icon: , + onClick: () => { + setModal("cloudLogs"); + setCloudLogFilters({ + type: "rowy", + timeRange: { type: "days", value: 7 }, + column: [column.key], + }); + }, + }, ]; const columnActions: IMenuContentsProps["menuItems"] = [ diff --git a/src/components/TableModals/CloudLogsModal/CloudLogItem.tsx b/src/components/TableModals/CloudLogsModal/CloudLogItem.tsx index f82a75f1..9e0887f3 100644 --- a/src/components/TableModals/CloudLogsModal/CloudLogItem.tsx +++ b/src/components/TableModals/CloudLogsModal/CloudLogItem.tsx @@ -187,22 +187,32 @@ export default function CloudLogItem({ )} - {data.payload === "textPayload" && data.textPayload} - {get(data, "httpRequest.requestUrl")?.split(".run.app").pop()} - {data.payload === "jsonPayload" && ( - - {data.jsonPayload.error}{" "} - + {data.logName.endsWith("rowy-logging") && data.jsonPayload.payload ? ( + <> + {typeof data.jsonPayload.payload === "string" + ? data.jsonPayload.payload + : JSON.stringify(data.jsonPayload.payload)} + + ) : ( + <> + {data.payload === "textPayload" && data.textPayload} + {get(data, "httpRequest.requestUrl")?.split(".run.app").pop()} + {data.payload === "jsonPayload" && ( + + {data.jsonPayload.error}{" "} + + )} + {data.payload === "jsonPayload" && + stringify(data.jsonPayload.body ?? data.jsonPayload, { + space: 2, + })} + )} - {data.payload === "jsonPayload" && - stringify(data.jsonPayload.body ?? data.jsonPayload, { - space: 2, - })} diff --git a/src/components/TableModals/CloudLogsModal/CloudLogList.tsx b/src/components/TableModals/CloudLogsModal/CloudLogList.tsx index 5e5382ee..93093d38 100644 --- a/src/components/TableModals/CloudLogsModal/CloudLogList.tsx +++ b/src/components/TableModals/CloudLogsModal/CloudLogList.tsx @@ -70,6 +70,13 @@ export default function CloudLogList({ items, ...props }: ICloudLogListProps) { "jsonPayload.rowyUser.displayName", // Webhook event "jsonPayload.params.endpoint", + // Rowy Logging + "jsonPayload.functionType", + "jsonPayload.loggingSource", + "jsonPayload.extensionName", + "jsonPayload.extensionType", + "jsonPayload.webhookName", + "jsonPayload.fieldName", ]} /> diff --git a/src/components/TableModals/CloudLogsModal/CloudLogSeverityIcon.tsx b/src/components/TableModals/CloudLogsModal/CloudLogSeverityIcon.tsx index 0cf0dc6e..f8413f65 100644 --- a/src/components/TableModals/CloudLogsModal/CloudLogSeverityIcon.tsx +++ b/src/components/TableModals/CloudLogsModal/CloudLogSeverityIcon.tsx @@ -22,6 +22,12 @@ export const SEVERITY_LEVELS = { EMERGENCY: "One or more systems are unusable.", }; +export const SEVERITY_LEVELS_ROWY = { + DEFAULT: "The log entry has no assigned severity level.", + WARNING: "Warning events might cause problems.", + ERROR: "Error events are likely to cause problems.", +}; + export interface ICloudLogSeverityIconProps extends SvgIconProps { severity: keyof typeof SEVERITY_LEVELS; } diff --git a/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx b/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx index 217b5929..95af37f1 100644 --- a/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx +++ b/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx @@ -1,6 +1,6 @@ import useSWR from "swr"; import { useAtom } from "jotai"; -import { startCase } from "lodash-es"; +import { startCase, upperCase } from "lodash-es"; import { ITableModalProps } from "@src/components/TableModals"; import { @@ -12,6 +12,7 @@ import { TextField, InputAdornment, Button, + Box, } from "@mui/material"; import RefreshIcon from "@mui/icons-material/Refresh"; import { CloudLogs as LogsIcon } from "@src/assets/icons"; @@ -23,7 +24,10 @@ import TimeRangeSelect from "./TimeRangeSelect"; import CloudLogList from "./CloudLogList"; import BuildLogs from "./BuildLogs"; import EmptyState from "@src/components/EmptyState"; -import CloudLogSeverityIcon, { SEVERITY_LEVELS } from "./CloudLogSeverityIcon"; +import CloudLogSeverityIcon, { + SEVERITY_LEVELS, + SEVERITY_LEVELS_ROWY, +} from "./CloudLogSeverityIcon"; import { projectScope, @@ -119,8 +123,6 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) { aria-label="Filter by log type" > Rowy Logging - Webhooks - Functions Audit Build @@ -141,36 +143,6 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) { )} {cloudLogFilters.type === "rowy" && <>} - {cloudLogFilters.type === "webhook" && ( - ({ - label: x.name, - value: x.endpoint, - })) - : [] - } - value={cloudLogFilters.webhook ?? []} - onChange={(v) => - setCloudLogFilters((prev) => ({ ...prev, webhook: v })) - } - TextFieldProps={{ - id: "webhook", - className: "labelHorizontal", - sx: { "& .MuiInputBase-root": { width: 180 } }, - fullWidth: false, - }} - itemRenderer={(option) => ( - <> - {option.label} {option.value} - - )} - /> - )} {cloudLogFilters.type === "audit" && ( )} - {/* Spacer */}
    {cloudLogFilters.type !== "build" && ( @@ -220,10 +191,311 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) { )} + {cloudLogFilters.type !== "rowy" && ( + + setCloudLogFilters((prev) => ({ ...prev, severity })) + } + TextFieldProps={{ + style: { width: 130 }, + placeholder: "Severity", + SelectProps: { + renderValue: () => { + if ( + !Array.isArray(cloudLogFilters.severity) || + cloudLogFilters.severity.length === 0 + ) + return `Severity`; + + if (cloudLogFilters.severity.length === 1) + return ( + <> + Severity{" "} + + + ); + + return `Severity (${cloudLogFilters.severity.length})`; + }, + }, + }} + itemRenderer={(option) => ( + <> + + {startCase(option.value.toLowerCase())} + + )} + /> + )} + + setCloudLogFilters((c) => ({ ...c, timeRange: value })) + } + /> + mutate()} + title="Refresh" + icon={} + disabled={isValidating} + /> + + )} + + + {isValidating && ( + + )} + + } + > + {cloudLogFilters.type === "build" ? ( + + ) : ( + + + {cloudLogFilters.type === "rowy" ? ( + + + { + setCloudLogFilters((prev) => ({ + ...prev, + functionType: v, + })); + }} + TextFieldProps={{ + id: "functionType", + className: "labelHorizontal", + sx: { "& .MuiInputBase-root": { width: 200 } }, + fullWidth: false, + SelectProps: { + renderValue: () => { + if (cloudLogFilters?.functionType?.length === 1) { + return `Type (${cloudLogFilters.functionType[0]})`; + } else if (cloudLogFilters?.functionType?.length) { + return `Type (${cloudLogFilters.functionType.length})`; + } else { + return `Type`; + } + }, + }, + }} + itemRenderer={(option) => <>{upperCase(option.value)}} + /> + { + setCloudLogFilters((prev) => ({ + ...prev, + loggingSource: v, + })); + }} + TextFieldProps={{ + id: "loggingSource", + className: "labelHorizontal", + sx: { "& .MuiInputBase-root": { width: 200 } }, + fullWidth: false, + SelectProps: { + renderValue: () => { + if (cloudLogFilters?.loggingSource?.length === 1) { + return `Source (${cloudLogFilters.loggingSource[0]})`; + } else if (cloudLogFilters?.loggingSource?.length) { + return `Source (${cloudLogFilters.loggingSource.length})`; + } else { + return `Source`; + } + }, + }, + }} + itemRenderer={(option) => <>{upperCase(option.value)}} + /> + ({ + label: x.name, + value: x.endpoint, + })) + : [] + } + value={cloudLogFilters.webhook ?? []} + onChange={(v) => + setCloudLogFilters((prev) => ({ ...prev, webhook: v })) + } + TextFieldProps={{ + id: "webhook", + className: "labelHorizontal", + sx: { "& .MuiInputBase-root": { width: 180 } }, + fullWidth: false, + SelectProps: { + renderValue: () => { + if (cloudLogFilters?.webhook?.length) { + return `Webhook (${cloudLogFilters.webhook.length})`; + } else { + return `Webhook`; + } + }, + }, + }} + itemRenderer={(option) => ( + <> + {option.label} {option.value} + + )} + /> + ({ + label: x.name, + value: x.name, + type: x.type, + })) + : [] + } + value={cloudLogFilters.extension ?? []} + onChange={(v) => + setCloudLogFilters((prev) => ({ ...prev, extension: v })) + } + TextFieldProps={{ + id: "extension", + className: "labelHorizontal", + sx: { "& .MuiInputBase-root": { width: 180 } }, + fullWidth: false, + placeholder: "Extension", + SelectProps: { + renderValue: () => { + if (cloudLogFilters?.extension?.length === 1) { + return `Extension (${cloudLogFilters.extension[0]})`; + } else if (cloudLogFilters?.extension?.length) { + return `Extension (${cloudLogFilters.extension.length})`; + } else { + return `Extension`; + } + }, + }, + }} + itemRenderer={(option) => ( + <> + {option.label} {option.type} + + )} + /> + ({ + label: config.name, + value: key, + type: config.type, + }) + )} + value={cloudLogFilters.column ?? []} + onChange={(v) => + setCloudLogFilters((prev) => ({ ...prev, column: v })) + } + TextFieldProps={{ + id: "column", + className: "labelHorizontal", + sx: { "& .MuiInputBase-root": { width: 200 } }, + fullWidth: false, + placeholder: "Column", + SelectProps: { + renderValue: () => { + if (cloudLogFilters?.column?.length === 1) { + return `Column (${cloudLogFilters.column[0]})`; + } else if (cloudLogFilters?.column?.length) { + return `Column (${cloudLogFilters.column.length})`; + } else { + return `Column`; + } + }, + }, + }} + itemRenderer={(option) => ( + <> + {option.label} {option.value}  + {option.type} + + )} + /> setCloudLogFilters((prev) => ({ ...prev, severity })) @@ -264,86 +536,61 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) { )} /> - - setCloudLogFilters((c) => ({ ...c, timeRange: value })) - } - /> - mutate()} - title="Refresh" - icon={} - disabled={isValidating} - /> - + + ) : null} + + + {Array.isArray(data) && data.length > 0 ? ( + + + {cloudLogFilters.timeRange.type !== "range" && ( + + )} + + ) : isValidating ? ( + + ) : ( + )} - - - {isValidating && ( - - )} - - {/* {logQueryUrl} */} - - } - > - {cloudLogFilters.type === "build" ? ( - - ) : Array.isArray(data) && data.length > 0 ? ( - <> - - {cloudLogFilters.timeRange.type !== "range" && ( - - )} - - ) : isValidating ? ( - - ) : ( - + + )} ); diff --git a/src/components/TableModals/CloudLogsModal/utils.ts b/src/components/TableModals/CloudLogsModal/utils.ts index 3c7fddb0..306c4794 100644 --- a/src/components/TableModals/CloudLogsModal/utils.ts +++ b/src/components/TableModals/CloudLogsModal/utils.ts @@ -15,21 +15,55 @@ export const cloudLogFetcher = ( switch (cloudLogFilters.type) { case "rowy": logQuery.push(`logName = "projects/${projectId}/logs/rowy-logging"`); - break; - case "webhook": - logQuery.push( - `logName = "projects/${projectId}/logs/rowy-webhook-events"` - ); - logQuery.push(`jsonPayload.url : "${tablePath}"`); - if ( - Array.isArray(cloudLogFilters.webhook) && - cloudLogFilters.webhook.length > 0 - ) + if (cloudLogFilters?.functionType?.length) + logQuery.push( + cloudLogFilters.functionType + .map((functionType) => { + return `jsonPayload.functionType = "${functionType}"`; + }) + .join(encodeURIComponent(" OR ")) + ); + if (cloudLogFilters?.loggingSource?.length) { + logQuery.push( + cloudLogFilters.loggingSource + .map((loggingSource) => { + return `jsonPayload.loggingSource = "${loggingSource}"`; + }) + .join(encodeURIComponent(" OR ")) + ); + } else { + // mandatory filter to remove unwanted gcp diagnostic logs + logQuery.push( + ["backend-scripts", "backend-function", "hooks"] + .map((loggingSource) => { + return `jsonPayload.loggingSource = "${loggingSource}"`; + }) + .join(encodeURIComponent(" OR ")) + ); + } + if (cloudLogFilters?.webhook?.length) { logQuery.push( cloudLogFilters.webhook .map((id) => `jsonPayload.url : "${id}"`) .join(encodeURIComponent(" OR ")) ); + } + if (cloudLogFilters?.extension?.length) + logQuery.push( + cloudLogFilters.extension + .map((extensionName) => { + return `jsonPayload.extensionName = "${extensionName}"`; + }) + .join(encodeURIComponent(" OR ")) + ); + if (cloudLogFilters?.column?.length) + logQuery.push( + cloudLogFilters.column + .map((column) => { + return `jsonPayload.fieldName = "${column}"`; + }) + .join(encodeURIComponent(" OR ")) + ); break; case "audit": @@ -41,10 +75,6 @@ export const cloudLogFetcher = ( ); break; - case "functions": - logQuery.push(`resource.labels.function_name = "R-${tablePath}"`); - break; - default: break; } diff --git a/src/components/TableModals/ExtensionsModal/ExtensionList.tsx b/src/components/TableModals/ExtensionsModal/ExtensionList.tsx index dbd63a9e..7ed8e6c6 100644 --- a/src/components/TableModals/ExtensionsModal/ExtensionList.tsx +++ b/src/components/TableModals/ExtensionsModal/ExtensionList.tsx @@ -14,6 +14,7 @@ import { import { Extension as ExtensionIcon, Copy as DuplicateIcon, + CloudLogs as LogsIcon, } from "@src/assets/icons"; import EditIcon from "@mui/icons-material/EditOutlined"; import DeleteIcon from "@mui/icons-material/DeleteOutlined"; @@ -21,6 +22,12 @@ import DeleteIcon from "@mui/icons-material/DeleteOutlined"; import EmptyState from "@src/components/EmptyState"; import { extensionNames, IExtension } from "./utils"; import { DATE_TIME_FORMAT } from "@src/constants/dates"; +import { useSetAtom } from "jotai"; +import { + cloudLogFiltersAtom, + tableModalAtom, + tableScope, +} from "@src/atoms/tableScope"; export interface IExtensionListProps { extensions: IExtension[]; @@ -37,6 +44,9 @@ export default function ExtensionList({ handleEdit, handleDelete, }: IExtensionListProps) { + const setModal = useSetAtom(tableModalAtom, tableScope); + const setCloudLogFilters = useSetAtom(cloudLogFiltersAtom, tableScope); + if (extensions.length === 0) return ( + + { + setModal("cloudLogs"); + setCloudLogFilters({ + type: "rowy", + functionType: ["extension"], + timeRange: { type: "days", value: 7 }, + extension: [extensionObject.name], + }); + }} + > + + + { setModal("cloudLogs"); setCloudLogFilters({ - type: "webhook", + type: "rowy", + functionType: ["hooks"], timeRange: { type: "days", value: 7 }, webhook: [webhook.endpoint], }); From 430833c46ebe8165a921de8ab7f10d6f4ec4f692 Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Sat, 17 Dec 2022 06:53:24 +0000 Subject: [PATCH 13/29] bug fix: small blip in the ordering --- src/components/Table/ColumnSelect.tsx | 10 +++++++- src/components/TableToolbar/HiddenFields.tsx | 27 +++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/components/Table/ColumnSelect.tsx b/src/components/Table/ColumnSelect.tsx index d5b3fa15..599b32f1 100644 --- a/src/components/Table/ColumnSelect.tsx +++ b/src/components/Table/ColumnSelect.tsx @@ -22,14 +22,22 @@ export interface IColumnSelectProps { filterColumns?: (column: ColumnConfig) => boolean; showFieldNames?: boolean; options?: ColumnOption[]; + tableColumnsOrdered?: ColumnConfig[]; } export default function ColumnSelect({ filterColumns, showFieldNames, + tableColumnsOrdered: tableColumnsOrdered_, ...props }: IColumnSelectProps & Omit, "options">) { - const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope); + // if tableColumnsOrdered is passed(in case of HiddenFields) else traditional way to get tableColumnsOrders (backward compitable) + let [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope); + + if (tableColumnsOrdered_) { + tableColumnsOrdered = tableColumnsOrdered_; + } + const options = props.options || (filterColumns diff --git a/src/components/TableToolbar/HiddenFields.tsx b/src/components/TableToolbar/HiddenFields.tsx index 0a537713..0ec52f78 100644 --- a/src/components/TableToolbar/HiddenFields.tsx +++ b/src/components/TableToolbar/HiddenFields.tsx @@ -2,6 +2,7 @@ import { useEffect, useRef, useMemo, + useCallback, useState, forwardRef, ChangeEvent, @@ -34,6 +35,7 @@ import { tableScope, tableIdAtom, updateColumnAtom, + tableColumnsOrderedAtom, } from "@src/atoms/tableScope"; import { formatSubTableName } from "@src/utils/table"; @@ -59,6 +61,10 @@ export default function HiddenFields() { setHiddenFields(userDocHiddenFields); }, [userDocHiddenFields]); + const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope); + // cashed tableColumnsOrdered from quick updates(reorder). + const [quickAccessTableColumnsOrdered, setQuickAccessTableColumnsOrdered] = + useState(tableColumnsOrdered); // const tableColumns = tableColumnsOrdered.map(({ key, name }) => ({ // value: key, // label: name, @@ -157,9 +163,27 @@ export default function HiddenFields() { const updateColumn = useSetAtom(updateColumnAtom, tableScope); + // function to reorder columns from cashed tableColumnsOrdered + const optimisticReorder = useCallback( + (from: number, to: number) => { + let temp = Array.from(quickAccessTableColumnsOrdered); + const currentColumn = temp.splice(from, 1)[0]; + temp.splice(to, 0, currentColumn); + + // update indexes + for (let i = from < to ? from : to; i < temp.length; i++) { + temp[i].index = i; + } + setQuickAccessTableColumnsOrdered(temp); + }, + [quickAccessTableColumnsOrdered] + ); + // updates column on drag end function handleOnDragEnd(result: DropResult) { - if (!result.destination) return; + if (!result.destination || result.destination.index === result.source.index) + return; + optimisticReorder(result.source.index, result.destination.index); updateColumn({ key: result.draggableId, config: {}, @@ -212,6 +236,7 @@ export default function HiddenFields() { {hiddenFields.length > 0 ? `${hiddenFields.length} hidden` : "Hide"} Date: Sat, 17 Dec 2022 08:49:26 +0000 Subject: [PATCH 14/29] revalidate cache --- src/components/TableToolbar/HiddenFields.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/TableToolbar/HiddenFields.tsx b/src/components/TableToolbar/HiddenFields.tsx index 0ec52f78..32f9118a 100644 --- a/src/components/TableToolbar/HiddenFields.tsx +++ b/src/components/TableToolbar/HiddenFields.tsx @@ -163,6 +163,11 @@ export default function HiddenFields() { const updateColumn = useSetAtom(updateColumnAtom, tableScope); + // function to revalidate quickAccessTableColumnsOrdered + function revalidateCache() { + setQuickAccessTableColumnsOrdered(tableColumnsOrdered); + } + // function to reorder columns from cashed tableColumnsOrdered const optimisticReorder = useCallback( (from: number, to: number) => { @@ -229,7 +234,10 @@ export default function HiddenFields() { <> } - onClick={() => setOpen((o) => !o)} + onClick={() => { + revalidateCache(); + setOpen((o) => !o); + }} active={hiddenFields.length > 0} ref={buttonRef} > From f9abf4b385fa6d04b54a05d6631eb5418504e7bc Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Sun, 25 Dec 2022 13:58:15 +0000 Subject: [PATCH 15/29] show a snackbar error when firestore pemission rejects a change --- .../Table/TableCell/EditorCellController.tsx | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/components/Table/TableCell/EditorCellController.tsx b/src/components/Table/TableCell/EditorCellController.tsx index a8978c8d..c80380d4 100644 --- a/src/components/Table/TableCell/EditorCellController.tsx +++ b/src/components/Table/TableCell/EditorCellController.tsx @@ -2,6 +2,7 @@ import { useEffect, useLayoutEffect } from "react"; import useStateRef from "react-usestateref"; import { useSetAtom } from "jotai"; import { isEqual } from "lodash-es"; +import { useSnackbar } from "notistack"; import { tableScope, updateFieldAtom } from "@src/atoms/tableScope"; import type { @@ -45,6 +46,8 @@ export default function EditorCellController({ const [isDirty, setIsDirty, isDirtyRef] = useStateRef(false); const updateField = useSetAtom(updateFieldAtom, tableScope); + const { enqueueSnackbar } = useSnackbar(); + // When this cell’s data has updated, update the local value if // it’s not dirty and the value is different useEffect(() => { @@ -53,17 +56,20 @@ export default function EditorCellController({ }, [isDirty, localValueRef, setLocalValue, value]); // This is where we update the documents - const handleSubmit = () => { + const handleSubmit = async () => { // props.disabled should always be false as withRenderTableCell would // render DisplayCell instead of EditorCell if (props.disabled || !isDirtyRef.current) return; - - updateField({ - path: props._rowy_ref.path, - fieldName: props.column.fieldName, - value: localValueRef.current, - deleteField: localValueRef.current === undefined, - }); + try { + await updateField({ + path: props._rowy_ref.path, + fieldName: props.column.fieldName, + value: localValueRef.current, + deleteField: localValueRef.current === undefined, + }); + } catch (e) { + enqueueSnackbar((e as Error).message, { variant: "error" }); + } }; useLayoutEffect(() => { From 072686bb66f3fb1e7cd9cd70b08555b862e1e175 Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Wed, 28 Dec 2022 17:01:48 +0930 Subject: [PATCH 16/29] expand rowy logging into tabs: extension, webhook, column(derivative/action/default value/connector) --- src/atoms/tableScope/ui.ts | 2 +- src/components/ColumnMenu/ColumnMenu.tsx | 45 +- .../CloudLogsModal/CloudLogsModal.tsx | 590 ++++++++---------- .../CloudLogsModal/TimeRangeSelect.tsx | 4 +- .../TableModals/CloudLogsModal/utils.ts | 83 +-- .../ExtensionsModal/ExtensionList.tsx | 3 +- .../TableModals/WebhooksModal/WebhookList.tsx | 3 +- 7 files changed, 314 insertions(+), 416 deletions(-) diff --git a/src/atoms/tableScope/ui.ts b/src/atoms/tableScope/ui.ts index 44afee10..177ae9b2 100644 --- a/src/atoms/tableScope/ui.ts +++ b/src/atoms/tableScope/ui.ts @@ -142,7 +142,7 @@ export const selectedCellAtom = atom(null); export const contextMenuTargetAtom = atom(null); export type CloudLogFilters = { - type: "rowy" | "audit" | "build"; + type: "extension" | "webhook" | "column" | "audit" | "build"; timeRange: | { type: "seconds" | "minutes" | "hours" | "days"; value: number } | { type: "range"; start: Date; end: Date }; diff --git a/src/components/ColumnMenu/ColumnMenu.tsx b/src/components/ColumnMenu/ColumnMenu.tsx index a5f76a67..99b168aa 100644 --- a/src/components/ColumnMenu/ColumnMenu.tsx +++ b/src/components/ColumnMenu/ColumnMenu.tsx @@ -25,7 +25,6 @@ import { import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward"; import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward"; import EditIcon from "@mui/icons-material/EditOutlined"; -// import ReorderIcon from "@mui/icons-material/Reorder"; import SettingsIcon from "@mui/icons-material/SettingsOutlined"; import EvalIcon from "@mui/icons-material/PlayCircleOutline"; @@ -319,24 +318,19 @@ export default function ColumnMenu({ }, disabled: !isConfigurable, }, - // { - // label: "Re-order", - // icon: , - // onClick: () => alert("REORDER"), - // }, - - // { - // label: "Hide for everyone", - // activeLabel: "Show", - // icon: , - // activeIcon: , - // onClick: () => { - // actions.update(column.key, { hidden: !column.hidden }); - // handleClose(); - // }, - // active: column.hidden, - // color: "error" as "error", - // }, + { + key: "logs", + label: altPress ? "Logs" : "Logs…", + icon: , + onClick: () => { + setModal("cloudLogs"); + setCloudLogFilters({ + type: "column", + timeRange: { type: "days", value: 7 }, + column: [column.key], + }); + }, + }, ]; // TODO: Generalize @@ -388,19 +382,6 @@ export default function ColumnMenu({ confirm: "Evaluate", }), }, - { - key: "logs", - label: altPress ? "Logs" : "Logs…", - icon: , - onClick: () => { - setModal("cloudLogs"); - setCloudLogFilters({ - type: "rowy", - timeRange: { type: "days", value: 7 }, - column: [column.key], - }); - }, - }, ]; const columnActions: IMenuContentsProps["menuItems"] = [ diff --git a/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx b/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx index 95af37f1..93c2a8af 100644 --- a/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx +++ b/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx @@ -13,6 +13,7 @@ import { InputAdornment, Button, Box, + CircularProgress, } from "@mui/material"; import RefreshIcon from "@mui/icons-material/Refresh"; import { CloudLogs as LogsIcon } from "@src/assets/icons"; @@ -96,7 +97,7 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) { "&, & .MuiTab-root": { minHeight: { md: "var(--dialog-title-height)" }, }, - ml: { md: 18 }, + ml: { md: 20 }, mr: { md: 40 / 8 + 3 }, minHeight: 32, @@ -114,15 +115,17 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) { + onChange={(_, v) => { setCloudLogFilters((c) => ({ type: v, timeRange: c.timeRange, - })) - } + })); + }} aria-label="Filter by log type" > - Rowy Logging + Extension + Webhook + Column Audit Build @@ -142,154 +145,18 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) { )} - {cloudLogFilters.type === "rowy" && <>} - {cloudLogFilters.type === "audit" && ( - - setCloudLogFilters((prev) => ({ - ...prev, - auditRowId: e.target.value, - })) - } - InputProps={{ - startAdornment: ( - - {tableSettings.collection}/ - - ), - }} - className="labelHorizontal" - sx={{ - "& .MuiInputBase-root, & .MuiInputBase-input": { - typography: "body2", - fontFamily: "mono", - }, - "& .MuiInputAdornment-positionStart": { - m: "0 !important", - pointerEvents: "none", - }, - "& .MuiInputBase-input": { pl: 0 }, - }} - /> - )} -
    {cloudLogFilters.type !== "build" && ( <> - {!isValidating && Array.isArray(data) && ( - - {data.length} entries - - )} - - {cloudLogFilters.type !== "rowy" && ( - - setCloudLogFilters((prev) => ({ ...prev, severity })) - } - TextFieldProps={{ - style: { width: 130 }, - placeholder: "Severity", - SelectProps: { - renderValue: () => { - if ( - !Array.isArray(cloudLogFilters.severity) || - cloudLogFilters.severity.length === 0 - ) - return `Severity`; - - if (cloudLogFilters.severity.length === 1) - return ( - <> - Severity{" "} - - - ); - - return `Severity (${cloudLogFilters.severity.length})`; - }, - }, - }} - itemRenderer={(option) => ( - <> - - {startCase(option.value.toLowerCase())} - - )} - /> - )} - - setCloudLogFilters((c) => ({ ...c, timeRange: value })) - } - /> - mutate()} - title="Refresh" - icon={} - disabled={isValidating} - /> - - )} - - - {isValidating && ( - - )} - - } - > - {cloudLogFilters.type === "build" ? ( - - ) : ( - - - {cloudLogFilters.type === "rowy" ? ( - + + {isValidating ? "Loading" : `${data?.length ?? 0} entries`} + - { - setCloudLogFilters((prev) => ({ - ...prev, - functionType: v, - })); - }} - TextFieldProps={{ - id: "functionType", - className: "labelHorizontal", - sx: { "& .MuiInputBase-root": { width: 200 } }, - fullWidth: false, - SelectProps: { - renderValue: () => { - if (cloudLogFilters?.functionType?.length === 1) { - return `Type (${cloudLogFilters.functionType[0]})`; - } else if (cloudLogFilters?.functionType?.length) { - return `Type (${cloudLogFilters.functionType.length})`; - } else { - return `Type`; - } - }, - }, - }} - itemRenderer={(option) => <>{upperCase(option.value)}} + + mutate()} + title="Refresh" + icon={ + isValidating ? ( + + ) : ( + + ) + } + disabled={isValidating} /> - { - setCloudLogFilters((prev) => ({ - ...prev, - loggingSource: v, - })); - }} - TextFieldProps={{ - id: "loggingSource", - className: "labelHorizontal", - sx: { "& .MuiInputBase-root": { width: 200 } }, - fullWidth: false, - SelectProps: { - renderValue: () => { - if (cloudLogFilters?.loggingSource?.length === 1) { - return `Source (${cloudLogFilters.loggingSource[0]})`; - } else if (cloudLogFilters?.loggingSource?.length) { - return `Source (${cloudLogFilters.loggingSource.length})`; - } else { - return `Source`; - } + + )} + + + } + > + {cloudLogFilters.type === "build" ? ( + + ) : ( + + {["extension", "webhook", "column", "audit"].includes( + cloudLogFilters.type + ) ? ( + + {cloudLogFilters.type === "extension" ? ( + <> + ({ + label: x.name, + value: x.name, + type: x.type, + })) + : [] + } + value={cloudLogFilters.extension ?? []} + onChange={(v) => + setCloudLogFilters((prev) => ({ ...prev, extension: v })) + } + TextFieldProps={{ + id: "extension", + className: "labelHorizontal", + sx: { + width: "100%", + "& .MuiInputBase-root": { width: "100%" }, }, - }, - }} - itemRenderer={(option) => <>{upperCase(option.value)}} - /> + fullWidth: false, + placeholder: "Extension", + SelectProps: { + renderValue: () => { + if (cloudLogFilters?.extension?.length === 1) { + return `Extension (${cloudLogFilters.extension[0]})`; + } else if (cloudLogFilters?.extension?.length) { + return `Extension (${cloudLogFilters.extension.length})`; + } else { + return `Extension`; + } + }, + }, + }} + itemRenderer={(option) => ( + <> + {option.label} {option.type} + + )} + /> + + ) : null} + {cloudLogFilters.type === "webhook" ? ( { @@ -412,133 +306,145 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) { )} /> - ({ - label: x.name, - value: x.name, - type: x.type, - })) - : [] - } - value={cloudLogFilters.extension ?? []} - onChange={(v) => - setCloudLogFilters((prev) => ({ ...prev, extension: v })) - } - TextFieldProps={{ - id: "extension", - className: "labelHorizontal", - sx: { "& .MuiInputBase-root": { width: 180 } }, - fullWidth: false, - placeholder: "Extension", - SelectProps: { - renderValue: () => { - if (cloudLogFilters?.extension?.length === 1) { - return `Extension (${cloudLogFilters.extension[0]})`; - } else if (cloudLogFilters?.extension?.length) { - return `Extension (${cloudLogFilters.extension.length})`; - } else { - return `Extension`; - } + ) : null} + {cloudLogFilters.type === "column" ? ( + <> + ({ + label: config.name, + value: key, + type: config.type, + }) + )} + value={cloudLogFilters.column ?? []} + onChange={(v) => + setCloudLogFilters((prev) => ({ ...prev, column: v })) + } + TextFieldProps={{ + id: "column", + className: "labelHorizontal", + sx: { + width: "100%", + "& .MuiInputBase-root": { width: "100%" }, }, - }, - }} - itemRenderer={(option) => ( - <> - {option.label} {option.type} - - )} - /> - ({ - label: config.name, - value: key, - type: config.type, - }) - )} - value={cloudLogFilters.column ?? []} - onChange={(v) => - setCloudLogFilters((prev) => ({ ...prev, column: v })) - } - TextFieldProps={{ - id: "column", - className: "labelHorizontal", - sx: { "& .MuiInputBase-root": { width: 200 } }, - fullWidth: false, - placeholder: "Column", - SelectProps: { - renderValue: () => { - if (cloudLogFilters?.column?.length === 1) { - return `Column (${cloudLogFilters.column[0]})`; - } else if (cloudLogFilters?.column?.length) { - return `Column (${cloudLogFilters.column.length})`; - } else { - return `Column`; - } + fullWidth: false, + placeholder: "Column", + SelectProps: { + renderValue: () => { + if (cloudLogFilters?.column?.length === 1) { + return `Column (${cloudLogFilters.column[0]})`; + } else if (cloudLogFilters?.column?.length) { + return `Column (${cloudLogFilters.column.length})`; + } else { + return `Column`; + } + }, }, - }, - }} - itemRenderer={(option) => ( - <> - {option.label} {option.value}  - {option.type} - - )} - /> - - setCloudLogFilters((prev) => ({ ...prev, severity })) - } - TextFieldProps={{ - style: { width: 130 }, - placeholder: "Severity", - SelectProps: { - renderValue: () => { - if ( - !Array.isArray(cloudLogFilters.severity) || - cloudLogFilters.severity.length === 0 - ) - return `Severity`; + }} + itemRenderer={(option) => ( + <> + {option.label} {option.value}  + {option.type} + + )} + /> + + ) : null} + {cloudLogFilters.type === "audit" ? ( + <> + + setCloudLogFilters((prev) => ({ + ...prev, + auditRowId: e.target.value, + })) + } + InputProps={{ + startAdornment: ( + + {tableSettings.collection}/ + + ), + }} + className="labelHorizontal" + sx={{ + width: "100%", + "& .MuiInputBase-root, & .MuiInputBase-input": { + width: "100%", + typography: "body2", + fontFamily: "mono", + }, + "& .MuiInputAdornment-positionStart": { + m: "0 !important", + pointerEvents: "none", + }, + "& .MuiInputBase-input": { pl: 0 }, + "& .MuiFormLabel-root": { + whiteSpace: "nowrap", + }, + }} + /> + + ) : null} + + setCloudLogFilters((prev) => ({ ...prev, severity })) + } + TextFieldProps={{ + style: { width: 200 }, + placeholder: "Severity", + SelectProps: { + renderValue: () => { + if ( + !Array.isArray(cloudLogFilters.severity) || + cloudLogFilters.severity.length === 0 + ) + return `Severity`; - if (cloudLogFilters.severity.length === 1) - return ( - <> - Severity{" "} - - - ); + if (cloudLogFilters.severity.length === 1) + return ( + <> + Severity{" "} + + + ); - return `Severity (${cloudLogFilters.severity.length})`; - }, + return `Severity (${cloudLogFilters.severity.length})`; }, - }} - itemRenderer={(option) => ( - <> - - {startCase(option.value.toLowerCase())} - - )} - /> - - ) : null} - + }, + }} + itemRenderer={(option) => ( + <> + + {startCase(option.value.toLowerCase())} + + )} + /> + + setCloudLogFilters((c) => ({ ...c, timeRange: value })) + } + /> + + ) : null} +
    {value && value.type !== "range" && ( { + return `jsonPayload.loggingSource = "${loggingSource}"`; + }) + .join(encodeURIComponent(" OR ")) + ); + } + switch (cloudLogFilters.type) { - case "rowy": + case "extension": logQuery.push(`logName = "projects/${projectId}/logs/rowy-logging"`); - if (cloudLogFilters?.functionType?.length) - logQuery.push( - cloudLogFilters.functionType - .map((functionType) => { - return `jsonPayload.functionType = "${functionType}"`; - }) - .join(encodeURIComponent(" OR ")) - ); - if (cloudLogFilters?.loggingSource?.length) { - logQuery.push( - cloudLogFilters.loggingSource - .map((loggingSource) => { - return `jsonPayload.loggingSource = "${loggingSource}"`; - }) - .join(encodeURIComponent(" OR ")) - ); - } else { - // mandatory filter to remove unwanted gcp diagnostic logs - logQuery.push( - ["backend-scripts", "backend-function", "hooks"] - .map((loggingSource) => { - return `jsonPayload.loggingSource = "${loggingSource}"`; - }) - .join(encodeURIComponent(" OR ")) - ); - } - if (cloudLogFilters?.webhook?.length) { - logQuery.push( - cloudLogFilters.webhook - .map((id) => `jsonPayload.url : "${id}"`) - .join(encodeURIComponent(" OR ")) - ); - } - if (cloudLogFilters?.extension?.length) + if (cloudLogFilters?.extension?.length) { logQuery.push( cloudLogFilters.extension .map((extensionName) => { @@ -56,7 +34,25 @@ export const cloudLogFetcher = ( }) .join(encodeURIComponent(" OR ")) ); - if (cloudLogFilters?.column?.length) + } else { + logQuery.push(`jsonPayload.functionType = "extension"`); + } + break; + + case "webhook": + if (cloudLogFilters?.webhook?.length) { + logQuery.push( + cloudLogFilters.webhook + .map((id) => `jsonPayload.url : "${id}"`) + .join(encodeURIComponent(" OR ")) + ); + } else { + logQuery.push(`jsonPayload.functionType = "hooks"`); + } + break; + + case "column": + if (cloudLogFilters?.column?.length) { logQuery.push( cloudLogFilters.column .map((column) => { @@ -64,6 +60,21 @@ export const cloudLogFetcher = ( }) .join(encodeURIComponent(" OR ")) ); + } else { + logQuery.push( + [ + "connector", + "derivative-script", + "action", + "derivative-function", + "defaultValue", + ] + .map((functionType) => { + return `jsonPayload.functionType = "${functionType}"`; + }) + .join(encodeURIComponent(" OR ")) + ); + } break; case "audit": diff --git a/src/components/TableModals/ExtensionsModal/ExtensionList.tsx b/src/components/TableModals/ExtensionsModal/ExtensionList.tsx index 7ed8e6c6..a0d5b82f 100644 --- a/src/components/TableModals/ExtensionsModal/ExtensionList.tsx +++ b/src/components/TableModals/ExtensionsModal/ExtensionList.tsx @@ -107,8 +107,7 @@ export default function ExtensionList({ onClick={() => { setModal("cloudLogs"); setCloudLogFilters({ - type: "rowy", - functionType: ["extension"], + type: "extension", timeRange: { type: "days", value: 7 }, extension: [extensionObject.name], }); diff --git a/src/components/TableModals/WebhooksModal/WebhookList.tsx b/src/components/TableModals/WebhooksModal/WebhookList.tsx index 01b67d07..d2f6b670 100644 --- a/src/components/TableModals/WebhooksModal/WebhookList.tsx +++ b/src/components/TableModals/WebhooksModal/WebhookList.tsx @@ -126,8 +126,7 @@ export default function WebhookList({ onClick={() => { setModal("cloudLogs"); setCloudLogFilters({ - type: "rowy", - functionType: ["hooks"], + type: "webhook", timeRange: { type: "days", value: 7 }, webhook: [webhook.endpoint], }); From 28f6ea519f22589965def134db46794b13ae28bf Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Thu, 29 Dec 2022 20:57:18 +0930 Subject: [PATCH 17/29] column logs: only allow logging view for connector/action/default value/derivative --- src/components/ColumnMenu/ColumnMenu.tsx | 14 +++++++++++--- .../CloudLogsModal/CloudLogsModal.tsx | 17 +++++++++++++---- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/components/ColumnMenu/ColumnMenu.tsx b/src/components/ColumnMenu/ColumnMenu.tsx index 99b168aa..7dff465e 100644 --- a/src/components/ColumnMenu/ColumnMenu.tsx +++ b/src/components/ColumnMenu/ColumnMenu.tsx @@ -318,7 +318,15 @@ export default function ColumnMenu({ }, disabled: !isConfigurable, }, - { + ]; + + if ( + column?.config?.defaultValue?.type === "dynamic" || + [FieldType.action, FieldType.derivative, FieldType.connector].includes( + column.type + ) + ) { + configActions.push({ key: "logs", label: altPress ? "Logs" : "Logs…", icon: , @@ -330,8 +338,8 @@ export default function ColumnMenu({ column: [column.key], }); }, - }, - ]; + }); + } // TODO: Generalize const handleEvaluateAll = async () => { diff --git a/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx b/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx index 93c2a8af..2b835e3a 100644 --- a/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx +++ b/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx @@ -43,6 +43,7 @@ import { cloudLogFiltersAtom, } from "@src/atoms/tableScope"; import { cloudLogFetcher } from "./utils"; +import { FieldType } from "@src/constants/fields"; export default function CloudLogsModal({ onClose }: ITableModalProps) { const [projectId] = useAtom(projectIdAtom, projectScope); @@ -312,13 +313,21 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) { ({ + options={Object.entries(tableSchema.columns ?? {}) + .filter( + ([key, config]) => + config?.config?.defaultValue?.type === "dynamic" || + [ + FieldType.action, + FieldType.derivative, + FieldType.connector, + ].includes(config.type) + ) + .map(([key, config]) => ({ label: config.name, value: key, type: config.type, - }) - )} + }))} value={cloudLogFilters.column ?? []} onChange={(v) => setCloudLogFilters((prev) => ({ ...prev, column: v })) From 503dae79d8dba7537b0444280136c8e1a5428598 Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Thu, 29 Dec 2022 21:41:06 +0930 Subject: [PATCH 18/29] auto refresh when switching log tabs --- .../TableModals/CloudLogsModal/CloudLogsModal.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx b/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx index 2b835e3a..2e20a475 100644 --- a/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx +++ b/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx @@ -116,11 +116,20 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) { { + onChange={(_, newType) => { setCloudLogFilters((c) => ({ - type: v, + type: newType, timeRange: c.timeRange, })); + if ( + ["extension", "webhook", "column", "audit"].includes( + newType + ) + ) { + setTimeout(() => { + mutate(); + }, 0); + } }} aria-label="Filter by log type" > @@ -169,6 +178,7 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) { severity: undefined, })); }} + disabled={isValidating} > Reset From 295974145acd0dafe4c7faf86ec4172c554ff25e Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Thu, 29 Dec 2022 21:58:36 +0930 Subject: [PATCH 19/29] add back old functions logging --- src/atoms/tableScope/ui.ts | 2 +- .../CloudLogsModal/CloudLogsModal.tsx | 32 ++++++++++++------- .../TableModals/CloudLogsModal/utils.ts | 4 +++ 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/atoms/tableScope/ui.ts b/src/atoms/tableScope/ui.ts index 177ae9b2..c1de1780 100644 --- a/src/atoms/tableScope/ui.ts +++ b/src/atoms/tableScope/ui.ts @@ -142,7 +142,7 @@ export const selectedCellAtom = atom(null); export const contextMenuTargetAtom = atom(null); export type CloudLogFilters = { - type: "extension" | "webhook" | "column" | "audit" | "build"; + type: "extension" | "webhook" | "column" | "audit" | "build" | "functions"; timeRange: | { type: "seconds" | "minutes" | "hours" | "days"; value: number } | { type: "range"; start: Date; end: Date }; diff --git a/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx b/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx index 2e20a475..b211dec2 100644 --- a/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx +++ b/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx @@ -17,6 +17,7 @@ import { } from "@mui/material"; import RefreshIcon from "@mui/icons-material/Refresh"; import { CloudLogs as LogsIcon } from "@src/assets/icons"; +import ClearIcon from "@mui/icons-material/Clear"; import Modal from "@src/components/Modal"; import TableToolbarButton from "@src/components/TableToolbar/TableToolbarButton"; @@ -122,9 +123,13 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) { timeRange: c.timeRange, })); if ( - ["extension", "webhook", "column", "audit"].includes( - newType - ) + [ + "extension", + "webhook", + "column", + "audit", + "functions", + ].includes(newType) ) { setTimeout(() => { mutate(); @@ -138,6 +143,9 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) { Column Audit Build + + Functions (legacy) + ) : ( - {isValidating ? "Loading" : `${data?.length ?? 0} entries`} + {isValidating ? "" : `${data?.length ?? 0} entries`} - - + /> mutate()} title="Refresh" @@ -212,7 +219,7 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) { overflowY: "visible", }} > - {["extension", "webhook", "column", "audit"].includes( + {["extension", "webhook", "column", "audit", "functions"].includes( cloudLogFilters.type ) ? ( + {cloudLogFilters.type === "functions" ? ( + + ) : null} {cloudLogFilters.type === "extension" ? ( <> Date: Thu, 29 Dec 2022 22:03:12 +0930 Subject: [PATCH 20/29] fix load more not working --- .../TableModals/CloudLogsModal/CloudLogsModal.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx b/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx index b211dec2..76cfe719 100644 --- a/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx +++ b/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx @@ -489,15 +489,18 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) { marginRight: "auto", display: "flex", }} - onClick={() => + onClick={() => { setCloudLogFilters((c) => ({ ...c, timeRange: { ...c.timeRange, value: (c.timeRange as any).value * 2, }, - })) - } + })); + setTimeout(() => { + mutate(); + }, 0); + }} > Load more (last {cloudLogFilters.timeRange.value * 2}{" "} {cloudLogFilters.timeRange.type}) From 9b7a31f029e2a2cc33d046529a010f4e24aca464 Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Fri, 30 Dec 2022 04:35:44 +0000 Subject: [PATCH 21/29] worked on adding "Import JSON feat" --- src/atoms/tableScope/ui.ts | 2 +- .../TableToolbar/ImportData/ImportData.tsx | 2 +- .../TableToolbar/ImportData/ImportFromCsv.tsx | 110 +++++++++++++++--- 3 files changed, 98 insertions(+), 16 deletions(-) diff --git a/src/atoms/tableScope/ui.ts b/src/atoms/tableScope/ui.ts index 3d11a081..f14c8f12 100644 --- a/src/atoms/tableScope/ui.ts +++ b/src/atoms/tableScope/ui.ts @@ -109,7 +109,7 @@ export type ImportAirtableData = { records: Record[] }; /** Store import CSV popover and wizard state */ export const importCsvAtom = atom<{ - importType: "csv" | "tsv"; + importType: "csv" | "tsv" | "json"; csvData: ImportCsvData | null; }>({ importType: "csv", csvData: null }); diff --git a/src/components/TableToolbar/ImportData/ImportData.tsx b/src/components/TableToolbar/ImportData/ImportData.tsx index 01b2aa4e..4bdfc216 100644 --- a/src/components/TableToolbar/ImportData/ImportData.tsx +++ b/src/components/TableToolbar/ImportData/ImportData.tsx @@ -91,7 +91,7 @@ export default function ImportData({ render, PopoverProps }: IImportDataProps) { variant="fullWidth" > (importMethodRef.current = ImportMethod.csv)} /> diff --git a/src/components/TableToolbar/ImportData/ImportFromCsv.tsx b/src/components/TableToolbar/ImportData/ImportFromCsv.tsx index 40ec3f84..d16f50bb 100644 --- a/src/components/TableToolbar/ImportData/ImportFromCsv.tsx +++ b/src/components/TableToolbar/ImportData/ImportFromCsv.tsx @@ -28,13 +28,73 @@ import { } from "@src/atoms/tableScope"; import { analytics, logEvent } from "@src/analytics"; +import { parse as parseJSON } from "json2csv"; +import { remove as _remove } from "lodash-es"; + export enum ImportMethod { paste = "paste", upload = "upload", url = "url", } -export default function ImportFromCsv() { +enum FileType { + CSV = "text/csv", + TSV = "text/tab-separated-values", + JSON = "application/json", +} +// extract the column names and return the names +function extractFields(data: JSON): string[] { + return _remove(Object.keys(data), (col: string) => col !== "id"); +} + +function convertJSONToCSV(rawData: string): string | false { + let rawDataJSONified: JSON[]; + try { + rawDataJSONified = JSON.parse(rawData); + } catch (e) { + return false; + } + if (rawDataJSONified.length < 1) { + return false; + } + const fields = extractFields(rawDataJSONified[0]); + const opts = { fields }; + + try { + const csv = parseJSON(rawDataJSONified, opts); + return csv; + } catch (err) { + return false; + } +} + +function hasProperJsonStructure(raw: string) { + try { + raw = JSON.parse(raw); + const type = Object.prototype.toString.call(raw); + // we don't want '[object Object]' + return type === "[object Array]"; + } catch (err) { + return false; + } +} + +function checkIsJson(raw: string): boolean { + raw = typeof raw !== "string" ? JSON.stringify(raw) : raw; + + try { + raw = JSON.parse(raw); + } catch (e) { + return false; + } + + if (typeof raw === "object" && raw !== null) { + return true; + } + return false; +} + +export default function ImportFromFile() { const [{ importType: importTypeCsv, csvData }, setImportCsv] = useAtom( importCsvAtom, tableScope @@ -55,6 +115,20 @@ export default function ImportFromCsv() { }; }, [setImportCsv]); + const parseFile = (rawData: string) => { + if (importTypeRef.current === "json") { + if (!hasProperJsonStructure(rawData)) { + return setError("Invalid Structure! It must be an Array"); + } + const converted = convertJSONToCSV(rawData); + if (!converted) { + return setError("No columns detected"); + } + rawData = converted; + } + parseCsv(rawData); + }; + const parseCsv = useCallback( (csvString: string) => parse(csvString, { delimiter: [",", "\t"] }, (err, rows) => { @@ -86,13 +160,17 @@ export default function ImportFromCsv() { async (acceptedFiles: File[]) => { try { const file = acceptedFiles[0]; - const reader = new FileReader(); - reader.onload = (event: any) => parseCsv(event.target.result); - reader.readAsText(file); importTypeRef.current = - file.type === "text/tab-separated-values" ? "tsv" : "csv"; + file.type === FileType.TSV + ? "tsv" + : file.type === FileType.JSON + ? "json" + : "csv"; + const reader = new FileReader(); + reader.onload = (event: any) => parseFile(event.target.result); + reader.readAsText(file); } catch (error) { - enqueueSnackbar(`Please import a .tsv or .csv file`, { + enqueueSnackbar(`Please import a .tsv or .csv or .json file`, { variant: "error", anchorOrigin: { vertical: "top", @@ -107,10 +185,14 @@ export default function ImportFromCsv() { const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop, multiple: false, - accept: ["text/csv", "text/tab-separated-values"], + accept: [FileType.CSV, FileType.TSV, FileType.JSON], }); function setDataTypeRef(data: string) { + if (checkIsJson(data)) { + return (importTypeRef.current = "json"); + } + const getFirstLine = data?.match(/^(.*)/)?.[0]; /* * Catching edge case with regex @@ -128,8 +210,8 @@ export default function ImportFromCsv() { : (importTypeRef.current = "csv"); } const handlePaste = useDebouncedCallback((value: string) => { - parseCsv(value); setDataTypeRef(value); + parseFile(value); }, 1000); const handleUrl = useDebouncedCallback((value: string) => { @@ -138,8 +220,8 @@ export default function ImportFromCsv() { fetch(value, { mode: "no-cors" }) .then((res) => res.text()) .then((data) => { - parseCsv(data); setDataTypeRef(data); + parseFile(data); setLoading(false); }) .catch((e) => { @@ -217,7 +299,7 @@ export default function ImportFromCsv() { {isDragActive ? ( - Drop CSV or TSV file here… + Drop CSV or TSV or JSON file here… ) : ( <> @@ -227,8 +309,8 @@ export default function ImportFromCsv() { {validCsv - ? "Valid CSV or TSV" - : "Click to upload or drop CSV or TSV file here"} + ? "Valid CSV or TSV or JSON" + : "Click to upload or drop CSV or TSV or JSON file here"} @@ -249,7 +331,7 @@ export default function ImportFromCsv() { inputProps={{ minRows: 3 }} autoFocus fullWidth - label="Paste CSV or TSV text" + label="Paste CSV or TSV or JSON text" placeholder="column, column, …" onChange={(e) => { if (csvData !== null) @@ -279,7 +361,7 @@ export default function ImportFromCsv() { variant="filled" autoFocus fullWidth - label="Paste URL to CSV or TSV file" + label="Paste URL to CSV or TSV or JSON file" placeholder="https://" onChange={(e) => { if (csvData !== null) From 305d47d1a139617ea319d010bb2d718042eb7114 Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Fri, 30 Dec 2022 15:29:30 +0930 Subject: [PATCH 22/29] reuse Rowy rowy loggin types --- src/components/CodeEditor/extensions.d.ts | 6 +----- src/components/CodeEditor/rowy.d.ts | 5 +++++ .../ColumnModals/ColumnConfigModal/defaultValue.d.ts | 6 +----- .../TableModals/WebhooksModal/Schemas/basic.tsx | 12 ++---------- src/components/TableModals/WebhooksModal/utils.tsx | 12 ++---------- .../TableModals/WebhooksModal/webhooks.d.ts | 12 ++---------- src/components/fields/Action/action.d.ts | 6 +----- src/components/fields/Connector/connector.d.ts | 6 +----- src/components/fields/Derivative/derivative.d.ts | 6 +----- src/components/fields/types.ts | 6 ------ 10 files changed, 16 insertions(+), 61 deletions(-) diff --git a/src/components/CodeEditor/extensions.d.ts b/src/components/CodeEditor/extensions.d.ts index 3da39ae9..22af5756 100644 --- a/src/components/CodeEditor/extensions.d.ts +++ b/src/components/CodeEditor/extensions.d.ts @@ -26,11 +26,7 @@ type ExtensionContext = { extensionBody: any; }; RULES_UTILS: any; - logging: { - log: (payload: any) => void; - warn: (payload: any) => void; - error: (payload: any) => void; - }; + logging: RowyLogging; }; // extension body definition diff --git a/src/components/CodeEditor/rowy.d.ts b/src/components/CodeEditor/rowy.d.ts index f6852cce..42582dd0 100644 --- a/src/components/CodeEditor/rowy.d.ts +++ b/src/components/CodeEditor/rowy.d.ts @@ -17,6 +17,11 @@ type uploadOptions = { folderPath?: string; fileName?: string; }; +type RowyLogging = { + log: (payload: any) => void; + warn: (payload: any) => void; + error: (payload: any) => void; +}; interface Rowy { metadata: { /** diff --git a/src/components/ColumnModals/ColumnConfigModal/defaultValue.d.ts b/src/components/ColumnModals/ColumnConfigModal/defaultValue.d.ts index e3b90e13..779bd9be 100644 --- a/src/components/ColumnModals/ColumnConfigModal/defaultValue.d.ts +++ b/src/components/ColumnModals/ColumnConfigModal/defaultValue.d.ts @@ -4,10 +4,6 @@ type DefaultValueContext = { storage: firebasestorage.Storage; db: FirebaseFirestore.Firestore; auth: firebaseauth.BaseAuth; - logging: { - log: (payload: any) => void; - warn: (payload: any) => void; - error: (payload: any) => void; - }; + logging: RowyLogging; }; type DefaultValue = (context: DefaultValueContext) => "PLACEHOLDER_OUTPUT_TYPE"; diff --git a/src/components/TableModals/WebhooksModal/Schemas/basic.tsx b/src/components/TableModals/WebhooksModal/Schemas/basic.tsx index 54075d43..9d8e7b8c 100644 --- a/src/components/TableModals/WebhooksModal/Schemas/basic.tsx +++ b/src/components/TableModals/WebhooksModal/Schemas/basic.tsx @@ -30,11 +30,7 @@ export const parserExtraLibs = [ send: (v:any)=>void; sendStatus: (status:number)=>void }; - logging: { - log: (payload: any) => void; - warn: (payload: any) => void; - error: (payload: any) => void; - }; + logging: RowyLogging; } ) => Promise;`, ]; @@ -49,11 +45,7 @@ export const conditionExtraLibs = [ send: (v:any)=>void; sendStatus: (status:number)=>void; }; - logging: { - log: (payload: any) => void; - warn: (payload: any) => void; - error: (payload: any) => void; - }; + logging: RowyLogging; } ) => Promise;`, ]; diff --git a/src/components/TableModals/WebhooksModal/utils.tsx b/src/components/TableModals/WebhooksModal/utils.tsx index b1ba8601..c7a992ef 100644 --- a/src/components/TableModals/WebhooksModal/utils.tsx +++ b/src/components/TableModals/WebhooksModal/utils.tsx @@ -35,11 +35,7 @@ export const parserExtraLibs = [ send: (v:any)=>void; sendStatus: (status:number)=>void }; - logging: { - log: (payload: any) => void; - warn: (payload: any) => void; - error: (payload: any) => void; - }; + logging: RowyLogging; } ) => Promise;`, ]; @@ -54,11 +50,7 @@ export const conditionExtraLibs = [ send: (v:any)=>void sendStatus: (status:number)=>void }; - logging: { - log: (payload: any) => void; - warn: (payload: any) => void; - error: (payload: any) => void; - }; + logging: RowyLogging; } ) => Promise;`, ]; diff --git a/src/components/TableModals/WebhooksModal/webhooks.d.ts b/src/components/TableModals/WebhooksModal/webhooks.d.ts index fa98341f..65df0530 100644 --- a/src/components/TableModals/WebhooksModal/webhooks.d.ts +++ b/src/components/TableModals/WebhooksModal/webhooks.d.ts @@ -3,20 +3,12 @@ type Condition = (args: { db: FirebaseFirestore.Firestore; ref: FirebaseFirestore.CollectionReference; res: Response; - logging: { - log: (payload: any) => void; - warn: (payload: any) => void; - error: (payload: any) => void; - }; + logging: RowyLogging; }) => Promise; type Parser = (args: { req: WebHookRequest; db: FirebaseFirestore.Firestore; ref: FirebaseFirestore.CollectionReference; - logging: { - log: (payload: any) => void; - warn: (payload: any) => void; - error: (payload: any) => void; - }; + logging: RowyLogging; }) => Promise; diff --git a/src/components/fields/Action/action.d.ts b/src/components/fields/Action/action.d.ts index 33fc7489..1daab613 100644 --- a/src/components/fields/Action/action.d.ts +++ b/src/components/fields/Action/action.d.ts @@ -15,11 +15,7 @@ type ActionContext = { auth: firebaseauth.BaseAuth; actionParams: actionParams; user: ActionUser; - logging: { - log: (payload: any) => void; - warn: (payload: any) => void; - error: (payload: any) => void; - }; + logging: RowyLogging; }; type ActionResult = { diff --git a/src/components/fields/Connector/connector.d.ts b/src/components/fields/Connector/connector.d.ts index d12041d4..c16585ae 100644 --- a/src/components/fields/Connector/connector.d.ts +++ b/src/components/fields/Connector/connector.d.ts @@ -15,11 +15,7 @@ type ConnectorContext = { auth: firebaseauth.BaseAuth; query: string; user: ConnectorUser; - logging: { - log: (payload: any) => void; - warn: (payload: any) => void; - error: (payload: any) => void; - }; + logging: RowyLogging; }; type ConnectorResult = any[]; type Connector = ( diff --git a/src/components/fields/Derivative/derivative.d.ts b/src/components/fields/Derivative/derivative.d.ts index 9d01f97c..a56afeba 100644 --- a/src/components/fields/Derivative/derivative.d.ts +++ b/src/components/fields/Derivative/derivative.d.ts @@ -5,11 +5,7 @@ type DerivativeContext = { db: FirebaseFirestore.Firestore; auth: firebaseauth.BaseAuth; change: any; - logging: { - log: (payload: any) => void; - warn: (payload: any) => void; - error: (payload: any) => void; - }; + logging: RowyLogging; }; type Derivative = (context: DerivativeContext) => "PLACEHOLDER_OUTPUT_TYPE"; diff --git a/src/components/fields/types.ts b/src/components/fields/types.ts index 299336f2..63374d81 100644 --- a/src/components/fields/types.ts +++ b/src/components/fields/types.ts @@ -113,9 +113,3 @@ export interface IFilterCustomInputProps { operator: TableFilter["operator"]; [key: string]: any; } - -export interface RowyLogging { - log: (payload: any) => void; - warn: (payload: any) => void; - error: (payload: any) => void; -} From 5fad8022f0dd0e2e7456ddc75de84a8e1ad6cfba Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Fri, 30 Dec 2022 13:39:04 +0000 Subject: [PATCH 23/29] fix: column names are only determined by the first row --- .../TableToolbar/ImportData/ImportFromCsv.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/components/TableToolbar/ImportData/ImportFromCsv.tsx b/src/components/TableToolbar/ImportData/ImportFromCsv.tsx index d16f50bb..3b8e90ce 100644 --- a/src/components/TableToolbar/ImportData/ImportFromCsv.tsx +++ b/src/components/TableToolbar/ImportData/ImportFromCsv.tsx @@ -42,9 +42,14 @@ enum FileType { TSV = "text/tab-separated-values", JSON = "application/json", } + // extract the column names and return the names -function extractFields(data: JSON): string[] { - return _remove(Object.keys(data), (col: string) => col !== "id"); +function extractFields(data: JSON[]): string[] { + let columns = new Set(); + for (let jsonRow of data) { + columns = new Set([...columns, ...Object.keys(jsonRow)]); + } + return _remove([...columns], (col: string) => col !== "id"); } function convertJSONToCSV(rawData: string): string | false { @@ -57,7 +62,7 @@ function convertJSONToCSV(rawData: string): string | false { if (rawDataJSONified.length < 1) { return false; } - const fields = extractFields(rawDataJSONified[0]); + const fields = extractFields(rawDataJSONified); const opts = { fields }; try { @@ -115,7 +120,7 @@ export default function ImportFromFile() { }; }, [setImportCsv]); - const parseFile = (rawData: string) => { + const parseFile = useCallback((rawData: string) => { if (importTypeRef.current === "json") { if (!hasProperJsonStructure(rawData)) { return setError("Invalid Structure! It must be an Array"); @@ -127,7 +132,7 @@ export default function ImportFromFile() { rawData = converted; } parseCsv(rawData); - }; + }, []); const parseCsv = useCallback( (csvString: string) => @@ -145,6 +150,7 @@ export default function ImportFromFile() { {} ) ); + console.log(mappedRows); setImportCsv({ importType: importTypeRef.current, csvData: { columns, rows: mappedRows }, From 58d02c2377084ba75ee649ecd99508ca53e41c96 Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Sat, 31 Dec 2022 00:05:39 +0930 Subject: [PATCH 24/29] add table path filter to logging --- src/components/TableModals/CloudLogsModal/utils.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/TableModals/CloudLogsModal/utils.ts b/src/components/TableModals/CloudLogsModal/utils.ts index 79904ccf..6d210e45 100644 --- a/src/components/TableModals/CloudLogsModal/utils.ts +++ b/src/components/TableModals/CloudLogsModal/utils.ts @@ -26,6 +26,7 @@ export const cloudLogFetcher = ( switch (cloudLogFilters.type) { case "extension": logQuery.push(`logName = "projects/${projectId}/logs/rowy-logging"`); + logQuery.push(`jsonPayload.tablePath : "${tablePath}"`); if (cloudLogFilters?.extension?.length) { logQuery.push( cloudLogFilters.extension @@ -40,6 +41,7 @@ export const cloudLogFetcher = ( break; case "webhook": + logQuery.push(`jsonPayload.tablePath : "${tablePath}"`); if (cloudLogFilters?.webhook?.length) { logQuery.push( cloudLogFilters.webhook @@ -52,6 +54,7 @@ export const cloudLogFetcher = ( break; case "column": + logQuery.push(`jsonPayload.tablePath : "${tablePath}"`); if (cloudLogFilters?.column?.length) { logQuery.push( cloudLogFilters.column From 8c364596d76fdce54e9f9e88f6acccf3c44c200c Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Sat, 31 Dec 2022 05:10:53 +0000 Subject: [PATCH 25/29] ignore id column --- src/components/TableToolbar/ImportData/ImportFromCsv.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/TableToolbar/ImportData/ImportFromCsv.tsx b/src/components/TableToolbar/ImportData/ImportFromCsv.tsx index 3b8e90ce..9739efab 100644 --- a/src/components/TableToolbar/ImportData/ImportFromCsv.tsx +++ b/src/components/TableToolbar/ImportData/ImportFromCsv.tsx @@ -1,6 +1,7 @@ import { useState, useCallback, useRef, useEffect } from "react"; import { useAtom, useSetAtom } from "jotai"; import { parse } from "csv-parse/browser/esm"; +import { parse as parseJSON } from "json2csv"; import { useDropzone } from "react-dropzone"; import { useDebouncedCallback } from "use-debounce"; import { useSnackbar } from "notistack"; @@ -28,9 +29,6 @@ import { } from "@src/atoms/tableScope"; import { analytics, logEvent } from "@src/analytics"; -import { parse as parseJSON } from "json2csv"; -import { remove as _remove } from "lodash-es"; - export enum ImportMethod { paste = "paste", upload = "upload", @@ -49,7 +47,8 @@ function extractFields(data: JSON[]): string[] { for (let jsonRow of data) { columns = new Set([...columns, ...Object.keys(jsonRow)]); } - return _remove([...columns], (col: string) => col !== "id"); + columns.delete("id"); + return [...columns]; } function convertJSONToCSV(rawData: string): string | false { From 5b49964aac1a27f88977e4d56e9ee18789ad4e64 Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Sat, 31 Dec 2022 05:55:10 +0000 Subject: [PATCH 26/29] optimistic updates with useFirestoreDocWithAtom --- src/components/Table/ColumnSelect.tsx | 7 ----- src/components/TableToolbar/HiddenFields.tsx | 30 -------------------- src/hooks/useFirestoreDocWithAtom.ts | 15 +++++++++- 3 files changed, 14 insertions(+), 38 deletions(-) diff --git a/src/components/Table/ColumnSelect.tsx b/src/components/Table/ColumnSelect.tsx index 599b32f1..3ea28155 100644 --- a/src/components/Table/ColumnSelect.tsx +++ b/src/components/Table/ColumnSelect.tsx @@ -22,22 +22,15 @@ export interface IColumnSelectProps { filterColumns?: (column: ColumnConfig) => boolean; showFieldNames?: boolean; options?: ColumnOption[]; - tableColumnsOrdered?: ColumnConfig[]; } export default function ColumnSelect({ filterColumns, showFieldNames, - tableColumnsOrdered: tableColumnsOrdered_, ...props }: IColumnSelectProps & Omit, "options">) { - // if tableColumnsOrdered is passed(in case of HiddenFields) else traditional way to get tableColumnsOrders (backward compitable) let [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope); - if (tableColumnsOrdered_) { - tableColumnsOrdered = tableColumnsOrdered_; - } - const options = props.options || (filterColumns diff --git a/src/components/TableToolbar/HiddenFields.tsx b/src/components/TableToolbar/HiddenFields.tsx index 32f9118a..1e4d2601 100644 --- a/src/components/TableToolbar/HiddenFields.tsx +++ b/src/components/TableToolbar/HiddenFields.tsx @@ -2,7 +2,6 @@ import { useEffect, useRef, useMemo, - useCallback, useState, forwardRef, ChangeEvent, @@ -35,7 +34,6 @@ import { tableScope, tableIdAtom, updateColumnAtom, - tableColumnsOrderedAtom, } from "@src/atoms/tableScope"; import { formatSubTableName } from "@src/utils/table"; @@ -61,10 +59,6 @@ export default function HiddenFields() { setHiddenFields(userDocHiddenFields); }, [userDocHiddenFields]); - const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope); - // cashed tableColumnsOrdered from quick updates(reorder). - const [quickAccessTableColumnsOrdered, setQuickAccessTableColumnsOrdered] = - useState(tableColumnsOrdered); // const tableColumns = tableColumnsOrdered.map(({ key, name }) => ({ // value: key, // label: name, @@ -163,32 +157,10 @@ export default function HiddenFields() { const updateColumn = useSetAtom(updateColumnAtom, tableScope); - // function to revalidate quickAccessTableColumnsOrdered - function revalidateCache() { - setQuickAccessTableColumnsOrdered(tableColumnsOrdered); - } - - // function to reorder columns from cashed tableColumnsOrdered - const optimisticReorder = useCallback( - (from: number, to: number) => { - let temp = Array.from(quickAccessTableColumnsOrdered); - const currentColumn = temp.splice(from, 1)[0]; - temp.splice(to, 0, currentColumn); - - // update indexes - for (let i = from < to ? from : to; i < temp.length; i++) { - temp[i].index = i; - } - setQuickAccessTableColumnsOrdered(temp); - }, - [quickAccessTableColumnsOrdered] - ); - // updates column on drag end function handleOnDragEnd(result: DropResult) { if (!result.destination || result.destination.index === result.source.index) return; - optimisticReorder(result.source.index, result.destination.index); updateColumn({ key: result.draggableId, config: {}, @@ -235,7 +207,6 @@ export default function HiddenFields() { } onClick={() => { - revalidateCache(); setOpen((o) => !o); }} active={hiddenFields.length > 0} @@ -244,7 +215,6 @@ export default function HiddenFields() { {hiddenFields.length > 0 ? `${hiddenFields.length} hidden` : "Hide"} ( dataScope ); const handleError = useErrorHandler(); + const { enqueueSnackbar } = useSnackbar(); // Create the doc ref and memoize using Firestore’s refEqual const memoizedDocRef = useMemoValue( @@ -145,7 +148,17 @@ export function useFirestoreDocWithAtom( } } - return setDoc(memoizedDocRef, updateToDb, { merge: true }); + setDataAtom((prev) => { + return { + ...prev, + ...updateToDb, + }; + }); + return setDoc(memoizedDocRef, updateToDb, { merge: true }).catch( + (e) => { + enqueueSnackbar((e as Error).message, { variant: "error" }); + } + ); }); } From d7a4cdfc74200fd988e92dac7599e5c0c624ce12 Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Sat, 31 Dec 2022 06:15:55 +0000 Subject: [PATCH 27/29] added required dependencies --- src/hooks/useFirestoreDocWithAtom.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/hooks/useFirestoreDocWithAtom.ts b/src/hooks/useFirestoreDocWithAtom.ts index 0ed56899..b66351df 100644 --- a/src/hooks/useFirestoreDocWithAtom.ts +++ b/src/hooks/useFirestoreDocWithAtom.ts @@ -167,7 +167,13 @@ export function useFirestoreDocWithAtom( // reset the atom’s value to prevent writes if (updateDataAtom) setUpdateDataAtom(undefined); }; - }, [memoizedDocRef, updateDataAtom, setUpdateDataAtom]); + }, [ + memoizedDocRef, + updateDataAtom, + setUpdateDataAtom, + enqueueSnackbar, + setDataAtom, + ]); } export default useFirestoreDocWithAtom; From e754b251217c7aa29f246e3a4ff7cb66a085689e Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Sat, 31 Dec 2022 06:28:18 +0000 Subject: [PATCH 28/29] disable drag if user does not have permission --- src/components/TableToolbar/HiddenFields.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/components/TableToolbar/HiddenFields.tsx b/src/components/TableToolbar/HiddenFields.tsx index 1e4d2601..ff1fc86a 100644 --- a/src/components/TableToolbar/HiddenFields.tsx +++ b/src/components/TableToolbar/HiddenFields.tsx @@ -29,6 +29,7 @@ import { projectScope, userSettingsAtom, updateUserSettingsAtom, + userRolesAtom, } from "@src/atoms/projectScope"; import { tableScope, @@ -41,6 +42,9 @@ export default function HiddenFields() { const buttonRef = useRef(null); const [userSettings] = useAtom(userSettingsAtom, projectScope); + const [userRoles] = useAtom(userRolesAtom, projectScope); + const canEditColumns = + userRoles.includes("ADMIN") || userRoles.includes("OPS"); const [tableId] = useAtom(tableIdAtom, tableScope); const [open, setOpen] = useState(false); @@ -76,7 +80,7 @@ export default function HiddenFields() { setOpen(false); }; - // disable drag if search box is not empty + // disable drag if search box is not empty and user does not have permission const [disableDrag, setDisableDrag] = useState(false); const renderOption: AutocompleteProps< any, @@ -92,7 +96,7 @@ export default function HiddenFields() { {(provided) => (
  • @@ -106,7 +110,9 @@ export default function HiddenFields() { { marginRight: "6px", opacity: (theme) => - disableDrag ? theme.palette.action.disabledOpacity : 1, + disableDrag || !canEditColumns + ? theme.palette.action.disabledOpacity + : 1, }, ]} /> @@ -170,7 +176,7 @@ export default function HiddenFields() { // checks whether to disable reordering when search filter is applied function checkToDisableDrag(e: ChangeEvent) { - setDisableDrag(e.target.value !== ""); + setDisableDrag(e.target.value !== "" || !canEditColumns); } const ListboxComponent = forwardRef(function ListboxComponent( From fed06b3e59848f3d97f8d6e5d612c70d63e0168d Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Sat, 31 Dec 2022 09:01:47 +0000 Subject: [PATCH 29/29] let -> const --- src/components/Table/ColumnSelect.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/Table/ColumnSelect.tsx b/src/components/Table/ColumnSelect.tsx index 3ea28155..d5b3fa15 100644 --- a/src/components/Table/ColumnSelect.tsx +++ b/src/components/Table/ColumnSelect.tsx @@ -29,8 +29,7 @@ export default function ColumnSelect({ showFieldNames, ...props }: IColumnSelectProps & Omit, "options">) { - let [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope); - + const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope); const options = props.options || (filterColumns