Skip to content

Commit

Permalink
Get add data source working
Browse files Browse the repository at this point in the history
  • Loading branch information
SeanLeRoy committed May 9, 2024
1 parent f6d8d58 commit 119c6df
Show file tree
Hide file tree
Showing 21 changed files with 356 additions and 378 deletions.
13 changes: 4 additions & 9 deletions packages/core/components/Modal/CsvManifest/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { PrimaryButton } from "@fluentui/react";
import classNames from "classnames";
import * as React from "react";
import { useDispatch, useSelector } from "react-redux";
import { useSelector } from "react-redux";

import { ModalProps } from "..";
import BaseModal from "../BaseModal";
Expand All @@ -9,8 +10,6 @@ import * as modalSelectors from "../selectors";
import { interaction, selection } from "../../../state";

import styles from "./CsvManifest.module.css";
import classNames from "classnames";
import { CsvService } from "../../../services";

/**
* Modal overlay for selecting columns to be included in a CSV manifest download of
Expand All @@ -23,18 +22,14 @@ export default function CsvManifest({ onDismiss }: ModalProps) {
const [selectedAnnotations, setSelectedAnnotations] = React.useState(
annotationsPreviouslySelected
);
const { databaseService, fileDownloadService } = useSelector(interaction.selectors.getPlatformDependentServices);
const csvService = new CsvService({
databaseService,
downloadService: fileDownloadService,
});
const csvService = useSelector(interaction.selectors.getCsvService);
const fileSelection = useSelector(selection.selectors.getFileSelection);

const onDownload = () => {
const downloadSelection = async () => {
const selections = fileSelection.toCompactSelectionList();
const selectedAnnotationNames = selectedAnnotations.map((annotation) => annotation.name);
const buffer = await csvService.getSelectionAsBuffer(selectedAnnotationNames, selections);
const buffer = await csvService.getCsvAsBytes({ annotations: selectedAnnotationNames, selections }, "");

// Generate a download link (ensure to revoke the object URL after the download).
// We could use window.showSaveFilePicker() but it is only supported in Chrome.
Expand Down
31 changes: 15 additions & 16 deletions packages/core/components/Modal/DataSourcePrompt/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { DefaultButton, Icon, TextField } from "@fluentui/react";
import { throttle } from "lodash";
import * as React from "react";
import { useDispatch } from "react-redux";
import { useDispatch, useSelector } from "react-redux";

import { ModalProps } from "..";
import BaseModal from "../BaseModal";
import FileExplorerURL from "../../../entity/FileExplorerURL";
import { interaction, selection } from "../../../state";

import styles from "./DataSourcePrompt.module.css";
import FileExplorerURL from "../../../entity/FileExplorerURL";

interface Props extends ModalProps {
isEditing?: boolean;
Expand All @@ -34,42 +34,41 @@ export default function DataSourcePrompt({ onDismiss }: Props) {

const [dataSourceURL, setDataSourceURL] = React.useState("");
const [isDataSourceDetailExpanded, setIsDataSourceDetailExpanded] = React.useState(false);
const { databaseService } = useSelector(interaction.selectors.getPlatformDependentServices);

const onChooseFile = (evt: React.FormEvent<HTMLInputElement>) => {
const selectedFile = (evt.target as HTMLInputElement).files?.[0];
console.log(selectedFile);
if (selectedFile) {
const reader = new FileReader();
reader.onload = () => {
const uri = reader.result as string;
console.log(uri);
// Grab name minus extension
const name = selectedFile.name.split(".").slice(0, -1).join("");
const addDataSource = async () => {
await databaseService.addDataSource(name, selectedFile);
dispatch(
selection.actions.addQuery({
name: "New Query",
name: `New ${name} Query`,
url: FileExplorerURL.encode({
collection: {
uri,
name: selectedFile.name,
name,
version: 1,
},
}),
})
);
onDismiss();
};
reader.readAsDataURL(selectedFile);
}
addDataSource();
onDismiss();
}
onDismiss();
};
const onEnterURL = throttle(
(evt: React.FormEvent) => {
evt.preventDefault();
const name = dataSourceURL.substring(dataSourceURL.lastIndexOf("/") + 1);
dispatch(
selection.actions.addQuery({
name: "New Query",
name: `New ${name} Query`,
url: FileExplorerURL.encode({
collection: {
name: dataSourceURL.substring(dataSourceURL.lastIndexOf("/") + 1),
name,
uri: dataSourceURL,
version: 1,
},
Expand Down
130 changes: 46 additions & 84 deletions packages/core/components/QuerySidebar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import {
ContextualMenuItemType,
DirectionalHint,
Icon,
IconButton,
TextField,
} from "@fluentui/react";
import classNames from "classnames";
import * as React from "react";
Expand All @@ -14,11 +12,11 @@ import { HELP_OPTIONS } from "./tutorials";
import { ModalType } from "../Modal";
import SvgIcon from "../SvgIcon";
import FileExplorerURL from "../../entity/FileExplorerURL";
import Tutorial from "../../entity/Tutorial";
import { interaction, metadata, selection } from "../../state";
import { AICS_LOGO } from "../../icons";

import styles from "./QuerySidebar.module.css";
import Tutorial from "../../entity/Tutorial";

interface QuerySidebarProps {
className?: string;
Expand Down Expand Up @@ -47,90 +45,54 @@ export default function QuerySidebar(props: QuerySidebarProps) {
}, [queries, selectedQuery, dispatch]);

const helpMenuOptions = React.useMemo(() => HELP_OPTIONS(dispatch), [dispatch]);
const addQueryOptions = React.useMemo(() => {
const onEnterURL = (evt: React.FormEvent) => {
evt.preventDefault();
// Form submission typing on the TextField is yucky, so we'll just cast the event target
const fileExplorerUrl = (evt.currentTarget as any)[0].value;
dispatch(selection.actions.addQuery({ name: "New query", url: fileExplorerUrl }));
};

return [
{
key: "new-queries-section",
itemType: ContextualMenuItemType.Section,
sectionProps: {
bottomDivider: true,
title: "New Query",
items: [
...(isAICSEmployee
? [
{
key: "AICS FMS",
text: "AICS FMS",
iconProps: { iconName: "Database" },
onClick: () => {
dispatch(
selection.actions.addQuery({
name: "New AICS Query",
url: FileExplorerURL.DEFAULT_FMS_URL,
})
);
},
secondaryText: "Data Source",
},
]
: []),
...collections
.filter((collection) => !!collection.uri)
.map((collection) => ({
key: collection.id,
text: `${
collection.name
} (${collection.created.toLocaleDateString()})`,
iconProps: { iconName: "Folder" },
onClick: () => {
dispatch(
selection.actions.addQuery({
name: `New ${collection.name} query`,
url: collection.uri as string,
})
);
},
secondaryText: "Data Source",
})),
{
key: "New Data Source...",
text: "New Data Source...",
iconProps: { iconName: "NewFolder" },
onClick: () => {
dispatch(
interaction.actions.setVisibleModal(ModalType.DataSourcePrompt)
);
},
const addQueryOptions = React.useMemo(() => ([
...(isAICSEmployee
? [
{
key: "AICS FMS",
text: "AICS FMS",
iconProps: { iconName: "Database" },
onClick: () => {
dispatch(
selection.actions.addQuery({
name: "New AICS Query",
url: FileExplorerURL.DEFAULT_FMS_URL,
})
);
},
],
},
},
{
key: "Import from URL",
text: "Import from URL",
iconProps: { iconName: "Import" },
subMenuProps: {
className: styles.buttonMenu,
items: [{ key: "placeholder" }],
onRenderMenuList: () => (
<form className={styles.importForm} onSubmit={onEnterURL}>
<TextField
placeholder="Paste URL here..."
iconProps={{ iconName: "ReturnKey" }}
/>
</form>
),
secondaryText: "Data Source",
},
]
: []),
...collections
.filter((collection) => !!collection.uri)
.map((collection) => ({
key: collection.id,
text: `${
collection.name
} (${collection.created.toLocaleDateString()})`,
iconProps: { iconName: "Folder" },
onClick: () => {
dispatch(
selection.actions.addQuery({
name: `New ${collection.name} query`,
url: collection.uri as string,
})
);
},
secondaryText: "Data Source",
})),
{
key: "New Data Source...",
text: "New Data Source...",
iconProps: { iconName: "NewFolder" },
onClick: () => {
dispatch(
interaction.actions.setVisibleModal(ModalType.DataSourcePrompt)
);
},
];
}, [dispatch, collections, isAICSEmployee]);
},
]), [dispatch, collections, isAICSEmployee]);

if (!isExpanded) {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ export const SHARE_VIEW_TUTORIAL = new Tutorial("Sharing current view (URL)")
.addStep({
targetId: Tutorial.ADD_QUERY_BUTTON_ID,
message:
'Start a new query from a URL by clicking the New Query + button then "Import from URL" then pasting or typing a URL into the textbox. Press Enter to load the query.',
'If in a web browser paste it into your URL bar in a new tab otherwise start a new query from a URL by clicking the New Query + button then "Import from URL" then pasting or typing a URL into the textbox. Press Enter to load the query.',
});
2 changes: 1 addition & 1 deletion packages/core/entity/SQLBuilder/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export default class SQLBuilder {
return `
${this.isSummarizing ? "SUMMARIZE" : ""}
SELECT ${this.selectStatement}
FROM ${this.fromStatement}
FROM "${this.fromStatement}"
${this.whereClauses.length ? `WHERE (${this.whereClauses.join(") AND (")})` : ""}
${this.orderByClause ? `ORDER BY ${this.orderByClause}` : ""}
${this.offsetNum !== undefined ? `OFFSET ${this.offsetNum}` : ""}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import SQLBuilder from "../../../entity/SQLBuilder";

interface Config {
databaseService: DatabaseService;
dataSourceName: string;
}

interface DescribeQueryResult {
Expand All @@ -27,8 +28,10 @@ interface SummarizeQueryResult {
*/
export default class DatabaseAnnotationService implements AnnotationService {
private readonly databaseService: DatabaseService;
private readonly dataSourceName: string;

constructor(config: Config = { databaseService: new DatabaseServiceNoop() }) {
constructor(config: Config = { dataSourceName: "Unknown", databaseService: new DatabaseServiceNoop() }) {
this.dataSourceName = config.dataSourceName;
this.databaseService = config.databaseService;
}

Expand All @@ -49,7 +52,7 @@ export default class DatabaseAnnotationService implements AnnotationService {
* Fetch all annotations.
*/
public async fetchAnnotations(): Promise<Annotation[]> {
const sql = `DESCRIBE ${this.databaseService.table}`;
const sql = `DESCRIBE "${this.dataSourceName}"`;
const rows = (await this.databaseService.query(sql)) as DescribeQueryResult[];
return rows.map(
(row) =>
Expand All @@ -69,7 +72,7 @@ export default class DatabaseAnnotationService implements AnnotationService {
const select_key = "select_key";
const sql = new SQLBuilder()
.select(`DISTINCT "${annotation}" AS ${select_key}`)
.from(this.databaseService.table)
.from(this.dataSourceName)
.toSQL();
const rows = await this.databaseService.query(sql);
return [
Expand Down Expand Up @@ -108,7 +111,7 @@ export default class DatabaseAnnotationService implements AnnotationService {

const sqlBuilder = new SQLBuilder()
.select(`DISTINCT "${hierarchy[path.length]}"`)
.from(this.databaseService.table);
.from(this.dataSourceName);
Object.keys(filtersByAnnotation).forEach((annotation) => {
const annotationValues = filtersByAnnotation[annotation];
if (annotationValues[0] === null) {
Expand All @@ -130,7 +133,7 @@ export default class DatabaseAnnotationService implements AnnotationService {
public async fetchAvailableAnnotationsForHierarchy(annotations: string[]): Promise<string[]> {
const sql = new SQLBuilder()
.summarize()
.from(this.databaseService.table)
.from(this.dataSourceName)
.where(annotations.map((annotation) => `"${annotation}" IS NOT NULL`))
.toSQL();
const rows = (await this.databaseService.query(sql)) as SummarizeQueryResult[];
Expand Down
Loading

0 comments on commit 119c6df

Please sign in to comment.