From c8279248de90e5cd5c000900d89ca65122dae177 Mon Sep 17 00:00:00 2001 From: Cheikh Gueye Wane Date: Tue, 22 Oct 2024 12:35:19 +0000 Subject: [PATCH] feat(Datasets): dataset explorer v1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(Datasets): dataset explorer * fix(Tabs): render only valid children * feat(Dataset): use router to change tab * feat(Datasets): DatasetLayout component * chore:clean up * feat(Datasets):add datasets tabs in dataset layout * fix(Datasets):regex to match url tabs * feat(Datasets): improve tabs navigation and file explorer * feat(Datasets): improvements and bugs fixes * feat(Datasets): display metadata * chore(Datasets): remove metadata for now * fix(Datasets): add version on TabLink * feat(Datasets): remove parsing methods * fix(analytics): The condition was not correct * wip * chore: Improve the design and implementation of the dataset explorer * chore: Add size to the files of a dataset --------- Co-authored-by: Quentin Gérôme Co-authored-by: Quentin Gérôme --- public/locales/en/messages.json | 10 +- public/locales/fr/messages.json | 22 +- schema.graphql | 111 +++++- src/core/components/Block/BlockContent.tsx | 2 +- src/core/components/Block/BlockSection.tsx | 3 +- src/core/components/DataCard/FormSection.tsx | 129 +++--- .../__snapshots__/DataCard.test.tsx.snap | 22 +- src/core/components/DataGrid/DataGrid.tsx | 13 +- .../DescriptionList/DescriptionList.tsx | 13 +- .../components/DescriptionList/helpers.tsx | 1 + .../ErrorBoundary/ErrorBoundary.tsx | 10 +- src/core/components/Listbox/Listbox.tsx | 2 +- src/core/components/Table.tsx | 18 +- src/core/components/Tabs/Tabs.tsx | 54 ++- src/core/helpers/analytics.ts | 2 +- .../DatasetExplorer.generated.tsx | 35 ++ .../DatasetExplorer/DatasetExplorer.tsx | 128 ++++++ .../features/DatasetExplorer/index.tsx | 3 + .../DatasetVersionFileSample.generated.tsx | 66 ++++ .../DatasetVersionFileSample.tsx | 123 ++++++ .../DatasetVersionFileSample/index.ts | 1 + .../DownloadVersionFile.tsx | 9 +- .../layouts/DatasetLayout.generated.tsx | 44 +++ src/datasets/layouts/DatasetLayout.tsx | 266 +++++++++++++ src/datasets/layouts/index.tsx | 3 + src/graphql/types.ts | 132 ++++++- .../datasets/[datasetSlug].tsx | 367 ------------------ .../datasets/[datasetSlug]/access.tsx | 112 ++++++ .../[datasetSlug]/files/[[...fileId]].tsx | 130 +++++++ .../datasets/[datasetSlug]/index.tsx | 185 +++++++++ src/workspaces/graphql/queries.generated.tsx | 249 +++++++++--- src/workspaces/graphql/queries.graphql | 118 ++++-- .../WorkspaceLayout/WorkspaceLayout.tsx | 3 +- 33 files changed, 1795 insertions(+), 591 deletions(-) create mode 100644 src/datasets/features/DatasetExplorer/DatasetExplorer.generated.tsx create mode 100644 src/datasets/features/DatasetExplorer/DatasetExplorer.tsx create mode 100644 src/datasets/features/DatasetExplorer/index.tsx create mode 100644 src/datasets/features/DatasetVersionFileSample/DatasetVersionFileSample.generated.tsx create mode 100644 src/datasets/features/DatasetVersionFileSample/DatasetVersionFileSample.tsx create mode 100644 src/datasets/features/DatasetVersionFileSample/index.ts create mode 100644 src/datasets/layouts/DatasetLayout.generated.tsx create mode 100644 src/datasets/layouts/DatasetLayout.tsx create mode 100644 src/datasets/layouts/index.tsx delete mode 100644 src/pages/workspaces/[workspaceSlug]/datasets/[datasetSlug].tsx create mode 100644 src/pages/workspaces/[workspaceSlug]/datasets/[datasetSlug]/access.tsx create mode 100644 src/pages/workspaces/[workspaceSlug]/datasets/[datasetSlug]/files/[[...fileId]].tsx create mode 100644 src/pages/workspaces/[workspaceSlug]/datasets/[datasetSlug]/index.tsx diff --git a/public/locales/en/messages.json b/public/locales/en/messages.json index f4fc4087..adfe5255 100644 --- a/public/locales/en/messages.json +++ b/public/locales/en/messages.json @@ -21,7 +21,7 @@ "Accept": "Accept", "Accepted": "Accepted", "Access key ID": "Access key ID", - "Access Management": "Access Management", + "Access management": "Access management", "Access Token": "Access Token", "Account": "Account", "Account settings": "Account settings", @@ -69,6 +69,7 @@ "Check your inbox and type the token you received to disable the two-factor authentication.": "Check your inbox and type the token you received to disable the two-factor authentication.", "Close": "Close", "Code": "Code", + "Columns": "Columns", "Configuration": "Configuration", "Configure & run": "Configure & run", "Configure & Run": "Configure & Run", @@ -104,6 +105,7 @@ "created by {{name}} on <4>": "created by {{name}} on <4>", "Creating ({{progress}}%)": "Creating ({{progress}}%)", "Credentials": "Credentials", + "Current version": "Current version", "Currently disabled": "Currently disabled", "Currently enabled": "Currently enabled", "Custom": "Custom", @@ -176,11 +178,13 @@ "From date": "From date", "From Notebook": "From Notebook", "From OpenHEXA CLI": "From OpenHEXA CLI", + "General": "General", "General settings": "General settings", "Generate": "Generate", "Generate a new URL": "Generate a new URL", "Generate extract": "Generate extract", "Generate new webhook URL": "Generate new webhook URL", + "Generating sample...": "Generating sample...", "Go back to login": "Go back to login", "Go to login page": "Go to login page", "Hide": "Hide", @@ -298,6 +302,7 @@ "Restart all opened notebooks": "Restart all opened notebooks", "Revoke": "Revoke", "Role": "Role", + "Rows in sample": "Rows in sample", "Run": "Run", "Run again": "Run again", "Run of {{label}}": "Run of {{label}}", @@ -305,6 +310,7 @@ "Running": "Running", "Runs": "Runs", "sabrina@bluesquarehub.com": "sabrina@bluesquarehub.com", + "Sample": "Sample", "Save": "Save", "Schedule": "Schedule", "Scheduled": "Scheduled", @@ -385,7 +391,6 @@ "There are no snippets for this type of connection.": "There are no snippets for this type of connection.", "This action cannot be undone.": "This action cannot be undone.", "This action will replace the current password of the workspace database.": "This action will replace the current password of the workspace database.", - "This dataset has no version. Upload a new version using your browser or the SDK to see it here.": "This dataset has no version. Upload a new version using your browser or the SDK to see it here.", "This dataset is already linked to this workspace": "This dataset is already linked to this workspace", "This email address is already taken. Please login instead.": "This email address is already taken. Please login instead.", "This field is required": "This field is required", @@ -455,6 +460,7 @@ "Waiting for messages...": "Waiting for messages...", "We were not able to add this run to your favorites": "We were not able to add this run to your favorites", "We were not able to generate a download url for this file": "We were not able to generate a download url for this file", + "We were not able to generate a sample for this file.": "We were not able to generate a sample for this file.", "We were not able to remove it from your favorites": "We were not able to remove it from your favorites", "We were unable to create a link for this output.": "We were unable to create a link for this output.", "Webhook": "Webhook", diff --git a/public/locales/fr/messages.json b/public/locales/fr/messages.json index 4b16285d..0334403b 100644 --- a/public/locales/fr/messages.json +++ b/public/locales/fr/messages.json @@ -5,7 +5,7 @@ "{{files}} is not a valid file___one": "{{files}} n'est pas un fichier valide", "{{files}} is not a valid file___many": "{{files}} ne sont pas des fichiers valides", "{{files}} is not a valid file___other": "{{files}} n'est pas un fichier valide", - "{{value}}d": "{{value}}d", + "{{value}}d": "{{value}}j", "{{value}}h": "{{value}}h", "{{value}}m": "{{value}}m", "{{value}}s": "{{value}}s", @@ -23,7 +23,7 @@ "Accept": "Accepter", "Accepted": "Accepté", "Access key ID": "ID de la clé d'accès", - "Access Management": "Gestion de l'accès", + "Access management": "Gestion de l'accès", "Access Token": "Jeton d'accès", "Account": "Compte", "Account settings": "Paramètres du compte", @@ -48,14 +48,14 @@ "An unexpected error ocurred.": "Une erreur inattendue s'est produite.", "Anyone with the URL will be able to trigger this pipeline": "Quiconque ayant l'URL pourra déclencher cette pipeline", "Archive": "Archives", - "Archive {{name}}": "Archive {{nom}}", + "Archive {{name}}": "Archive {{name}}", "Are you sure to delete this directory ? It will delete all its content.": "Êtes-vous sûr de vouloir supprimer ce répertoire ? Cela supprimera tout son contenu.", "Are you sure to delete this file ?": "Êtes-vous sûr de vouloir supprimer ce fichier ?", "Are you sure to disable the two-factor authentication for your account?": "Êtes-vous sûr d'avoir désactivé l'authentification à deux facteurs pour votre compte ?", "Are you sure to remove this run from your favorites?": "Êtes-vous sûr de supprimer cette course de vos favoris ?", "Are you sure you want to decline this invitation?": "Êtes-vous sûr de vouloir décliner cette invitation ?", "Are you sure you want to delete pipeline <1>{pipeline.name} ?": "Êtes-vous sûr de vouloir supprimer le pipeline <1>{pipeline.name}?", - "Are you sure you want to delete table \"{{name}}\"?": "Êtes-vous sûr de vouloir supprimer la table \"{{nom}}\" ?", + "Are you sure you want to delete table \"{{name}}\"?": "Êtes-vous sûr de vouloir supprimer la table \"{{name}}\" ?", "Are you sure you want to delete the access to \"{{name}}\" for this workspace?": "Êtes-vous sûr de vouloir supprimer l'accès à \"{{name}}\" pour ce workspace ?", "Are you sure you want to delete the connection \"{{name}}\"?": "Êtes-vous sûr de vouloir supprimer la connexion \"{{name}}\" ?", "Are you sure you want to delete the dataset \"{{name}}\"? It will make it unavailable for all workspaces.": "Êtes-vous sûr de vouloir supprimer le jeu de données \"{{name}}\" ? Cela le rendra indisponible pour tous les workspaces.", @@ -71,6 +71,7 @@ "Check your inbox and type the token you received to disable the two-factor authentication.": "Vérifiez votre boîte de réception et saisissez le jeton que vous avez reçu pour désactiver l'authentification à deux facteurs.", "Close": "Fermer", "Code": "Code", + "Columns": "Colonnes", "Configuration": "Configuration", "Configure & run": "Configurer et exécuter", "Configure & Run": "Configurer et exécuter", @@ -91,7 +92,7 @@ "Create a connection": "Créer une connexion", "Create a dataset": "Créer un jeu de données", "Create a folder": "Créer un dossier", - "Create a new link to '{{name}}'": "Créer un nouveau lien vers '{{nom}}'", + "Create a new link to '{{name}}'": "Créer un nouveau lien vers '{{name}}'", "Create a new run of {{externalId}}": "Créer un nouveau cycle de {{externalId}}", "Create a new workspace": "Créer un nouveau workspace", "Create a workspace": "Créer un workspace", @@ -106,6 +107,7 @@ "created by {{name}} on <4>": "créé par {{name}} le <4>", "Creating ({{progress}}%)": "Création ({{progress}}%)", "Credentials": "Titres de compétences", + "Current version": "Version actuelle", "Currently disabled": "Actuellement désactivé", "Currently enabled": "Actuellement activé", "Custom": "Sur mesure", @@ -178,11 +180,13 @@ "From date": "A partir de la date", "From Notebook": "Du carnet de notes", "From OpenHEXA CLI": "À partir de l'interface de commande OpenHEXA", + "General": "Général", "General settings": "Paramètres généraux", "Generate": "Générer", "Generate a new URL": "Générer une nouvelle URL", "Generate extract": "Générer un extrait", "Generate new webhook URL": "Générer une nouvelle URL de webhook", + "Generating sample...": "Génération de l'échantillon...", "Go back to login": "Retourner à la connexion", "Go to login page": "Accéder à la page de connexion", "Hide": "Cacher", @@ -225,7 +229,7 @@ "Logs": "Journaux", "Logs will appear here on run completion": "Les journaux apparaîtront ici à la fin de l'exécution", "Manual": "Manuel", - "Manual run of {{label}} by {{user}}": "Exécution manuelle de {{label}} par {{utilisateur}}", + "Manual run of {{label}} by {{user}}": "Exécution manuelle de {{label}} par {{user}}", "Mark this run as favorite": "Marquer cette course comme favorite", "Marking this run as favorite will put it on top of the list of the runs. Please enter a label that better describes it.": "Marquer cette course comme favorite la placera en haut de la liste des courses. Veuillez saisir une étiquette qui la décrit mieux.", "Maximum 40 characters": "Maximum 40 caractères", @@ -300,6 +304,7 @@ "Restart all opened notebooks": "Redémarrer tous les notebooks ouverts", "Revoke": "Révoquer", "Role": "Rôle", + "Rows in sample": "Lignes dans l'échantillon", "Run": "Exécuter", "Run again": "Recommencer", "Run of {{label}}": "Exécution de {{label}}", @@ -307,6 +312,7 @@ "Running": "En cours d'exécution", "Runs": "Exécutions", "sabrina@bluesquarehub.com": "sabrina@bluesquarehub.com", + "Sample": "Échantillon", "Save": "Sauvegarder", "Schedule": "Calendrier", "Scheduled": "Prévu", @@ -387,7 +393,6 @@ "There are no snippets for this type of connection.": "Il n'y a pas de snippets pour ce type de connexion.", "This action cannot be undone.": "Cette action ne peut être annulée.", "This action will replace the current password of the workspace database.": "Cette action remplacera le mot de passe actuel de la base de données du workspace.", - "This dataset has no version. Upload a new version using your browser or the SDK to see it here.": "Ce jeu de données n'a pas de version. Téléchargez une nouvelle version en utilisant votre navigateur ou le SDK pour la voir ici.", "This dataset is already linked to this workspace": "Ce jeu de données est déjà lié à ce workspace", "This email address is already taken. Please login instead.": "Cette adresse e-mail est déjà prise. Veuillez vous connecter à la place.", "This field is required": "Ce champ est obligatoire", @@ -457,6 +462,7 @@ "Waiting for messages...": "En attente de messages...", "We were not able to add this run to your favorites": "Nous n'avons pas pu ajouter cette course à vos favoris.", "We were not able to generate a download url for this file": "Nous n'avons pas été en mesure de générer un lien de téléchargement pour ce fichier.", + "We were not able to generate a sample for this file.": "Nous n'avons pas pu générer un échantillon pour ce fichier.", "We were not able to remove it from your favorites": "Nous n'avons pas pu le supprimer de vos favoris.", "We were unable to create a link for this output.": "Nous n'avons pas pu créer de lien pour cette sortie.", "Webhook": "Webhook", @@ -486,4 +492,4 @@ "You're about to remove {{name}} from this workspace.": "Vous êtes sur le point de supprimer {{name}} de ce workspace.", "Your account": "Votre compte", "Your workspaces": "Vos workspaces" -} \ No newline at end of file +} diff --git a/schema.graphql b/schema.graphql index d3c5cad1..5327e3ae 100644 --- a/schema.graphql +++ b/schema.graphql @@ -708,6 +708,25 @@ type CreateMembershipResult { errors: [CreateMembershipError!]! } +"""Errors that can occur when creating an attribute.""" +enum CreateMetadataAttributeError { + PERMISSION_DENIED + TARGET_NOT_FOUND + DUPLICATE_KEY +} + +"""Input to add a custom attribute, empty field for value is accepted""" +input CreateMetadataAttributeInput { + targetId: OpaqueID! + key: String! + value: JSON +} + +type CreateMetadataAttributeResult { + success: Boolean! + errors: [CreateMetadataAttributeError!]! +} + """Represents the input for creating a pipeline.""" input CreatePipelineInput { code: String! @@ -927,7 +946,7 @@ type DatabaseTablePage { """ Dataset is a collection of files that are related to each other and are versioned. """ -type Dataset { +type Dataset implements MetadataObject { id: ID! slug: String! name: String! @@ -941,12 +960,15 @@ type Dataset { version(id: ID!): DatasetVersion latestVersion: DatasetVersion links(page: Int = 1, perPage: Int = 15): DatasetLinkPage! + attributes: [MetadataAttribute!]! + targetId: OpaqueID! } -"""Metadata for dataset file""" -type DatasetFileMetadata { - sample: JSON! - status: FileMetadataStatus! +"""File sample for dataset file""" +type DatasetFileSample { + sample: JSON + status: FileSampleStatus! + statusReason: String } """A link of a dataset with a workspace.""" @@ -1000,7 +1022,7 @@ type DatasetPermissions { """ A version of a dataset. A version is a snapshot of the dataset at a point in time. """ -type DatasetVersion { +type DatasetVersion implements MetadataObject { id: ID! name: String! description: String @@ -1010,17 +1032,24 @@ type DatasetVersion { permissions: DatasetVersionPermissions! fileByName(name: String!): DatasetVersionFile files(page: Int = 1, perPage: Int = 15): DatasetVersionFilePage! + attributes: [MetadataAttribute!]! + targetId: OpaqueID! } """A file in a dataset version.""" -type DatasetVersionFile { +type DatasetVersionFile implements MetadataObject { id: ID! uri: String! filename: String! createdAt: DateTime! createdBy: User contentType: String! - fileMetadata: DatasetFileMetadata + size: BigInt! + fileSample: DatasetFileSample + properties: JSON + attributes: [MetadataAttribute!]! + targetId: OpaqueID! + downloadUrl(attachment: Boolean): String } """A page of dataset version files.""" @@ -1250,6 +1279,24 @@ type DeleteMembershipResult { errors: [DeleteMembershipError!]! } +"""Errors that can occur when deleting an attribute.""" +enum DeleteMetadataAttributeError { + PERMISSION_DENIED + TARGET_NOT_FOUND + METADATA_ATTRIBUTE_NOT_FOUND +} + +"""Input to delete custom attribute""" +input DeleteMetadataAttributeInput { + targetId: OpaqueID! + key: String! +} + +type DeleteMetadataAttributeResult { + success: Boolean! + errors: [DeleteMetadataAttributeError!]! +} + """Represents the input for deleting a pipeline.""" input DeletePipelineInput { id: UUID! @@ -1439,6 +1486,24 @@ type DisableTwoFactorResult { errors: [DisableTwoFactorError!] } +"""Errors that can occur when editing an attribute.""" +enum EditMetadataAttributeError { + PERMISSION_DENIED + TARGET_NOT_FOUND +} + +"""Input to edit a custom attribute, empty field for value is accepted""" +input EditMetadataAttributeInput { + targetId: OpaqueID! + key: String! + value: JSON +} + +type EditMetadataAttributeResult { + success: Boolean! + errors: [EditMetadataAttributeError!]! +} + """ The EnableTwoFactorError enum represents the possible errors that can occur during the enableTwoFactor mutation. """ @@ -1472,8 +1537,8 @@ type FeatureFlag { config: JSON! } -"""Statuses that can occur when generating file metadata""" -enum FileMetadataStatus { +"""Statuses that can occur when generating file sample""" +enum FileSampleStatus { PROCESSING FAILED FINISHED @@ -1827,6 +1892,20 @@ enum MessagePriority { CRITICAL } +"""Generic metadata attribute""" +type MetadataAttribute { + id: UUID! + key: String! + value: JSON + system: Boolean! +} + +"""Interface for type implementing metadata""" +interface MetadataObject { + targetId: OpaqueID! + attributes: [MetadataAttribute!]! +} + scalar MovingSpeeds type Mutation { @@ -1950,6 +2029,15 @@ type Mutation { updateConnection(input: UpdateConnectionInput!): UpdateConnectionResult! deleteConnection(input: DeleteConnectionInput!): DeleteConnectionResult! + """Add a custom metadata attribute to an object instance""" + addMetadataAttribute(input: CreateMetadataAttributeInput!): CreateMetadataAttributeResult! + + """Delete an metadata attribute from an object instance""" + deleteMetadataAttribute(input: DeleteMetadataAttributeInput!): DeleteMetadataAttributeResult! + + """Edit metadata attribute for an object instance""" + editMetadataAttribute(input: EditMetadataAttributeInput!): EditMetadataAttributeResult! + """Generates a new password for a database.""" generateNewDatabasePassword(input: GenerateNewDatabasePasswordInput!): GenerateNewDatabasePasswordResult! @@ -2005,6 +2093,8 @@ type NotebookServer { ready: Boolean! } +scalar OpaqueID + """The direction in which to order a list of items.""" enum OrderByDirection { ASC @@ -2454,6 +2544,7 @@ type Query { databaseTable(id: String!): DatabaseTable connection(id: UUID!): Connection connectionBySlug(workspaceSlug: String!, connectionSlug: String!): Connection + metadataAttributes(targetId: OpaqueID!): [MetadataAttribute]! """Retrieves the configuration of the system.""" config: Config! diff --git a/src/core/components/Block/BlockContent.tsx b/src/core/components/Block/BlockContent.tsx index 6a03636c..a5cb2b2f 100644 --- a/src/core/components/Block/BlockContent.tsx +++ b/src/core/components/Block/BlockContent.tsx @@ -8,7 +8,7 @@ const BlockContent = ({ }: HTMLAttributes & { title?: ReactNode | string }) => { return (
- {title &&

{title}

} + {title &&

{title}

}
{children}
); diff --git a/src/core/components/Block/BlockSection.tsx b/src/core/components/Block/BlockSection.tsx index e0d19fcb..e7ed0736 100644 --- a/src/core/components/Block/BlockSection.tsx +++ b/src/core/components/Block/BlockSection.tsx @@ -16,7 +16,7 @@ type BlockSectionProps = { | ReactNode; defaultOpen?: boolean; loading?: boolean; - title?: string | (({ open }: { open: boolean }) => ReactElement); + title?: string | (({ open }: { open: boolean }) => ReactElement | null); }; function BlockSection(props: BlockSectionProps) { @@ -51,7 +51,6 @@ function BlockSection(props: BlockSectionProps) { ); - return collapsible ? ( {header} diff --git a/src/core/components/DataCard/FormSection.tsx b/src/core/components/DataCard/FormSection.tsx index 809912aa..b6260c52 100644 --- a/src/core/components/DataCard/FormSection.tsx +++ b/src/core/components/DataCard/FormSection.tsx @@ -20,6 +20,7 @@ import DisableClickPropagation from "../DisableClickPropagation"; import Spinner from "../Spinner"; import { DataCardSectionContext } from "./context"; import { Property, PropertyDefinition, PropertyFlag } from "./types"; +import clsx from "clsx"; export type OnSaveFn = ( values: { [key: string]: any }, @@ -170,72 +171,88 @@ function FormSection( }, }; + const renderEditButton = () => { + if (!onSave || isEdited) { + return null; + } + return ( + + + + ); + }; + + const renderTitle = ({ open }: { open: boolean }) => ( + <> + {typeof title === "string" ? ( +

{title}

+ ) : ( + title + )} + {open && renderEditButton()} +
+ {collapsible && ( + + )} +
+ + ); + return ( ( + className={clsx("relative", className)} + title={title && renderTitle} + > + {() => ( <> - {typeof title === "string" ? ( -

{title}

- ) : ( - title - )} - {onSave && open && !isEdited && ( - - - + {!title && ( +
{renderEditButton()}
)} -
- {collapsible && ( - - )} -
- - )} - > - {() => - isEdited ? ( -
+ {isEdited ? ( + + + {children} + + + {form.submitError && ( +

+ {form.submitError} +

+ )} +
+ + +
+
+ ) : ( {children} - - {form.submitError && ( -

- {form.submitError} -

- )} -
- - -
- - ) : ( - - {children} - - ) - } + )} + + )}
); diff --git a/src/core/components/DataCard/__tests__/__snapshots__/DataCard.test.tsx.snap b/src/core/components/DataCard/__tests__/__snapshots__/DataCard.test.tsx.snap index ba87a1bc..5368a67d 100644 --- a/src/core/components/DataCard/__tests__/__snapshots__/DataCard.test.tsx.snap +++ b/src/core/components/DataCard/__tests__/__snapshots__/DataCard.test.tsx.snap @@ -15,24 +15,18 @@ exports[`DataCard renders 1`] = `
-
-
-
-
+
+
diff --git a/src/core/components/DataGrid/DataGrid.tsx b/src/core/components/DataGrid/DataGrid.tsx index c3cba5e1..019bd1e9 100644 --- a/src/core/components/DataGrid/DataGrid.tsx +++ b/src/core/components/DataGrid/DataGrid.tsx @@ -28,7 +28,14 @@ import { useTable, } from "react-table"; import Pagination from "../Pagination"; -import { Table, TableBody, TableCell, TableHead, TableRow } from "../Table"; +import { + Table, + TableBody, + TableCell, + TableCellProps, + TableHead, + TableRow, +} from "../Table"; import { BaseColumnProps } from "./BaseColumn"; import { CellContextProvider } from "./helpers"; import Overflow from "../Overflow"; @@ -67,6 +74,7 @@ interface IDataGridProps { defaultSortBy?: SortingRule[]; pageSizeOptions?: number[]; rowClassName?: string; + spacing?: TableCellProps["spacing"]; } type DataGridProps = IDataGridProps; @@ -91,6 +99,7 @@ function DataGrid(props: DataGridProps) { defaultSortBy = [], defaultPageSize = 10, defaultPageIndex = 0, + spacing, } = props; const [loading, setLoading] = useState(false); @@ -266,6 +275,7 @@ function DataGrid(props: DataGridProps) { heading className={column.headerClassName} {...column.getHeaderProps(column.getSortByToggleProps())} + spacing={spacing} > {column.hideLabel ? ( {column.render("Header")} @@ -308,6 +318,7 @@ function DataGrid(props: DataGridProps) { {...cell.getCellProps({ className: cell.column.className, })} + spacing={spacing} > {cell.render("Cell")} diff --git a/src/core/components/DescriptionList/DescriptionList.tsx b/src/core/components/DescriptionList/DescriptionList.tsx index 4c720126..3adae1c3 100644 --- a/src/core/components/DescriptionList/DescriptionList.tsx +++ b/src/core/components/DescriptionList/DescriptionList.tsx @@ -14,17 +14,26 @@ const COLUMNS = { export type DescriptionListProps = React.HTMLAttributes & { displayMode?: DescriptionListDisplayMode; columns?: keyof typeof COLUMNS; + compact?: boolean; }; const DescriptionList = ({ children, className, columns = 1, + compact = false, displayMode = DescriptionListDisplayMode.LABEL_LEFT, }: DescriptionListProps) => { return ( - -
+ +
{children}
diff --git a/src/core/components/DescriptionList/helpers.tsx b/src/core/components/DescriptionList/helpers.tsx index a430548e..41df0e56 100644 --- a/src/core/components/DescriptionList/helpers.tsx +++ b/src/core/components/DescriptionList/helpers.tsx @@ -7,6 +7,7 @@ export enum DescriptionListDisplayMode { export type DescriptionListContext = { displayMode: DescriptionListDisplayMode; + compact: boolean; }; export const ctx = createContext(null); diff --git a/src/core/components/ErrorBoundary/ErrorBoundary.tsx b/src/core/components/ErrorBoundary/ErrorBoundary.tsx index 9ab2448d..5ed87cdf 100644 --- a/src/core/components/ErrorBoundary/ErrorBoundary.tsx +++ b/src/core/components/ErrorBoundary/ErrorBoundary.tsx @@ -1,14 +1,22 @@ import { ErrorBoundary as SentryErrorBoundary } from "@sentry/nextjs"; +import clsx from "clsx"; export default function ErrorBoundary({ children, + fullScreen = true, }: { children: React.ReactNode; + fullScreen?: boolean; }) { return ( +

Something went wrong.

diff --git a/src/core/components/Listbox/Listbox.tsx b/src/core/components/Listbox/Listbox.tsx index 23403ab1..0066daeb 100644 --- a/src/core/components/Listbox/Listbox.tsx +++ b/src/core/components/Listbox/Listbox.tsx @@ -81,7 +81,7 @@ const Listbox = (props: ListboxProps) => { ) => ( ); -export const TableCell = ( - props: HTMLAttributes & { - width?: string; - heading?: boolean; - wrap?: boolean; - spacing?: "tight" | "loose"; - overrideStyle?: boolean; - }, -) => { +export type TableCellProps = HTMLAttributes & { + width?: string; + heading?: boolean; + wrap?: boolean; + spacing?: "tight" | "loose"; + overrideStyle?: boolean; +}; + +export const TableCell = (props: TableCellProps) => { const { heading = false, wrap = false, diff --git a/src/core/components/Tabs/Tabs.tsx b/src/core/components/Tabs/Tabs.tsx index a39088b2..d2c6cbbf 100644 --- a/src/core/components/Tabs/Tabs.tsx +++ b/src/core/components/Tabs/Tabs.tsx @@ -6,6 +6,7 @@ import React, { isValidElement, ReactElement, ReactNode, + useMemo, } from "react"; export type TabsProps = { @@ -18,9 +19,22 @@ export type TabsProps = { const Tabs = (props: TabsProps) => { const { children, defaultIndex = 0, onChange, className } = props; const { t } = useTranslation(); + + const validChildren: React.ReactNode[] = useMemo( + () => + React.Children.toArray(children).filter((child: React.ReactNode) => + isValidElement(child), + ), + [children], + ); + return ( // HeadlessTab.Group is a wrapper for the tabs and panels. To not break Sentry, we need to add the as="div" prop. - + { )} >

- {React.Children.map(children, (child) => ( + {React.Children.map(validChildren, (child) => ( {child} ))} diff --git a/src/core/helpers/analytics.ts b/src/core/helpers/analytics.ts index 73d3c23a..9959d39f 100644 --- a/src/core/helpers/analytics.ts +++ b/src/core/helpers/analytics.ts @@ -12,7 +12,7 @@ async function sendEvent( properties: TrackEventProperties, headers?: Headers, ): Promise { - if (publicRuntimeConfig.DISABLE_ANALYTICS === "true") { + if (publicRuntimeConfig.DISABLE_ANALYTICS) { return; } const res = await fetch( diff --git a/src/datasets/features/DatasetExplorer/DatasetExplorer.generated.tsx b/src/datasets/features/DatasetExplorer/DatasetExplorer.generated.tsx new file mode 100644 index 00000000..443db562 --- /dev/null +++ b/src/datasets/features/DatasetExplorer/DatasetExplorer.generated.tsx @@ -0,0 +1,35 @@ +import * as Types from '../../../graphql/types'; + +import { gql } from '@apollo/client'; +import { DownloadVersionFile_FileFragmentDoc } from '../DownloadVersionFile/DownloadVersionFile.generated'; +import { DatasetVersionFileSample_FileFragmentDoc } from '../DatasetVersionFileSample/DatasetVersionFileSample.generated'; +export type DatasetExplorer_FileFragment = { __typename?: 'DatasetVersionFile', id: string, filename: string, createdAt: any, contentType: string, size: any, uri: string, createdBy?: { __typename?: 'User', displayName: string } | null }; + +export type DatasetExplorer_VersionFragment = { __typename?: 'DatasetVersion', id: string, files: { __typename?: 'DatasetVersionFilePage', items: Array<{ __typename?: 'DatasetVersionFile', id: string, filename: string, createdAt: any, contentType: string, size: any, uri: string, createdBy?: { __typename?: 'User', displayName: string } | null }> } }; + +export const DatasetExplorer_FileFragmentDoc = gql` + fragment DatasetExplorer_file on DatasetVersionFile { + id + filename + createdAt + createdBy { + displayName + } + ...DownloadVersionFile_file + ...DatasetVersionFileSample_file + contentType + size + uri +} + ${DownloadVersionFile_FileFragmentDoc} +${DatasetVersionFileSample_FileFragmentDoc}`; +export const DatasetExplorer_VersionFragmentDoc = gql` + fragment DatasetExplorer_version on DatasetVersion { + id + files { + items { + ...DatasetExplorer_file + } + } +} + ${DatasetExplorer_FileFragmentDoc}`; \ No newline at end of file diff --git a/src/datasets/features/DatasetExplorer/DatasetExplorer.tsx b/src/datasets/features/DatasetExplorer/DatasetExplorer.tsx new file mode 100644 index 00000000..a003c220 --- /dev/null +++ b/src/datasets/features/DatasetExplorer/DatasetExplorer.tsx @@ -0,0 +1,128 @@ +import { gql } from "@apollo/client"; +import clsx from "clsx"; +import DescriptionList from "core/components/DescriptionList"; +import Overflow from "core/components/Overflow"; +import Tabs from "core/components/Tabs"; +import Time from "core/components/Time"; +import Title from "core/components/Title"; +import { useTranslation } from "react-i18next"; +import DownloadVersionFile from "../DownloadVersionFile"; +import { + DatasetExplorer_FileFragment, + DatasetExplorer_VersionFragment, +} from "./DatasetExplorer.generated"; +import DatasetVersionFileSample from "../DatasetVersionFileSample"; +import { DocumentIcon } from "@heroicons/react/24/outline"; +import Filesize from "core/components/Filesize"; + +type DatasetExplorerProps = { + version: DatasetExplorer_VersionFragment; + currentFile?: DatasetExplorer_FileFragment | null; + onClickFile: (file: DatasetExplorer_FileFragment) => void; +}; + +const DatasetExplorer = ({ + version, + currentFile, + onClickFile, +}: DatasetExplorerProps) => { + const { t } = useTranslation(); + + return ( +
+ +
    + {version.files.items.map((file) => ( +
  • onClickFile(file)} + title={file.filename} + className={clsx( + "pl-6 pr-3 py-2 text-xs font-mono tracking-tighter hover:bg-gray-100 hover:text-gray-900 cursor-pointer truncate text-ellipsis max-w-[50ch]", + currentFile?.id === file.id && + "bg-gray-100 text-gray-800 font-semibold", + )} + > + {file.filename} +
  • + ))} +
+
+
+ {currentFile && ( +
+ + <span className="font-mono tracking-tighter"> + {currentFile.filename} + </span> + <DownloadVersionFile + file={currentFile} + variant="secondary" + size="sm" + /> + + + + + + {currentFile.createdBy?.displayName ?? "-"} + + + + {currentFile.contentType} + + + + + + + + + + + + + + +
+ )} +
+
+ ); +}; + +DatasetExplorer.fragments = { + file: gql` + fragment DatasetExplorer_file on DatasetVersionFile { + id + filename + createdAt + createdBy { + displayName + } + ...DownloadVersionFile_file + ...DatasetVersionFileSample_file + contentType + size + uri + } + ${DownloadVersionFile.fragments.file} + ${DatasetVersionFileSample.fragments.file} + `, + version: gql` + fragment DatasetExplorer_version on DatasetVersion { + id + files { + items { + ...DatasetExplorer_file + } + } + } + `, +}; + +export default DatasetExplorer; diff --git a/src/datasets/features/DatasetExplorer/index.tsx b/src/datasets/features/DatasetExplorer/index.tsx new file mode 100644 index 00000000..65913294 --- /dev/null +++ b/src/datasets/features/DatasetExplorer/index.tsx @@ -0,0 +1,3 @@ +import DatasetExplorer from "./DatasetExplorer"; + +export default DatasetExplorer; diff --git a/src/datasets/features/DatasetVersionFileSample/DatasetVersionFileSample.generated.tsx b/src/datasets/features/DatasetVersionFileSample/DatasetVersionFileSample.generated.tsx new file mode 100644 index 00000000..70cd87c4 --- /dev/null +++ b/src/datasets/features/DatasetVersionFileSample/DatasetVersionFileSample.generated.tsx @@ -0,0 +1,66 @@ +import * as Types from '../../../graphql/types'; + +import { gql } from '@apollo/client'; +import * as Apollo from '@apollo/client'; +const defaultOptions = {} as const; +export type GetDatasetVersionFileSampleQueryVariables = Types.Exact<{ + id: Types.Scalars['ID']['input']; +}>; + + +export type GetDatasetVersionFileSampleQuery = { __typename?: 'Query', datasetVersionFile?: { __typename?: 'DatasetVersionFile', id: string, properties?: any | null, fileSample?: { __typename?: 'DatasetFileSample', sample?: any | null, status: Types.FileSampleStatus, statusReason?: string | null } | null } | null }; + +export type DatasetVersionFileSample_FileFragment = { __typename?: 'DatasetVersionFile', id: string, contentType: string }; + +export const DatasetVersionFileSample_FileFragmentDoc = gql` + fragment DatasetVersionFileSample_file on DatasetVersionFile { + id + contentType +} + `; +export const GetDatasetVersionFileSampleDocument = gql` + query GetDatasetVersionFileSample($id: ID!) { + datasetVersionFile(id: $id) { + id + properties + fileSample { + sample + status + statusReason + } + } +} + `; + +/** + * __useGetDatasetVersionFileSampleQuery__ + * + * To run a query within a React component, call `useGetDatasetVersionFileSampleQuery` and pass it any options that fit your needs. + * When your component renders, `useGetDatasetVersionFileSampleQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetDatasetVersionFileSampleQuery({ + * variables: { + * id: // value for 'id' + * }, + * }); + */ +export function useGetDatasetVersionFileSampleQuery(baseOptions: Apollo.QueryHookOptions & ({ variables: GetDatasetVersionFileSampleQueryVariables; skip?: boolean; } | { skip: boolean; }) ) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(GetDatasetVersionFileSampleDocument, options); + } +export function useGetDatasetVersionFileSampleLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(GetDatasetVersionFileSampleDocument, options); + } +export function useGetDatasetVersionFileSampleSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions) { + const options = baseOptions === Apollo.skipToken ? baseOptions : {...defaultOptions, ...baseOptions} + return Apollo.useSuspenseQuery(GetDatasetVersionFileSampleDocument, options); + } +export type GetDatasetVersionFileSampleQueryHookResult = ReturnType; +export type GetDatasetVersionFileSampleLazyQueryHookResult = ReturnType; +export type GetDatasetVersionFileSampleSuspenseQueryHookResult = ReturnType; +export type GetDatasetVersionFileSampleQueryResult = Apollo.QueryResult; \ No newline at end of file diff --git a/src/datasets/features/DatasetVersionFileSample/DatasetVersionFileSample.tsx b/src/datasets/features/DatasetVersionFileSample/DatasetVersionFileSample.tsx new file mode 100644 index 00000000..fc1ea2c0 --- /dev/null +++ b/src/datasets/features/DatasetVersionFileSample/DatasetVersionFileSample.tsx @@ -0,0 +1,123 @@ +import { gql, useQuery } from "@apollo/client"; +import Spinner from "core/components/Spinner"; +import { ApolloComponent } from "core/helpers/types"; +import { useTranslation } from "react-i18next"; +import { DatasetVersionFileSample_FileFragment } from "./DatasetVersionFileSample.generated"; +import DataGrid, { BaseColumn } from "core/components/DataGrid"; +import { TextColumn } from "core/components/DataGrid/TextColumn"; +import { useMemo } from "react"; +import DescriptionList from "core/components/DescriptionList"; + +interface DatasetVersionFileSampleProps { + file: DatasetVersionFileSample_FileFragment; +} + +const GET_DATASET_VERSION_FILE_SAMPLE = gql` + query GetDatasetVersionFileSample($id: ID!) { + datasetVersionFile(id: $id) { + id + properties + fileSample { + sample + status + statusReason + } + } + } +`; + +export const DatasetVersionFileSample: ApolloComponent< + DatasetVersionFileSampleProps +> = (props) => { + const { t } = useTranslation(); + const { data, loading } = useQuery(GET_DATASET_VERSION_FILE_SAMPLE, { + variables: { + id: props.file.id, + }, + }); + + const { sample, columns, status } = useMemo(() => { + if (data?.datasetVersionFile?.fileSample?.status === "FINISHED") { + const sample = data.datasetVersionFile.fileSample.sample; + return { + sample, + columns: sample.length > 0 ? Object.keys(sample[0]) : [], + status: "FINISHED", + }; + } else if ( + !data?.datasetVersionFile?.fileSample || + data?.datasetVersionFile?.fileSample?.status === "PROCESSING" + ) { + return { + sample: [], + columns: [], + status: "PROCESSING", + }; + } + return { + sample: [], + columns: [], + status: "ERROR", + }; + }, [data]); + + if (loading) + return ( +
+ +
+ ); + + if (status === "ERROR") { + return ( +
+ {t("We were not able to generate a sample for this file.")} +
+ ); + } else if (status === "PROCESSING") { + return ( +
+ {t("Generating sample...")} +
+ ); + } else if (status === "FINISHED") { + return ( +
+ + + + {columns.length} + + + {sample.length} + + + + + + {columns.map((col) => ( + + ))} + +
+ ); + } else { + return null; + } +}; + +DatasetVersionFileSample.fragments = { + file: gql` + fragment DatasetVersionFileSample_file on DatasetVersionFile { + id + contentType + } + `, +}; + +export default DatasetVersionFileSample; diff --git a/src/datasets/features/DatasetVersionFileSample/index.ts b/src/datasets/features/DatasetVersionFileSample/index.ts new file mode 100644 index 00000000..9b1f9097 --- /dev/null +++ b/src/datasets/features/DatasetVersionFileSample/index.ts @@ -0,0 +1 @@ +export { default } from "./DatasetVersionFileSample"; diff --git a/src/datasets/features/DownloadVersionFile/DownloadVersionFile.tsx b/src/datasets/features/DownloadVersionFile/DownloadVersionFile.tsx index bada85a0..1f9468d0 100644 --- a/src/datasets/features/DownloadVersionFile/DownloadVersionFile.tsx +++ b/src/datasets/features/DownloadVersionFile/DownloadVersionFile.tsx @@ -12,6 +12,7 @@ import { DownloadVersionFile_FileFragment, } from "./DownloadVersionFile.generated"; import { PrepareVersionFileDownloadError } from "graphql/types"; +import { ArrowDownTrayIcon } from "@heroicons/react/24/outline"; type DownloadVersionFileProps = { children?({ @@ -81,8 +82,12 @@ const DownloadVersionFile = (props: DownloadVersionFileProps) => { return ( ); }; diff --git a/src/datasets/layouts/DatasetLayout.generated.tsx b/src/datasets/layouts/DatasetLayout.generated.tsx new file mode 100644 index 00000000..3e72825e --- /dev/null +++ b/src/datasets/layouts/DatasetLayout.generated.tsx @@ -0,0 +1,44 @@ +import * as Types from '../../graphql/types'; + +import { gql } from '@apollo/client'; +import { WorkspaceLayout_WorkspaceFragmentDoc } from '../../workspaces/layouts/WorkspaceLayout/WorkspaceLayout.generated'; +import { UploadDatasetVersionDialog_DatasetLinkFragmentDoc } from '../features/UploadDatasetVersionDialog/UploadDatasetVersionDialog.generated'; +import { PinDatasetButton_LinkFragmentDoc } from '../features/PinDatasetButton/PinDatasetButton.generated'; +import { DatasetVersionPicker_VersionFragmentDoc } from '../features/DatasetVersionPicker/DatasetVersionPicker.generated'; +export type DatasetLayout_WorkspaceFragment = { __typename?: 'Workspace', name: string, slug: string, permissions: { __typename?: 'WorkspacePermissions', manageMembers: boolean, update: boolean, launchNotebookServer: boolean }, countries: Array<{ __typename?: 'Country', flag: string, code: string }> }; + +export type DatasetLayout_DatasetLinkFragment = { __typename?: 'DatasetLink', id: string, isPinned: boolean, dataset: { __typename?: 'Dataset', slug: string, id: string, name: string, workspace?: { __typename?: 'Workspace', slug: string } | null, permissions: { __typename?: 'DatasetPermissions', delete: boolean, createVersion: boolean } }, workspace: { __typename?: 'Workspace', slug: string }, permissions: { __typename?: 'DatasetLinkPermissions', pin: boolean } }; + +export type DatasetLayout_VersionFragment = { __typename?: 'DatasetVersion', id: string, name: string, createdAt: any }; + +export const DatasetLayout_WorkspaceFragmentDoc = gql` + fragment DatasetLayout_workspace on Workspace { + ...WorkspaceLayout_workspace + name + slug +} + ${WorkspaceLayout_WorkspaceFragmentDoc}`; +export const DatasetLayout_DatasetLinkFragmentDoc = gql` + fragment DatasetLayout_datasetLink on DatasetLink { + ...UploadDatasetVersionDialog_datasetLink + ...PinDatasetButton_link + dataset { + workspace { + slug + } + slug + permissions { + delete + createVersion + } + } +} + ${UploadDatasetVersionDialog_DatasetLinkFragmentDoc} +${PinDatasetButton_LinkFragmentDoc}`; +export const DatasetLayout_VersionFragmentDoc = gql` + fragment DatasetLayout_version on DatasetVersion { + id + name + ...DatasetVersionPicker_version +} + ${DatasetVersionPicker_VersionFragmentDoc}`; \ No newline at end of file diff --git a/src/datasets/layouts/DatasetLayout.tsx b/src/datasets/layouts/DatasetLayout.tsx new file mode 100644 index 00000000..48f03898 --- /dev/null +++ b/src/datasets/layouts/DatasetLayout.tsx @@ -0,0 +1,266 @@ +import { gql } from "@apollo/client"; +import { PlusIcon, TrashIcon } from "@heroicons/react/24/outline"; +import clsx from "clsx"; +import Breadcrumbs from "core/components/Breadcrumbs"; +import Button from "core/components/Button"; +import DataCard from "core/components/DataCard"; +import Link from "core/components/Link"; +import Title from "core/components/Title"; +import { useRouter } from "next/router"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import WorkspaceLayout from "workspaces/layouts/WorkspaceLayout"; +import DatasetVersionPicker from "../features/DatasetVersionPicker"; +import DeleteDatasetTrigger from "../features/DeleteDatasetTrigger"; +import PinDatasetButton from "../features/PinDatasetButton"; +import UploadDatasetVersionDialog from "../features/UploadDatasetVersionDialog"; +import { + DatasetLayout_DatasetLinkFragment, + DatasetLayout_VersionFragment, + DatasetLayout_WorkspaceFragment, +} from "./DatasetLayout.generated"; +import { trackEvent } from "core/helpers/analytics"; + +type TabsProps = { + selected?: string; + tabs: { label: string; href: string; id: string }[]; + className?: string; +}; +const Tabs = ({ tabs, selected, className }: TabsProps) => { + return ( +
+ {tabs.map((tab) => ( + + {tab.label} + + ))} +
+ ); +}; + +type DatasetLayoutProps = { + datasetLink: DatasetLayout_DatasetLinkFragment; + version: DatasetLayout_VersionFragment | null; + workspace: DatasetLayout_WorkspaceFragment; + tab?: string; + extraBreadcrumbs?: { href: string; title: string }[]; + children: React.ReactNode; +}; + +const DatasetLayout = (props: DatasetLayoutProps) => { + const { + children, + datasetLink, + workspace, + version, + tab = "general", + extraBreadcrumbs = [], + } = props; + + const { t } = useTranslation(); + const router = useRouter(); + const [isUploadDialogOpen, setUploadDialogOpen] = useState(false); + + const onChangeVersion: React.ComponentProps< + typeof DatasetVersionPicker + >["onChange"] = (version) => { + delete router.query["fileId"]; + router.push({ + pathname: router.pathname, + query: { ...router.query, version: version?.id }, + }); + }; + + useEffect(() => { + trackEvent("datasets.dataset_open", { + workspace: workspace.slug, + dataset_id: datasetLink.dataset.slug, + dataset_version: version?.name, + }); + }, []); + + if (!datasetLink) { + return null; + } + + const { dataset } = datasetLink; + const isWorkspaceSource = workspace.slug === dataset.workspace?.slug; + + return ( + + + + + {workspace.name} + + + {t("Datasets")} + + + {dataset.name} + + {extraBreadcrumbs.map(({ href, title }, index) => ( + + {title} + + ))} + + + {dataset.permissions.createVersion && isWorkspaceSource && ( + + )} + {isWorkspaceSource && dataset.permissions.delete && ( + + router.push({ + pathname: "/workspaces/[workspaceSlug]/datasets", + query: { workspaceSlug: workspace.slug }, + }) + } + > + {({ onClick }) => ( + + )} + + )} + + + + {dataset.name} + {version && ( + // Only show the version picker if we have a version + <DatasetVersionPicker + onChange={onChangeVersion} + dataset={dataset} + version={version} + className="min-w-40" + /> + )} + + + + {children} + + + + setUploadDialogOpen(false)} + datasetLink={datasetLink} + /> + + ); +}; + +DatasetLayout.fragments = { + workspace: gql` + fragment DatasetLayout_workspace on Workspace { + ...WorkspaceLayout_workspace + name + slug + } + ${WorkspaceLayout.fragments.workspace} + `, + datasetLink: gql` + fragment DatasetLayout_datasetLink on DatasetLink { + ...UploadDatasetVersionDialog_datasetLink + ...PinDatasetButton_link + dataset { + workspace { + slug + } + slug + permissions { + delete + createVersion + } + } + } + ${UploadDatasetVersionDialog.fragments.datasetLink} + ${PinDatasetButton.fragments.link} + `, + + version: gql` + fragment DatasetLayout_version on DatasetVersion { + id + name + ...DatasetVersionPicker_version + } + ${DatasetVersionPicker.fragments.version} + `, +}; + +export default DatasetLayout; diff --git a/src/datasets/layouts/index.tsx b/src/datasets/layouts/index.tsx new file mode 100644 index 00000000..402c8f90 --- /dev/null +++ b/src/datasets/layouts/index.tsx @@ -0,0 +1,3 @@ +import DatasetLayout from "./DatasetLayout"; + +export default DatasetLayout; diff --git a/src/graphql/types.ts b/src/graphql/types.ts index c25050ae..fae55439 100644 --- a/src/graphql/types.ts +++ b/src/graphql/types.ts @@ -19,6 +19,7 @@ export type Scalars = { Generic: { input: any; output: any; } JSON: { input: any; output: any; } MovingSpeeds: { input: any; output: any; } + OpaqueID: { input: any; output: any; } SimplifiedExtentType: { input: any; output: any; } StackPriorities: { input: any; output: any; } TimeThresholds: { input: any; output: any; } @@ -762,6 +763,26 @@ export type CreateMembershipResult = { success: Scalars['Boolean']['output']; }; +/** Errors that can occur when creating an attribute. */ +export enum CreateMetadataAttributeError { + DuplicateKey = 'DUPLICATE_KEY', + PermissionDenied = 'PERMISSION_DENIED', + TargetNotFound = 'TARGET_NOT_FOUND' +} + +/** Input to add a custom attribute, empty field for value is accepted */ +export type CreateMetadataAttributeInput = { + key: Scalars['String']['input']; + targetId: Scalars['OpaqueID']['input']; + value?: InputMaybe; +}; + +export type CreateMetadataAttributeResult = { + __typename?: 'CreateMetadataAttributeResult'; + errors: Array; + success: Scalars['Boolean']['output']; +}; + /** Represents the input for creating a pipeline. */ export type CreatePipelineInput = { code: Scalars['String']['input']; @@ -1015,8 +1036,9 @@ export type DatabaseTablePage = { }; /** Dataset is a collection of files that are related to each other and are versioned. */ -export type Dataset = { +export type Dataset = MetadataObject & { __typename?: 'Dataset'; + attributes: Array; createdAt: Scalars['DateTime']['output']; createdBy?: Maybe; description?: Maybe; @@ -1026,6 +1048,7 @@ export type Dataset = { name: Scalars['String']['output']; permissions: DatasetPermissions; slug: Scalars['String']['output']; + targetId: Scalars['OpaqueID']['output']; updatedAt: Scalars['DateTime']['output']; version?: Maybe; versions: DatasetVersionPage; @@ -1052,11 +1075,12 @@ export type DatasetVersionsArgs = { perPage?: InputMaybe; }; -/** Metadata for dataset file */ -export type DatasetFileMetadata = { - __typename?: 'DatasetFileMetadata'; - sample: Scalars['JSON']['output']; - status: FileMetadataStatus; +/** File sample for dataset file */ +export type DatasetFileSample = { + __typename?: 'DatasetFileSample'; + sample?: Maybe; + status: FileSampleStatus; + statusReason?: Maybe; }; /** A link of a dataset with a workspace. */ @@ -1110,8 +1134,9 @@ export type DatasetPermissions = { }; /** A version of a dataset. A version is a snapshot of the dataset at a point in time. */ -export type DatasetVersion = { +export type DatasetVersion = MetadataObject & { __typename?: 'DatasetVersion'; + attributes: Array; createdAt: Scalars['DateTime']['output']; createdBy?: Maybe; dataset: Dataset; @@ -1121,6 +1146,7 @@ export type DatasetVersion = { id: Scalars['ID']['output']; name: Scalars['String']['output']; permissions: DatasetVersionPermissions; + targetId: Scalars['OpaqueID']['output']; }; @@ -1137,14 +1163,18 @@ export type DatasetVersionFilesArgs = { }; /** A file in a dataset version. */ -export type DatasetVersionFile = { +export type DatasetVersionFile = MetadataObject & { __typename?: 'DatasetVersionFile'; + attributes: Array; contentType: Scalars['String']['output']; createdAt: Scalars['DateTime']['output']; createdBy?: Maybe; - fileMetadata?: Maybe; + fileSample?: Maybe; filename: Scalars['String']['output']; id: Scalars['ID']['output']; + properties?: Maybe; + size: Scalars['BigInt']['output']; + targetId: Scalars['OpaqueID']['output']; uri: Scalars['String']['output']; }; @@ -1371,6 +1401,25 @@ export type DeleteMembershipResult = { success: Scalars['Boolean']['output']; }; +/** Errors that can occur when deleting an attribute. */ +export enum DeleteMetadataAttributeError { + MetadataAttributeNotFound = 'METADATA_ATTRIBUTE_NOT_FOUND', + PermissionDenied = 'PERMISSION_DENIED', + TargetNotFound = 'TARGET_NOT_FOUND' +} + +/** Input to delete custom attribute */ +export type DeleteMetadataAttributeInput = { + key: Scalars['String']['input']; + targetId: Scalars['OpaqueID']['input']; +}; + +export type DeleteMetadataAttributeResult = { + __typename?: 'DeleteMetadataAttributeResult'; + errors: Array; + success: Scalars['Boolean']['output']; +}; + /** Represents the input for deleting a pipeline. */ export type DeletePipelineInput = { id: Scalars['UUID']['input']; @@ -1529,6 +1578,25 @@ export type DisableTwoFactorResult = { success: Scalars['Boolean']['output']; }; +/** Errors that can occur when editing an attribute. */ +export enum EditMetadataAttributeError { + PermissionDenied = 'PERMISSION_DENIED', + TargetNotFound = 'TARGET_NOT_FOUND' +} + +/** Input to edit a custom attribute, empty field for value is accepted */ +export type EditMetadataAttributeInput = { + key: Scalars['String']['input']; + targetId: Scalars['OpaqueID']['input']; + value?: InputMaybe; +}; + +export type EditMetadataAttributeResult = { + __typename?: 'EditMetadataAttributeResult'; + errors: Array; + success: Scalars['Boolean']['output']; +}; + /** The EnableTwoFactorError enum represents the possible errors that can occur during the enableTwoFactor mutation. */ export enum EnableTwoFactorError { AlreadyEnabled = 'ALREADY_ENABLED', @@ -1557,8 +1625,8 @@ export type FeatureFlag = { config: Scalars['JSON']['output']; }; -/** Statuses that can occur when generating file metadata */ -export enum FileMetadataStatus { +/** Statuses that can occur when generating file sample */ +export enum FileSampleStatus { Failed = 'FAILED', Finished = 'FINISHED', Processing = 'PROCESSING' @@ -1892,8 +1960,25 @@ export enum MessagePriority { Warning = 'WARNING' } +/** Generic metadata attribute */ +export type MetadataAttribute = { + __typename?: 'MetadataAttribute'; + id: Scalars['UUID']['output']; + key: Scalars['String']['output']; + system: Scalars['Boolean']['output']; + value?: Maybe; +}; + +/** Interface for type implementing metadata */ +export type MetadataObject = { + attributes: Array; + targetId: Scalars['OpaqueID']['output']; +}; + export type Mutation = { __typename?: 'Mutation'; + /** Add a custom metadata attribute to an object instance */ + addMetadataAttribute: CreateMetadataAttributeResult; /** Adds an output to a pipeline. */ addPipelineOutput: AddPipelineOutputResult; approveAccessmodAccessRequest: ApproveAccessmodAccessRequestResult; @@ -1933,6 +2018,8 @@ export type Mutation = { /** Delete a dataset version. */ deleteDatasetVersion: DeleteDatasetVersionResult; deleteMembership: DeleteMembershipResult; + /** Delete an metadata attribute from an object instance */ + deleteMetadataAttribute: DeleteMetadataAttributeResult; /** Deletes a pipeline. */ deletePipeline: DeletePipelineResult; /** Deletes a pipeline version. */ @@ -1945,6 +2032,8 @@ export type Mutation = { denyAccessmodAccessRequest: DenyAccessmodAccessRequestResult; /** Disables two-factor authentication for the currently authenticated user. */ disableTwoFactor: DisableTwoFactorResult; + /** Edit metadata attribute for an object instance */ + editMetadataAttribute: EditMetadataAttributeResult; /** Enables two-factor authentication for the currently authenticated user. */ enableTwoFactor: EnableTwoFactorResult; /** Generates a challenge for two-factor authentication. */ @@ -2023,6 +2112,11 @@ export type Mutation = { }; +export type MutationAddMetadataAttributeArgs = { + input: CreateMetadataAttributeInput; +}; + + export type MutationAddPipelineOutputArgs = { input: AddPipelineOutputInput; }; @@ -2168,6 +2262,11 @@ export type MutationDeleteMembershipArgs = { }; +export type MutationDeleteMetadataAttributeArgs = { + input: DeleteMetadataAttributeInput; +}; + + export type MutationDeletePipelineArgs = { input?: InputMaybe; }; @@ -2213,6 +2312,11 @@ export type MutationDisableTwoFactorArgs = { }; +export type MutationEditMetadataAttributeArgs = { + input: EditMetadataAttributeInput; +}; + + export type MutationEnableTwoFactorArgs = { input?: InputMaybe; }; @@ -2912,6 +3016,7 @@ export type Query = { datasets: DatasetPage; /** Retrieves the currently authenticated user. */ me: Me; + metadataAttributes: Array>; notebooksUrl: Scalars['URL']['output']; /** Retrieves a list of organizations. */ organizations: Array; @@ -3063,6 +3168,11 @@ export type QueryDatasetsArgs = { }; +export type QueryMetadataAttributesArgs = { + targetId: Scalars['OpaqueID']['input']; +}; + + export type QueryPendingWorkspaceInvitationsArgs = { page?: Scalars['Int']['input']; perPage?: InputMaybe; diff --git a/src/pages/workspaces/[workspaceSlug]/datasets/[datasetSlug].tsx b/src/pages/workspaces/[workspaceSlug]/datasets/[datasetSlug].tsx deleted file mode 100644 index d4339bcd..00000000 --- a/src/pages/workspaces/[workspaceSlug]/datasets/[datasetSlug].tsx +++ /dev/null @@ -1,367 +0,0 @@ -import { - CloudArrowUpIcon, - PlusIcon, - TrashIcon, -} from "@heroicons/react/24/outline"; -import Breadcrumbs from "core/components/Breadcrumbs"; -import Button from "core/components/Button/Button"; -import DataCard from "core/components/DataCard"; -import TextProperty from "core/components/DataCard/TextProperty"; -import Page from "core/components/Page"; -import { createGetServerSideProps } from "core/helpers/page"; -import { NextPageWithLayout } from "core/helpers/types"; -import DatasetVersionFilesDataGrid from "datasets/features/DatasetVersionFilesDataGrid/DatasetVersionFilesDataGrid"; -import DatasetVersionPicker from "datasets/features/DatasetVersionPicker/DatasetVersionPicker"; -import { useTranslation } from "next-i18next"; -import { useRouter } from "next/router"; -import { - useWorkspaceDatasetPageQuery, - WorkspaceDatasetPageDocument, - WorkspaceDatasetPageQuery, - WorkspaceDatasetPageQueryVariables, -} from "workspaces/graphql/queries.generated"; -import WorkspaceLayout from "workspaces/layouts/WorkspaceLayout"; -import DatasetLinksDataGrid from "../../../../datasets/features/DatasetLinksDataGrid"; -import UserProperty from "../../../../core/components/DataCard/UserProperty"; -import DateProperty from "../../../../core/components/DataCard/DateProperty"; -import { useEffect, useState } from "react"; -import { updateDataset } from "datasets/helpers/dataset"; -import UploadDatasetVersionDialog from "datasets/features/UploadDatasetVersionDialog"; -import DescriptionList from "core/components/DescriptionList"; -import Time from "core/components/Time"; -import PinDatasetButton from "datasets/features/PinDatasetButton"; -import LinkDatasetDialog from "datasets/features/LinkDatasetDialog"; -import { LinkIcon } from "@heroicons/react/24/solid"; -import useCacheKey from "core/hooks/useCacheKey"; -import DeleteDatasetTrigger from "datasets/features/DeleteDatasetTrigger"; -import RenderProperty from "core/components/DataCard/RenderProperty"; -import Clipboard from "core/components/Clipboard"; -import { trackEvent } from "core/helpers/analytics"; - -type Props = { - datasetSlug: string; - workspaceSlug: string; - versionId: string; - isSpecificVersion: boolean; -}; - -const WorkspaceDatasetPage: NextPageWithLayout = (props: Props) => { - const { datasetSlug, workspaceSlug, isSpecificVersion, versionId } = props; - - const { t } = useTranslation(); - const router = useRouter(); - const [isUploadDialogOpen, setUploadDialogOpen] = useState(false); - const [isLinkDialogOpen, setLinkDialogOpen] = useState(false); - const { data, refetch } = useWorkspaceDatasetPageQuery({ - variables: { - workspaceSlug, - datasetSlug, - versionId, - isSpecificVersion, - }, - }); - useCacheKey(["datasets"], () => refetch()); - - useEffect(() => { - if (data?.datasetLink) { - const version = dataset.version || dataset.latestVersion || null; - trackEvent("datasets.dataset_open", { - workspace: workspaceSlug, - dataset_id: datasetSlug, - dataset_version: version?.name, - }); - } - }, []); - - const onChangeVersion: React.ComponentProps< - typeof DatasetVersionPicker - >["onChange"] = (version) => { - router.push({ - pathname: router.pathname, - query: { ...router.query, version: version?.id }, - }); - }; - - if (!data?.datasetLink) { - return null; - } - const { datasetLink } = data; - const { dataset, workspace } = datasetLink; - const isWorkspaceSource = workspace.slug === dataset.workspace?.slug; - const version = dataset.version || dataset.latestVersion || null; - - const onSave = async (values: any) => { - await updateDataset(datasetLink.dataset.id, values); - }; - - return ( - - - - - - {workspace.name} - - - {t("Datasets")} - - - {datasetLink.dataset.name} - - - - {isWorkspaceSource && datasetLink.dataset.permissions.delete && ( - - router.push({ - pathname: "/workspaces/[workspaceSlug]/datasets", - query: { workspaceSlug: workspace.slug }, - }) - } - > - {({ onClick }) => ( - - )} - - )} - - - - - - isEditing} - /> - - - {(property) => ( -
- - {property.displayValue} - -
- )} -
- - - -
- ( -
-

- {!version && t("Versions")} - {version && - version.id === datasetLink.dataset.latestVersion?.id && - t("Latest version")} - {version && - version.id !== datasetLink.dataset.latestVersion?.id && - t("Version {{version}}", { version: version.name })} -

- {datasetLink.dataset.latestVersion && ( - - )} - {datasetLink.dataset.permissions.createVersion && - isWorkspaceSource && ( - - )} -
- )} - collapsible={false} - > - {version ? ( - <> - - - {version.name} - - - - - {version.createdBy?.displayName ?? "-"} - - - -
- -
- - ) : ( -

- {t( - "This dataset has no version. Upload a new version using your browser or the SDK to see it here.", - )} -

- )} -
- {isWorkspaceSource ? ( - ( -
-

{t("Access Management")}

- {workspace.permissions.update && ( - - )} -
- )} - collapsible={false} - > -
- -
-
- ) : null} -
-
-
- - setUploadDialogOpen(false)} - datasetLink={datasetLink} - /> - setLinkDialogOpen(false)} - /> -
- ); -}; - -WorkspaceDatasetPage.getLayout = (page) => page; - -export const getServerSideProps = createGetServerSideProps({ - requireAuth: true, - async getServerSideProps(ctx, client) { - const versionId = (ctx.query.version as string) ?? ""; - - const variables = { - workspaceSlug: ctx.params!.workspaceSlug as string, - datasetSlug: ctx.params!.datasetSlug as string, - versionId: versionId, - isSpecificVersion: Boolean(versionId), - }; - - const { data } = await client.query< - WorkspaceDatasetPageQuery, - WorkspaceDatasetPageQueryVariables - >({ - query: WorkspaceDatasetPageDocument, - variables, - }); - - if (!data.datasetLink) { - return { notFound: true }; - } - // If we have a versionId or there is a version in the dataset, prefetch the files - if (versionId || data.datasetLink.dataset.latestVersion?.id) { - await DatasetVersionFilesDataGrid.prefetch(client, { - perPage: 10, - versionId: (versionId || - data.datasetLink.dataset.latestVersion?.id) as string, - }); - } - - return { - props: variables, - }; - }, -}); - -export default WorkspaceDatasetPage; diff --git a/src/pages/workspaces/[workspaceSlug]/datasets/[datasetSlug]/access.tsx b/src/pages/workspaces/[workspaceSlug]/datasets/[datasetSlug]/access.tsx new file mode 100644 index 00000000..b0f24ebd --- /dev/null +++ b/src/pages/workspaces/[workspaceSlug]/datasets/[datasetSlug]/access.tsx @@ -0,0 +1,112 @@ +import { LinkIcon } from "@heroicons/react/24/outline"; +import Block from "core/components/Block"; +import Button from "core/components/Button"; +import Page from "core/components/Page"; +import { createGetServerSideProps } from "core/helpers/page"; +import { NextPageWithLayout } from "core/helpers/types"; +import DatasetLinksDataGrid from "datasets/features/DatasetLinksDataGrid"; +import LinkDatasetDialog from "datasets/features/LinkDatasetDialog"; +import DatasetLayout from "datasets/layouts/DatasetLayout"; +import { useTranslation } from "next-i18next"; +import { useState } from "react"; +import { + useWorkspaceDatasetAccessPageQuery, + WorkspaceDatasetAccessPageDocument, + WorkspaceDatasetAccessPageQuery, + WorkspaceDatasetAccessPageQueryVariables, +} from "workspaces/graphql/queries.generated"; + +export type WorkspaceDatasetAccessPageProps = { + isSpecificVersion: boolean; + workspaceSlug: string; + datasetSlug: string; + versionId: string; +}; + +const WorkspaceDatasetAccessPage: NextPageWithLayout = ( + props: WorkspaceDatasetAccessPageProps, +) => { + const { t } = useTranslation(); + const { data } = useWorkspaceDatasetAccessPageQuery({ + variables: props, + }); + const [isLinkDialogOpen, setLinkDialogOpen] = useState(false); + if (!data || !data.datasetLink || !data.workspace) { + return null; + } + const { datasetLink, workspace } = data; + const { dataset } = datasetLink; + const version = props.isSpecificVersion + ? datasetLink.dataset.version + : datasetLink.dataset.latestVersion; + + return ( + + + + + {workspace.permissions.update && ( + + )} + + + setLinkDialogOpen(false)} + /> + + ); +}; + +WorkspaceDatasetAccessPage.getLayout = (page) => page; + +export const getServerSideProps = createGetServerSideProps({ + requireAuth: true, + async getServerSideProps(ctx, client) { + const versionId = (ctx.query.version as string) ?? ""; + + const variables = { + workspaceSlug: ctx.params!.workspaceSlug as string, + datasetSlug: ctx.params!.datasetSlug as string, + versionId: versionId, + isSpecificVersion: Boolean(versionId), + }; + + const { data } = await client.query< + WorkspaceDatasetAccessPageQuery, + WorkspaceDatasetAccessPageQueryVariables + >({ + query: WorkspaceDatasetAccessPageDocument, + variables, + }); + + if (!data.datasetLink || !data.workspace) { + return { notFound: true }; + } + + return { + props: variables, + }; + }, +}); + +export default WorkspaceDatasetAccessPage; diff --git a/src/pages/workspaces/[workspaceSlug]/datasets/[datasetSlug]/files/[[...fileId]].tsx b/src/pages/workspaces/[workspaceSlug]/datasets/[datasetSlug]/files/[[...fileId]].tsx new file mode 100644 index 00000000..1315c7ba --- /dev/null +++ b/src/pages/workspaces/[workspaceSlug]/datasets/[datasetSlug]/files/[[...fileId]].tsx @@ -0,0 +1,130 @@ +import Page from "core/components/Page"; +import { createGetServerSideProps } from "core/helpers/page"; +import { NextPageWithLayout } from "core/helpers/types"; +import DatasetExplorer from "datasets/features/DatasetExplorer"; +import LinkDatasetDialog from "datasets/features/LinkDatasetDialog"; +import DatasetLayout from "datasets/layouts/DatasetLayout"; +import { useTranslation } from "next-i18next"; +import { useRouter } from "next/router"; +import { useState } from "react"; +import { + useWorkspaceDatasetFilesPageQuery, + WorkspaceDatasetFilesPageDocument, + WorkspaceDatasetFilesPageQuery, + WorkspaceDatasetFilesPageQueryVariables, +} from "workspaces/graphql/queries.generated"; + +export type WorkspaceDatasetFilesPageProps = { + isSpecificVersion: boolean; + workspaceSlug: string; + datasetSlug: string; + versionId: string; + fileId: string | null; +}; + +const WorkspaceDatasetFilesPage: NextPageWithLayout = ( + props: WorkspaceDatasetFilesPageProps, +) => { + const { t } = useTranslation(); + const router = useRouter(); + const [isLinkDialogOpen, setLinkDialogOpen] = useState(false); + const { fileId, isSpecificVersion, workspaceSlug, datasetSlug, versionId } = + props; + const { data } = useWorkspaceDatasetFilesPageQuery({ + variables: { isSpecificVersion, workspaceSlug, datasetSlug, versionId }, + }); + if (!data || !data.datasetLink || !data.workspace) { + return null; + } + const { datasetLink, workspace } = data; + const { dataset } = datasetLink; + const version = isSpecificVersion ? dataset.version! : dataset.latestVersion!; + + const currentFile = (() => { + if (!fileId) { + return version.files.items[0]; + } else { + return version.files.items.find( + (file: { id: string }) => file.id === fileId, + ); + } + })(); + + return ( + + + + router.push({ + pathname: `${router.pathname}`, + query: { ...router.query, version: version?.id, fileId: file.id }, + }) + } + /> + + setLinkDialogOpen(false)} + /> + + ); +}; + +WorkspaceDatasetFilesPage.getLayout = (page) => page; + +export const getServerSideProps = createGetServerSideProps({ + requireAuth: true, + async getServerSideProps(ctx, client) { + const versionId = (ctx.query.version as string) ?? ""; + + const variables = { + workspaceSlug: ctx.params!.workspaceSlug as string, + datasetSlug: ctx.params!.datasetSlug as string, + versionId: versionId, + isSpecificVersion: Boolean(versionId), + }; + + const { data } = await client.query< + WorkspaceDatasetFilesPageQuery, + WorkspaceDatasetFilesPageQueryVariables + >({ + query: WorkspaceDatasetFilesPageDocument, + variables, + }); + + const version = variables.isSpecificVersion + ? data.datasetLink?.dataset.version + : data.datasetLink?.dataset.latestVersion; + if (!data.datasetLink || !data.workspace || !version) { + return { notFound: true }; + } + + // optional route parameters + const fileArr = (ctx.query.fileId as string[]) ?? []; + + return { + props: { + ...variables, + fileId: fileArr.length === 1 ? fileArr[0] : null, + }, + }; + }, +}); + +export default WorkspaceDatasetFilesPage; diff --git a/src/pages/workspaces/[workspaceSlug]/datasets/[datasetSlug]/index.tsx b/src/pages/workspaces/[workspaceSlug]/datasets/[datasetSlug]/index.tsx new file mode 100644 index 00000000..25ef14ce --- /dev/null +++ b/src/pages/workspaces/[workspaceSlug]/datasets/[datasetSlug]/index.tsx @@ -0,0 +1,185 @@ +import Badge from "core/components/Badge"; +import Clipboard from "core/components/Clipboard"; +import DataCard from "core/components/DataCard"; +import DateProperty from "core/components/DataCard/DateProperty"; +import RenderProperty from "core/components/DataCard/RenderProperty"; +import TextProperty from "core/components/DataCard/TextProperty"; +import UserProperty from "core/components/DataCard/UserProperty"; +import DescriptionList from "core/components/DescriptionList"; +import Page from "core/components/Page"; +import Time from "core/components/Time"; +import { createGetServerSideProps } from "core/helpers/page"; +import { NextPageWithLayout } from "core/helpers/types"; +import { updateDataset } from "datasets/helpers/dataset"; +import DatasetLayout from "datasets/layouts/DatasetLayout"; +import { useTranslation } from "next-i18next"; +import { + useWorkspaceDatasetIndexPageQuery, + WorkspaceDatasetIndexPageDocument, + WorkspaceDatasetIndexPageQuery, + WorkspaceDatasetIndexPageQueryVariables, +} from "workspaces/graphql/queries.generated"; + +export type WorkspaceDatasetPageProps = { + isSpecificVersion: boolean; + workspaceSlug: string; + datasetSlug: string; + versionId: string; +}; + +const WorkspaceDatasetPage: NextPageWithLayout = ( + props: WorkspaceDatasetPageProps, +) => { + const { t } = useTranslation(); + const { data } = useWorkspaceDatasetIndexPageQuery({ + variables: props, + }); + if (!data || !data.datasetLink || !data.workspace) { + return null; + } + const { datasetLink, workspace } = data; + const { dataset } = datasetLink; + const version = props.isSpecificVersion + ? datasetLink.dataset.version + : datasetLink.dataset.latestVersion; + + const isWorkspaceSource = workspace.slug === dataset.workspace?.slug; + + const onSave = async (values: any) => { + await updateDataset(dataset.id, values); + }; + + return ( + + + + isEditing} + /> + + + {(property) => ( +
+ + {property.displayValue} + +
+ )} +
+ + + isEditing && !isWorkspaceSource} + id={"workspace"} + accessor={"workspace.name"} + label={t("Source workspace")} + > + {(property) => {property.displayValue}} + +
+ {version && ( + ( +
+

+ {version && + version.id === datasetLink.dataset.latestVersion?.id && + t("Current version")} + {version && + version.id !== datasetLink.dataset.latestVersion?.id && + t("Version {{version}}", { + version: version.name, + })} +

+
+ )} + collapsible={false} + > + + + {version.name} + + + + + {version.createdBy?.displayName ?? "-"} + + +
+ )} +
+
+ ); +}; + +WorkspaceDatasetPage.getLayout = (page) => page; + +export const getServerSideProps = createGetServerSideProps({ + requireAuth: true, + async getServerSideProps(ctx, client) { + const versionId = (ctx.query.version as string) ?? ""; + + const variables = { + workspaceSlug: ctx.params!.workspaceSlug as string, + datasetSlug: ctx.params!.datasetSlug as string, + versionId: versionId, + isSpecificVersion: Boolean(versionId), + }; + + const { data } = await client.query< + WorkspaceDatasetIndexPageQuery, + WorkspaceDatasetIndexPageQueryVariables + >({ + query: WorkspaceDatasetIndexPageDocument, + variables, + }); + + if (!data.datasetLink || !data.workspace) { + return { notFound: true }; + } + + return { + props: variables, + }; + }, +}); + +export default WorkspaceDatasetPage; diff --git a/src/workspaces/graphql/queries.generated.tsx b/src/workspaces/graphql/queries.generated.tsx index 8f07d13e..5fbbddc5 100644 --- a/src/workspaces/graphql/queries.generated.tsx +++ b/src/workspaces/graphql/queries.generated.tsx @@ -22,11 +22,9 @@ import { RunLogs_RunFragmentDoc } from '../../pipelines/features/RunLogs/RunLogs import { CreateDatasetDialog_WorkspaceFragmentDoc } from '../../datasets/features/CreateDatasetDialog/CreateDatasetDialog.generated'; import { DatasetCard_LinkFragmentDoc } from '../../datasets/features/DatasetCard/DatasetCard.generated'; import { PinDatasetButton_LinkFragmentDoc } from '../../datasets/features/PinDatasetButton/PinDatasetButton.generated'; -import { UploadDatasetVersionDialog_DatasetLinkFragmentDoc } from '../../datasets/features/UploadDatasetVersionDialog/UploadDatasetVersionDialog.generated'; -import { DeleteDatasetTrigger_DatasetFragmentDoc } from '../../datasets/features/DeleteDatasetTrigger/DeleteDatasetTrigger.generated'; +import { DatasetLayout_WorkspaceFragmentDoc, DatasetLayout_DatasetLinkFragmentDoc, DatasetLayout_VersionFragmentDoc } from '../../datasets/layouts/DatasetLayout.generated'; import { DatasetLinksDataGrid_DatasetFragmentDoc } from '../../datasets/features/DatasetLinksDataGrid/DatasetLinksDataGrid.generated'; -import { DatasetVersionPicker_DatasetFragmentDoc, DatasetVersionPicker_VersionFragmentDoc } from '../../datasets/features/DatasetVersionPicker/DatasetVersionPicker.generated'; -import { DatasetVersionFilesDataGrid_VersionFragmentDoc } from '../../datasets/features/DatasetVersionFilesDataGrid/DatasetVersionFilesDataGrid.generated'; +import { DatasetExplorer_VersionFragmentDoc } from '../../datasets/features/DatasetExplorer/DatasetExplorer.generated'; import { BucketExplorer_WorkspaceFragmentDoc, BucketExplorer_ObjectsFragmentDoc } from '../features/BucketExplorer/BucketExplorer.generated'; import { UploadObjectDialog_WorkspaceFragmentDoc } from '../features/UploadObjectDialog/UploadObjectDialog.generated'; import { CreateBucketFolderDialog_WorkspaceFragmentDoc } from '../features/CreateBucketFolderDialog/CreateBucketFolderDialog.generated'; @@ -110,7 +108,7 @@ export type WorkspaceDatasetsPageQueryVariables = Types.Exact<{ export type WorkspaceDatasetsPageQuery = { __typename?: 'Query', workspace?: { __typename?: 'Workspace', slug: string, name: string, permissions: { __typename?: 'WorkspacePermissions', createDataset: boolean, manageMembers: boolean, update: boolean, launchNotebookServer: boolean }, pinnedDatasets: { __typename?: 'DatasetLinkPage', items: Array<{ __typename?: 'DatasetLink', id: string, dataset: { __typename?: 'Dataset', name: string, slug: string, description?: string | null, updatedAt: any, workspace?: { __typename?: 'Workspace', slug: string, name: string } | null }, workspace: { __typename?: 'Workspace', slug: string, name: string } }> }, datasets: { __typename?: 'DatasetLinkPage', totalItems: number, totalPages: number, pageNumber: number, items: Array<{ __typename?: 'DatasetLink', id: string, isPinned: boolean, dataset: { __typename?: 'Dataset', id: string, name: string, slug: string, description?: string | null, updatedAt: any, workspace?: { __typename?: 'Workspace', slug: string, name: string } | null, permissions: { __typename?: 'DatasetPermissions', update: boolean, delete: boolean }, createdBy?: { __typename?: 'User', id: string, email: string, displayName: string, avatar: { __typename?: 'Avatar', initials: string, color: string } } | null }, permissions: { __typename?: 'DatasetLinkPermissions', pin: boolean } }> }, countries: Array<{ __typename?: 'Country', flag: string, code: string }> } | null }; -export type WorkspaceDatasetPageQueryVariables = Types.Exact<{ +export type WorkspaceDatasetIndexPageQueryVariables = Types.Exact<{ workspaceSlug: Types.Scalars['String']['input']; datasetSlug: Types.Scalars['String']['input']; versionId: Types.Scalars['ID']['input']; @@ -118,7 +116,27 @@ export type WorkspaceDatasetPageQueryVariables = Types.Exact<{ }>; -export type WorkspaceDatasetPageQuery = { __typename?: 'Query', datasetLink?: { __typename?: 'DatasetLink', id: string, isPinned: boolean, workspace: { __typename?: 'Workspace', slug: string, name: string, permissions: { __typename?: 'WorkspacePermissions', manageMembers: boolean, update: boolean, launchNotebookServer: boolean }, countries: Array<{ __typename?: 'Country', flag: string, code: string }> }, dataset: { __typename?: 'Dataset', id: string, name: string, slug: string, description?: string | null, updatedAt: any, createdAt: any, workspace?: { __typename?: 'Workspace', slug: string, name: string } | null, createdBy?: { __typename?: 'User', id: string, email: string, displayName: string, avatar: { __typename?: 'Avatar', initials: string, color: string } } | null, latestVersion?: { __typename?: 'DatasetVersion', createdAt: any, id: string, name: string, createdBy?: { __typename?: 'User', displayName: string } | null, permissions: { __typename?: 'DatasetVersionPermissions', download: boolean } } | null, version?: { __typename?: 'DatasetVersion', createdAt: any, id: string, name: string, createdBy?: { __typename?: 'User', displayName: string } | null, permissions: { __typename?: 'DatasetVersionPermissions', download: boolean } } | null, permissions: { __typename?: 'DatasetPermissions', update: boolean, delete: boolean, createVersion: boolean } }, permissions: { __typename?: 'DatasetLinkPermissions', pin: boolean } } | null }; +export type WorkspaceDatasetIndexPageQuery = { __typename?: 'Query', workspace?: { __typename?: 'Workspace', slug: string, name: string, permissions: { __typename?: 'WorkspacePermissions', manageMembers: boolean, update: boolean, launchNotebookServer: boolean }, countries: Array<{ __typename?: 'Country', flag: string, code: string }> } | null, datasetLink?: { __typename?: 'DatasetLink', id: string, isPinned: boolean, dataset: { __typename?: 'Dataset', description?: string | null, updatedAt: any, createdAt: any, slug: string, id: string, name: string, permissions: { __typename?: 'DatasetPermissions', update: boolean, delete: boolean, createVersion: boolean }, workspace?: { __typename?: 'Workspace', name: string, slug: string } | null, createdBy?: { __typename?: 'User', id: string, email: string, displayName: string, avatar: { __typename?: 'Avatar', initials: string, color: string } } | null, version?: { __typename?: 'DatasetVersion', id: string, createdAt: any, name: string, createdBy?: { __typename?: 'User', displayName: string } | null } | null, latestVersion?: { __typename?: 'DatasetVersion', id: string, createdAt: any, name: string, createdBy?: { __typename?: 'User', displayName: string } | null } | null }, workspace: { __typename?: 'Workspace', slug: string }, permissions: { __typename?: 'DatasetLinkPermissions', pin: boolean } } | null }; + +export type WorkspaceDatasetAccessPageQueryVariables = Types.Exact<{ + workspaceSlug: Types.Scalars['String']['input']; + datasetSlug: Types.Scalars['String']['input']; + versionId: Types.Scalars['ID']['input']; + isSpecificVersion: Types.Scalars['Boolean']['input']; +}>; + + +export type WorkspaceDatasetAccessPageQuery = { __typename?: 'Query', workspace?: { __typename?: 'Workspace', slug: string, name: string, permissions: { __typename?: 'WorkspacePermissions', manageMembers: boolean, update: boolean, launchNotebookServer: boolean }, countries: Array<{ __typename?: 'Country', flag: string, code: string }> } | null, datasetLink?: { __typename?: 'DatasetLink', id: string, isPinned: boolean, dataset: { __typename?: 'Dataset', name: string, slug: string, id: string, permissions: { __typename?: 'DatasetPermissions', update: boolean, delete: boolean, createVersion: boolean }, version?: { __typename?: 'DatasetVersion', id: string, name: string, createdAt: any } | null, latestVersion?: { __typename?: 'DatasetVersion', id: string, name: string, createdAt: any } | null, workspace?: { __typename?: 'Workspace', slug: string } | null }, workspace: { __typename?: 'Workspace', slug: string }, permissions: { __typename?: 'DatasetLinkPermissions', pin: boolean } } | null }; + +export type WorkspaceDatasetFilesPageQueryVariables = Types.Exact<{ + workspaceSlug: Types.Scalars['String']['input']; + datasetSlug: Types.Scalars['String']['input']; + versionId: Types.Scalars['ID']['input']; + isSpecificVersion: Types.Scalars['Boolean']['input']; +}>; + + +export type WorkspaceDatasetFilesPageQuery = { __typename?: 'Query', workspace?: { __typename?: 'Workspace', slug: string, name: string, permissions: { __typename?: 'WorkspacePermissions', manageMembers: boolean, update: boolean, launchNotebookServer: boolean }, countries: Array<{ __typename?: 'Country', flag: string, code: string }> } | null, datasetLink?: { __typename?: 'DatasetLink', id: string, isPinned: boolean, dataset: { __typename?: 'Dataset', name: string, slug: string, id: string, version?: { __typename?: 'DatasetVersion', id: string, name: string, createdAt: any, files: { __typename?: 'DatasetVersionFilePage', items: Array<{ __typename?: 'DatasetVersionFile', id: string, filename: string, createdAt: any, contentType: string, size: any, uri: string, createdBy?: { __typename?: 'User', displayName: string } | null }> } } | null, latestVersion?: { __typename?: 'DatasetVersion', id: string, name: string, createdAt: any, files: { __typename?: 'DatasetVersionFilePage', items: Array<{ __typename?: 'DatasetVersionFile', id: string, filename: string, createdAt: any, contentType: string, size: any, uri: string, createdBy?: { __typename?: 'User', displayName: string } | null }> } } | null, workspace?: { __typename?: 'Workspace', slug: string } | null, permissions: { __typename?: 'DatasetPermissions', delete: boolean, createVersion: boolean } }, workspace: { __typename?: 'Workspace', slug: string }, permissions: { __typename?: 'DatasetLinkPermissions', pin: boolean } } | null }; export type WorkspaceFilesPageQueryVariables = Types.Exact<{ workspaceSlug: Types.Scalars['String']['input']; @@ -771,82 +789,203 @@ export type WorkspaceDatasetsPageQueryHookResult = ReturnType; export type WorkspaceDatasetsPageSuspenseQueryHookResult = ReturnType; export type WorkspaceDatasetsPageQueryResult = Apollo.QueryResult; -export const WorkspaceDatasetPageDocument = gql` - query WorkspaceDatasetPage($workspaceSlug: String!, $datasetSlug: String!, $versionId: ID!, $isSpecificVersion: Boolean!) { +export const WorkspaceDatasetIndexPageDocument = gql` + query WorkspaceDatasetIndexPage($workspaceSlug: String!, $datasetSlug: String!, $versionId: ID!, $isSpecificVersion: Boolean!) { + workspace(slug: $workspaceSlug) { + slug + ...DatasetLayout_workspace + } datasetLink: datasetLinkBySlug( workspaceSlug: $workspaceSlug datasetSlug: $datasetSlug ) { + ...DatasetLayout_datasetLink id - ...PinDatasetButton_link - ...UploadDatasetVersionDialog_datasetLink - workspace { - slug - name - ...WorkspaceLayout_workspace - } dataset { - id - name - slug + permissions { + update + } description updatedAt + createdAt workspace { - slug name + slug } - ...DeleteDatasetTrigger_dataset - ...DatasetLinksDataGrid_dataset - ...DatasetVersionPicker_dataset createdBy { ...User_user } - createdAt - latestVersion { + version(id: $versionId) @include(if: $isSpecificVersion) { + id + createdAt createdBy { displayName } - createdAt - ...DatasetVersionFilesDataGrid_version - ...DatasetVersionPicker_version + name + ...DatasetLayout_version } - version(id: $versionId) @include(if: $isSpecificVersion) { + latestVersion @skip(if: $isSpecificVersion) { + id + createdAt createdBy { displayName } - createdAt - ...DatasetVersionFilesDataGrid_version - ...DatasetVersionPicker_version + name + ...DatasetLayout_version + } + } + } +} + ${DatasetLayout_WorkspaceFragmentDoc} +${DatasetLayout_DatasetLinkFragmentDoc} +${User_UserFragmentDoc} +${DatasetLayout_VersionFragmentDoc}`; + +/** + * __useWorkspaceDatasetIndexPageQuery__ + * + * To run a query within a React component, call `useWorkspaceDatasetIndexPageQuery` and pass it any options that fit your needs. + * When your component renders, `useWorkspaceDatasetIndexPageQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useWorkspaceDatasetIndexPageQuery({ + * variables: { + * workspaceSlug: // value for 'workspaceSlug' + * datasetSlug: // value for 'datasetSlug' + * versionId: // value for 'versionId' + * isSpecificVersion: // value for 'isSpecificVersion' + * }, + * }); + */ +export function useWorkspaceDatasetIndexPageQuery(baseOptions: Apollo.QueryHookOptions & ({ variables: WorkspaceDatasetIndexPageQueryVariables; skip?: boolean; } | { skip: boolean; }) ) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(WorkspaceDatasetIndexPageDocument, options); } +export function useWorkspaceDatasetIndexPageLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(WorkspaceDatasetIndexPageDocument, options); + } +export function useWorkspaceDatasetIndexPageSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions) { + const options = baseOptions === Apollo.skipToken ? baseOptions : {...defaultOptions, ...baseOptions} + return Apollo.useSuspenseQuery(WorkspaceDatasetIndexPageDocument, options); + } +export type WorkspaceDatasetIndexPageQueryHookResult = ReturnType; +export type WorkspaceDatasetIndexPageLazyQueryHookResult = ReturnType; +export type WorkspaceDatasetIndexPageSuspenseQueryHookResult = ReturnType; +export type WorkspaceDatasetIndexPageQueryResult = Apollo.QueryResult; +export const WorkspaceDatasetAccessPageDocument = gql` + query WorkspaceDatasetAccessPage($workspaceSlug: String!, $datasetSlug: String!, $versionId: ID!, $isSpecificVersion: Boolean!) { + workspace(slug: $workspaceSlug) { + slug + ...DatasetLayout_workspace + } + datasetLink: datasetLinkBySlug( + workspaceSlug: $workspaceSlug + datasetSlug: $datasetSlug + ) { + ...DatasetLayout_datasetLink + id + dataset { + name permissions { update - delete - createVersion + } + ...DatasetLinksDataGrid_dataset + version(id: $versionId) @include(if: $isSpecificVersion) { + ...DatasetLayout_version + } + latestVersion @skip(if: $isSpecificVersion) { + ...DatasetLayout_version } } } } - ${PinDatasetButton_LinkFragmentDoc} -${UploadDatasetVersionDialog_DatasetLinkFragmentDoc} -${WorkspaceLayout_WorkspaceFragmentDoc} -${DeleteDatasetTrigger_DatasetFragmentDoc} + ${DatasetLayout_WorkspaceFragmentDoc} +${DatasetLayout_DatasetLinkFragmentDoc} ${DatasetLinksDataGrid_DatasetFragmentDoc} -${DatasetVersionPicker_DatasetFragmentDoc} -${User_UserFragmentDoc} -${DatasetVersionFilesDataGrid_VersionFragmentDoc} -${DatasetVersionPicker_VersionFragmentDoc}`; +${DatasetLayout_VersionFragmentDoc}`; + +/** + * __useWorkspaceDatasetAccessPageQuery__ + * + * To run a query within a React component, call `useWorkspaceDatasetAccessPageQuery` and pass it any options that fit your needs. + * When your component renders, `useWorkspaceDatasetAccessPageQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useWorkspaceDatasetAccessPageQuery({ + * variables: { + * workspaceSlug: // value for 'workspaceSlug' + * datasetSlug: // value for 'datasetSlug' + * versionId: // value for 'versionId' + * isSpecificVersion: // value for 'isSpecificVersion' + * }, + * }); + */ +export function useWorkspaceDatasetAccessPageQuery(baseOptions: Apollo.QueryHookOptions & ({ variables: WorkspaceDatasetAccessPageQueryVariables; skip?: boolean; } | { skip: boolean; }) ) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(WorkspaceDatasetAccessPageDocument, options); + } +export function useWorkspaceDatasetAccessPageLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(WorkspaceDatasetAccessPageDocument, options); + } +export function useWorkspaceDatasetAccessPageSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions) { + const options = baseOptions === Apollo.skipToken ? baseOptions : {...defaultOptions, ...baseOptions} + return Apollo.useSuspenseQuery(WorkspaceDatasetAccessPageDocument, options); + } +export type WorkspaceDatasetAccessPageQueryHookResult = ReturnType; +export type WorkspaceDatasetAccessPageLazyQueryHookResult = ReturnType; +export type WorkspaceDatasetAccessPageSuspenseQueryHookResult = ReturnType; +export type WorkspaceDatasetAccessPageQueryResult = Apollo.QueryResult; +export const WorkspaceDatasetFilesPageDocument = gql` + query WorkspaceDatasetFilesPage($workspaceSlug: String!, $datasetSlug: String!, $versionId: ID!, $isSpecificVersion: Boolean!) { + workspace(slug: $workspaceSlug) { + slug + ...DatasetLayout_workspace + } + datasetLink: datasetLinkBySlug( + workspaceSlug: $workspaceSlug + datasetSlug: $datasetSlug + ) { + ...DatasetLayout_datasetLink + id + dataset { + name + ...DatasetLinksDataGrid_dataset + version(id: $versionId) @include(if: $isSpecificVersion) { + ...DatasetLayout_version + ...DatasetExplorer_version + } + latestVersion @skip(if: $isSpecificVersion) { + ...DatasetLayout_version + ...DatasetExplorer_version + } + } + } +} + ${DatasetLayout_WorkspaceFragmentDoc} +${DatasetLayout_DatasetLinkFragmentDoc} +${DatasetLinksDataGrid_DatasetFragmentDoc} +${DatasetLayout_VersionFragmentDoc} +${DatasetExplorer_VersionFragmentDoc}`; /** - * __useWorkspaceDatasetPageQuery__ + * __useWorkspaceDatasetFilesPageQuery__ * - * To run a query within a React component, call `useWorkspaceDatasetPageQuery` and pass it any options that fit your needs. - * When your component renders, `useWorkspaceDatasetPageQuery` returns an object from Apollo Client that contains loading, error, and data properties + * To run a query within a React component, call `useWorkspaceDatasetFilesPageQuery` and pass it any options that fit your needs. + * When your component renders, `useWorkspaceDatasetFilesPageQuery` returns an object from Apollo Client that contains loading, error, and data properties * you can use to render your UI. * * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; * * @example - * const { data, loading, error } = useWorkspaceDatasetPageQuery({ + * const { data, loading, error } = useWorkspaceDatasetFilesPageQuery({ * variables: { * workspaceSlug: // value for 'workspaceSlug' * datasetSlug: // value for 'datasetSlug' @@ -855,22 +994,22 @@ ${DatasetVersionPicker_VersionFragmentDoc}`; * }, * }); */ -export function useWorkspaceDatasetPageQuery(baseOptions: Apollo.QueryHookOptions & ({ variables: WorkspaceDatasetPageQueryVariables; skip?: boolean; } | { skip: boolean; }) ) { +export function useWorkspaceDatasetFilesPageQuery(baseOptions: Apollo.QueryHookOptions & ({ variables: WorkspaceDatasetFilesPageQueryVariables; skip?: boolean; } | { skip: boolean; }) ) { const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(WorkspaceDatasetPageDocument, options); + return Apollo.useQuery(WorkspaceDatasetFilesPageDocument, options); } -export function useWorkspaceDatasetPageLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { +export function useWorkspaceDatasetFilesPageLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(WorkspaceDatasetPageDocument, options); + return Apollo.useLazyQuery(WorkspaceDatasetFilesPageDocument, options); } -export function useWorkspaceDatasetPageSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions) { +export function useWorkspaceDatasetFilesPageSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions) { const options = baseOptions === Apollo.skipToken ? baseOptions : {...defaultOptions, ...baseOptions} - return Apollo.useSuspenseQuery(WorkspaceDatasetPageDocument, options); + return Apollo.useSuspenseQuery(WorkspaceDatasetFilesPageDocument, options); } -export type WorkspaceDatasetPageQueryHookResult = ReturnType; -export type WorkspaceDatasetPageLazyQueryHookResult = ReturnType; -export type WorkspaceDatasetPageSuspenseQueryHookResult = ReturnType; -export type WorkspaceDatasetPageQueryResult = Apollo.QueryResult; +export type WorkspaceDatasetFilesPageQueryHookResult = ReturnType; +export type WorkspaceDatasetFilesPageLazyQueryHookResult = ReturnType; +export type WorkspaceDatasetFilesPageSuspenseQueryHookResult = ReturnType; +export type WorkspaceDatasetFilesPageQueryResult = Apollo.QueryResult; export const WorkspaceFilesPageDocument = gql` query WorkspaceFilesPage($workspaceSlug: String!, $page: Int!, $perPage: Int!, $prefix: String!, $query: String, $ignoreHiddenFiles: Boolean) { workspace(slug: $workspaceSlug) { diff --git a/src/workspaces/graphql/queries.graphql b/src/workspaces/graphql/queries.graphql index a301d34b..eb9a6b61 100644 --- a/src/workspaces/graphql/queries.graphql +++ b/src/workspaces/graphql/queries.graphql @@ -275,67 +275,129 @@ query WorkspaceDatasetsPage( } } -query WorkspaceDatasetPage( + +query WorkspaceDatasetIndexPage( $workspaceSlug: String! $datasetSlug: String! $versionId: ID! $isSpecificVersion: Boolean! ) { + workspace(slug:$workspaceSlug) { + slug + ...DatasetLayout_workspace + } datasetLink: datasetLinkBySlug( workspaceSlug: $workspaceSlug datasetSlug: $datasetSlug ) { + + ...DatasetLayout_datasetLink id - ...PinDatasetButton_link - ...UploadDatasetVersionDialog_datasetLink - workspace { - slug - name - ...WorkspaceLayout_workspace - } dataset { - id - name - slug + permissions {update} description updatedAt + createdAt workspace { - slug name + slug } - ...DeleteDatasetTrigger_dataset - ...DatasetLinksDataGrid_dataset - ...DatasetVersionPicker_dataset createdBy { ...User_user } - createdAt - latestVersion { + # If we have a specific version, fetch it + version(id: $versionId) @include(if: $isSpecificVersion) { + id + createdAt createdBy { displayName } - createdAt - ...DatasetVersionFilesDataGrid_version - ...DatasetVersionPicker_version + name + ...DatasetLayout_version } - version(id: $versionId) @include(if: $isSpecificVersion) { + # Or the last version + latestVersion @skip(if: $isSpecificVersion) { + id + createdAt createdBy { displayName } - createdAt - ...DatasetVersionFilesDataGrid_version - ...DatasetVersionPicker_version + name + ...DatasetLayout_version } + } + } +} - permissions { - update - delete - createVersion +query WorkspaceDatasetAccessPage( + $workspaceSlug: String! + $datasetSlug: String! + $versionId: ID! + $isSpecificVersion: Boolean! +) { + workspace(slug:$workspaceSlug) { + slug + ...DatasetLayout_workspace + } + datasetLink: datasetLinkBySlug( + workspaceSlug: $workspaceSlug + datasetSlug: $datasetSlug + ) { + + ...DatasetLayout_datasetLink + id + dataset { + name + permissions {update} + ...DatasetLinksDataGrid_dataset + # If we have a specific version, fetch it + version(id: $versionId) @include(if: $isSpecificVersion) { + ...DatasetLayout_version + } + # Or the last version + latestVersion @skip(if: $isSpecificVersion) { + ...DatasetLayout_version + } + } + } +} + +query WorkspaceDatasetFilesPage( + $workspaceSlug: String! + $datasetSlug: String! + $versionId: ID! + $isSpecificVersion: Boolean! +) { + workspace(slug:$workspaceSlug) { + slug + ...DatasetLayout_workspace + } + datasetLink: datasetLinkBySlug( + workspaceSlug: $workspaceSlug + datasetSlug: $datasetSlug + ) { + ...DatasetLayout_datasetLink + id + dataset { + name + ...DatasetLinksDataGrid_dataset + # If we have a specific version, fetch it + version(id: $versionId) @include(if: $isSpecificVersion) { + ...DatasetLayout_version + ...DatasetExplorer_version + } + # Or the last version + latestVersion @skip(if: $isSpecificVersion) { + ...DatasetLayout_version + ...DatasetExplorer_version } } } } + + + query WorkspaceFilesPage( $workspaceSlug: String! $page: Int! diff --git a/src/workspaces/layouts/WorkspaceLayout/WorkspaceLayout.tsx b/src/workspaces/layouts/WorkspaceLayout/WorkspaceLayout.tsx index 14f49bb1..7c29821d 100644 --- a/src/workspaces/layouts/WorkspaceLayout/WorkspaceLayout.tsx +++ b/src/workspaces/layouts/WorkspaceLayout/WorkspaceLayout.tsx @@ -16,7 +16,8 @@ import Help from "./Help"; import PageContent from "./PageContent"; import Sidebar from "./Sidebar"; import { WorkspaceLayout_WorkspaceFragment } from "./WorkspaceLayout.generated"; -type WorkspaceLayoutProps = { + +export type WorkspaceLayoutProps = { children: ReactElement | ReactElement[]; className?: string; workspace: WorkspaceLayout_WorkspaceFragment;