diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..35aaf69b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: ๐Ÿค” Support & questions + url: https://discord.com/invite/fjBugmvzZP + about: Chat with us for live support on discord. + - name: ๐Ÿ™Œ Want to join our team? + url: https://www.rowy.io/jobs + about: Get in touch to contribute & work with Rowy diff --git a/package.json b/package.json index e8539ccf..1ccd218a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rowy", - "version": "2.3.2", + "version": "2.4.0-rc.0", "homepage": "https://rowy.io", "repository": { "type": "git", diff --git a/src/components/CodeEditor/rowy.d.ts b/src/components/CodeEditor/rowy.d.ts index d3282d8a..de9d706e 100644 --- a/src/components/CodeEditor/rowy.d.ts +++ b/src/components/CodeEditor/rowy.d.ts @@ -1,32 +1,95 @@ -/** - * utility functions - */ -declare namespace rowy { +type RowyFile = { + downloadURL: string; + name: string; + type: string; + lastModifiedTS: number; +}; +type RowyUser = { + email: any; + emailVerified: boolean; + displayName: string; + photoURL: string; + uid: string; + timestamp: number; +}; +type uploadOptions = { + bucket?: string; + folderPath?: string; + fileName?: string; +}; +interface Rowy { + metadata: { + /** + * The project ID of the project running this function. + */ + projectId: () => Promise; + /** + * The numeric project ID of the project running this function. + */ + projectNumber: () => Promise; + /** + * The email address of service account running this function. + * This is the service account that is used to call other APIs. + * Ensure that the service account has the correct permissions. + */ + serviceAccountEmail: () => Promise; + /** + * a user object of the service account running this function. + * Compatible with Rowy audit fields + * Can be used to add createdBy or updatedBy fields to a document. + */ + serviceAccountUser: () => Promise; + }; /** - * uploads a file to the cloud storage from a url + * Gives access to the Secret Manager. + * manage your secrets in the Google Cloud Console. */ - function url2storage( - url: string, - storagePath: string, - fileName?: string - ): Promise<{ - downloadURL: string; - name: string; - type: string; - lastModifiedTS: Date; - }>; - + secrets: { + /** + * Get an existing secret from the secret manager. + */ + get: (name: string, version?: string) => Promise; + }; /** - * Gets the secret defined in Google Cloud Secret + * Gives access to the Cloud Storage. */ - async function getSecret(name: string, v?: string): Promise; - - async function getServiceAccountUser(): Promise<{ - email: string; - emailVerified: boolean; - displayName: string; - photoURL: string; - uid: string; - timestamp: number; - }>; + storage: { + upload: { + /** + * uploads a file to storage bucket from an external url. + */ + url: ( + url: string, + options: uploadOptions + ) => Promise; + /** + * uploads a file to storage bucket from a buffer or string + */ + data: ( + data: Buffer | string, + options: uploadOptions + ) => Promise; + }; + }; + /** + * @deprecated will be removed in version 2.0. + * use rowy.secrets.get instead. + * Get an existing secret from the secret manager. + */ + getSecret: (name: string, version?: string) => Promise; + /** + * @deprecated will be removed in version 2.0. + * use rowy.metadata.serviceAccountUser instead. + * Compatible with Rowy audit fields + * Can be used to add createdBy or updatedBy fields to a document. + */ + getServiceAccountUser: () => Promise; + /** + * @deprecated will be removed in version 2.0. + * use rowy.storage.upload.url instead. + * uploads a file to storage bucket from an external url. + */ + url2storage: (url: string) => Promise; } + +declare const rowy: Rowy; diff --git a/src/components/SideDrawer/Form/index.tsx b/src/components/SideDrawer/Form/index.tsx index 6895fc2b..2236bd7f 100644 --- a/src/components/SideDrawer/Form/index.tsx +++ b/src/components/SideDrawer/Form/index.tsx @@ -2,6 +2,7 @@ import { createElement, useEffect } from "react"; import { useForm } from "react-hook-form"; import _sortBy from "lodash/sortBy"; import _isEmpty from "lodash/isEmpty"; +import _set from "lodash/set"; import createPersistedState from "use-persisted-state"; import { Stack, FormControlLabel, Switch } from "@mui/material"; @@ -44,7 +45,16 @@ export default function Form({ values }: IFormProps) { // Get initial values from fields config. This wonโ€™t be written to the db // when the SideDrawer is opened. Only dirty fields will be written const initialValues = fields.reduce( - (a, { key, type }) => ({ ...a, [key]: getFieldProp("initialValue", type) }), + (a, { key, type }) => { + const initialValue = getFieldProp("initialValue", type); + const nextValues = { ...a }; + if (key.indexOf('.') !== -1) { + _set(nextValues, key, initialValue); + } else { + nextValues[key] = initialValue; + } + return nextValues; + }, {} ); const { ref: docRef, ...rowValues } = values; diff --git a/src/components/TableHeader/Filters/index.tsx b/src/components/TableHeader/Filters/index.tsx index c26f85a7..06849a67 100644 --- a/src/components/TableHeader/Filters/index.tsx +++ b/src/components/TableHeader/Filters/index.tsx @@ -19,6 +19,7 @@ import FiltersPopover from "./FiltersPopover"; import FilterInputs from "./FilterInputs"; import { useFilterInputs, INITIAL_QUERY } from "./useFilterInputs"; +import { analytics } from "@src/analytics"; import type { TableFilter } from "@src/hooks/useTable"; import { useProjectContext } from "@src/contexts/ProjectContext"; import { useAppContext } from "@src/contexts/AppContext"; @@ -30,6 +31,11 @@ const shouldDisableApplyButton = (value: any) => typeof value !== "number" && typeof value !== "object"; +enum FilterType { + yourFilter = "local_filter", + tableFilter = "table_filter", +} + export default function Filters() { const { table, tableState, tableActions } = useProjectContext(); const { userDoc, userClaims } = useAppContext(); @@ -109,12 +115,14 @@ export default function Filters() { // Save table filters to table schema document const setTableFilters = (filters: TableFilter[]) => { + analytics.logEvent(FilterType.tableFilter); tableActions?.table.updateConfig("filters", filters); tableActions?.table.updateConfig("filtersOverridable", canOverrideCheckbox); }; // Save user filters to user document // null overrides table filters const setUserFilters = (filters: TableFilter[] | null) => { + analytics.logEvent(FilterType.yourFilter); userDoc.dispatch({ action: DocActions.update, data: { diff --git a/src/components/TableHeader/ImportCsv.tsx b/src/components/TableHeader/ImportCsv.tsx index 8a26737a..49034dc6 100644 --- a/src/components/TableHeader/ImportCsv.tsx +++ b/src/components/TableHeader/ImportCsv.tsx @@ -1,8 +1,9 @@ -import React, { useState, useCallback } from "react"; +import React, { useState, useCallback, useRef } from "react"; import clsx from "clsx"; import parse from "csv-parse"; import { useDropzone } from "react-dropzone"; import { useDebouncedCallback } from "use-debounce"; +import { useSnackbar } from "notistack"; import { makeStyles, createStyles } from "@mui/styles"; import { @@ -27,6 +28,7 @@ import ImportIcon from "@src/assets/icons/Import"; import FileUploadIcon from "@src/assets/icons/Upload"; import CheckIcon from "@mui/icons-material/CheckCircle"; +import { analytics } from "@src/analytics"; import ImportCsvWizard, { IImportCsvWizardProps, } from "@src/components/Wizards/ImportCsvWizard"; @@ -79,6 +81,17 @@ const useStyles = makeStyles((theme) => }) ); +export enum ImportType { + csv = "csv", + tsv = "tsv", +} + +export enum ImportMethod { + paste = "paste", + upload = "upload", + url = "url", +} + export interface IImportCsvProps { render?: ( onClick: (event: React.MouseEvent) => void @@ -90,7 +103,10 @@ export default function ImportCsv({ render, PopoverProps }: IImportCsvProps) { const classes = useStyles(); const { userClaims } = useAppContext(); const { table } = useProjectContext(); + const { enqueueSnackbar } = useSnackbar(); + const importTypeRef = useRef(ImportType.csv); + const importMethodRef = useRef(ImportMethod.upload); const [open, setOpen] = useState(null); const [tab, setTab] = useState("upload"); const [csvData, setCsvData] = @@ -128,21 +144,53 @@ export default function ImportCsv({ render, PopoverProps }: IImportCsvProps) { }); const onDrop = useCallback(async (acceptedFiles) => { - const file = acceptedFiles[0]; - const reader = new FileReader(); - reader.onload = (event: any) => parseCsv(event.target.result); - reader.readAsText(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" + ? ImportType.tsv + : ImportType.csv; + } catch (error) { + enqueueSnackbar(`Please import a .tsv or .csv file`, { + variant: "error", + anchorOrigin: { + vertical: "top", + horizontal: "center", + }, + }); + } }, []); + const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop, multiple: false, accept: ["text/csv", "text/tab-separated-values"], }); - const [handlePaste] = useDebouncedCallback( - (value: string) => parseCsv(value), - 1000 - ); + function setDataTypeRef(data: string) { + const getFirstLine = data?.match(/^(.*)/)?.[0]; + /** + * Catching edge case with regex + * EG: "hello\tworld"\tFirst + * - find \t between quotes, and replace with '\s' + * - w/ the \t pattern test it against the formatted string + */ + const strInQuotes = /"(.*?)"/; + const tabsWithSpace = (str: string) => str.replace("\t", "s"); + const formatString = + getFirstLine?.replace(strInQuotes, tabsWithSpace) ?? ""; + const tabPattern = /\t/; + return tabPattern.test(formatString) + ? (importTypeRef.current = ImportType.tsv) + : (importTypeRef.current = ImportType.csv); + } + const [handlePaste] = useDebouncedCallback((value: string) => { + parseCsv(value); + setDataTypeRef(value); + }, 1000); const [loading, setLoading] = useState(false); const [handleUrl] = useDebouncedCallback((value: string) => { @@ -152,6 +200,7 @@ export default function ImportCsv({ render, PopoverProps }: IImportCsvProps) { .then((res) => res.text()) .then((data) => { parseCsv(data); + setDataTypeRef(data); setLoading(false); }) .catch((e) => { @@ -204,9 +253,21 @@ export default function ImportCsv({ render, PopoverProps }: IImportCsvProps) { } variant="fullWidth" > - - - + (importMethodRef.current = ImportMethod.upload)} + /> + (importMethodRef.current = ImportMethod.paste)} + /> + (importMethodRef.current = ImportMethod.url)} + /> @@ -295,7 +356,12 @@ export default function ImportCsv({ render, PopoverProps }: IImportCsvProps) { color="primary" disabled={!validCsv} className={classes.continueButton} - onClick={() => setOpenWizard(true)} + onClick={() => { + setOpenWizard(true); + analytics.logEvent(`import_${importMethodRef.current}`, { + type: importTypeRef.current, + }); + }} > Continue @@ -303,6 +369,7 @@ export default function ImportCsv({ render, PopoverProps }: IImportCsvProps) { {openWizard && csvData && ( setOpenWizard(false)} csvData={csvData} /> diff --git a/src/components/TableSettings/ActionsMenu/ExportSettings.tsx b/src/components/TableSettings/ActionsMenu/ExportSettings.tsx index ebd723b2..4027a0d5 100644 --- a/src/components/TableSettings/ActionsMenu/ExportSettings.tsx +++ b/src/components/TableSettings/ActionsMenu/ExportSettings.tsx @@ -6,6 +6,7 @@ import { useSnackbar } from "notistack"; import { MenuItem, DialogContentText, LinearProgress } from "@mui/material"; +import { analytics } from "@src/analytics"; import Modal from "@src/components/Modal"; import CodeEditor from "@src/components/CodeEditor"; @@ -49,6 +50,7 @@ export default function ExportSettings({ const { enqueueSnackbar } = useSnackbar(); const handleExport = () => { + analytics.logEvent("export_tableSettings"); navigator.clipboard.writeText(formattedJson); enqueueSnackbar("Copied to clipboard"); handleClose(); diff --git a/src/components/TableSettings/ActionsMenu/ImportSettings.tsx b/src/components/TableSettings/ActionsMenu/ImportSettings.tsx index 8d98fed2..11cf513d 100644 --- a/src/components/TableSettings/ActionsMenu/ImportSettings.tsx +++ b/src/components/TableSettings/ActionsMenu/ImportSettings.tsx @@ -8,6 +8,7 @@ import { useSnackbar } from "notistack"; import { MenuItem, DialogContentText, FormHelperText } from "@mui/material"; +import { analytics } from "@src/analytics"; import Modal from "@src/components/Modal"; import DiffEditor from "@src/components/CodeEditor/DiffEditor"; @@ -59,8 +60,8 @@ export default function ImportSettings({ const { enqueueSnackbar } = useSnackbar(); const { setValue } = useFormMethods; const handleImport = () => { + analytics.logEvent("import_tableSettings"); const { id, collection, ...newValues } = JSON.parse(newSettings); - for (const key in newValues) { setValue(key, newValues[key], { shouldDirty: true, diff --git a/src/components/Wizards/ImportCsvWizard/index.tsx b/src/components/Wizards/ImportCsvWizard/index.tsx index 940a5459..bf474f83 100644 --- a/src/components/Wizards/ImportCsvWizard/index.tsx +++ b/src/components/Wizards/ImportCsvWizard/index.tsx @@ -21,6 +21,7 @@ import { ColumnConfig } from "@src/hooks/useTable/useTableConfig"; import { useProjectContext } from "@src/contexts/ProjectContext"; import { getFieldProp } from "@src/components/fields"; import { analytics } from "@src/analytics"; +import { ImportType } from "@src/components/TableHeader/ImportCsv"; export type CsvConfig = { pairs: { csvKey: string; columnKey: string }[]; @@ -36,6 +37,7 @@ export interface IStepProps { } export interface IImportCsvWizardProps { + importType: ImportType; handleClose: () => void; csvData: { columns: string[]; @@ -44,6 +46,7 @@ export interface IImportCsvWizardProps { } export default function ImportCsvWizard({ + importType, handleClose, csvData, }: IImportCsvWizardProps) { @@ -96,7 +99,7 @@ export default function ImportCsvWizard({ for (const col of config.newColumns) { tableActions.column.add(col.name, col.type, col); } - analytics.logEvent("import_csv"); + analytics.logEvent("import_success", { type: importType }); //change this import_success // Close wizard setOpen(false); setTimeout(handleClose, 300); diff --git a/src/components/fields/index.tsx b/src/components/fields/index.tsx index 6ac07b56..04ed6d45 100644 --- a/src/components/fields/index.tsx +++ b/src/components/fields/index.tsx @@ -31,7 +31,7 @@ import Json from "./Json"; import Code from "./Code"; import Action from "./Action"; import Derivative from "./Derivative"; -import Aggregate from "./Aggregate"; +// import Aggregate from "./Aggregate"; import CreatedBy from "./CreatedBy"; import UpdatedBy from "./UpdatedBy"; import CreatedAt from "./CreatedAt"; @@ -77,7 +77,7 @@ export const FIELDS: IFieldConfig[] = [ // CLOUD FUNCTION Action, Derivative, - Aggregate, + // Aggregate, Status, // AUDITING CreatedBy, diff --git a/yarn.lock b/yarn.lock index 343692c5..a2b2fb42 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3753,25 +3753,25 @@ acorn-walk@^7.1.1: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== +acorn-walk@^8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" + integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + acorn@^6.4.1: version "6.4.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== -acorn@^7.1.0, acorn@^7.4.0: +acorn@^7.1.0, acorn@^7.1.1, acorn@^7.4.0: version "7.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.1.tgz#e35668de0b402f359de515c5482a1ab9f89a69bf" - integrity sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg== - -acorn@^8.2.4: - version "8.4.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.4.1.tgz#56c36251fc7cabc7096adc18f05afe814321a28c" - integrity sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA== +acorn@^8.2.4, acorn@^8.7.0: + version "8.7.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf" + integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== address@1.1.2, address@^1.0.1: version "1.1.2" @@ -8011,9 +8011,9 @@ fn.name@1.x.x: integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== follow-redirects@^1.0.0: - version "1.14.7" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.7.tgz#2004c02eb9436eee9a21446a6477debf17e81685" - integrity sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ== + version "1.14.8" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc" + integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA== for-in@^1.0.2: version "1.0.2" @@ -14052,9 +14052,9 @@ querystring@^0.2.0: integrity sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg== querystringify@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" - integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== + version "2.2.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" + integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== queue-microtask@^1.2.2: version "1.2.3" @@ -17108,18 +17108,10 @@ url-parse-lax@^3.0.0: dependencies: prepend-http "^2.0.0" -url-parse@^1.4.3: - version "1.4.7" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278" - integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg== - dependencies: - querystringify "^2.1.1" - requires-port "^1.0.0" - -url-parse@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.1.tgz#d5fa9890af8a5e1f274a2c98376510f6425f6e3b" - integrity sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q== +url-parse@^1.4.3, url-parse@^1.5.1: + version "1.5.10" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" + integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== dependencies: querystringify "^2.1.1" requires-port "^1.0.0" @@ -17320,9 +17312,12 @@ vm-browserify@^1.0.1: integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== vm2@^3.9.3: - version "3.9.5" - resolved "https://registry.yarnpkg.com/vm2/-/vm2-3.9.5.tgz#5288044860b4bbace443101fcd3bddb2a0aa2496" - integrity sha512-LuCAHZN75H9tdrAiLFf030oW7nJV5xwNMuk1ymOZwopmuK3d2H4L1Kv4+GFHgarKiLfXXLFU+7LDABHnwOkWng== + version "3.9.7" + resolved "https://registry.yarnpkg.com/vm2/-/vm2-3.9.7.tgz#bb87aa677c97c61e23a6cb6547e44e990517a6f6" + integrity sha512-g/GZ7V0Mlmch3eDVOATvAXr1GsJNg6kQ5PjvYy3HbJMCRn5slNbo/u73Uy7r5yUej1cRa3ZjtoVwcWSQuQ/fow== + dependencies: + acorn "^8.7.0" + acorn-walk "^8.2.0" w3c-hr-time@^1.0.2: version "1.0.2"