mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-05-07 17:02:57 +01:00
Merge branch 'feature/materialized-views' of https://github.com/Azure/cosmos-explorer into feature/materialized-views
This commit is contained in:
commit
176bb47cb5
@ -45,6 +45,10 @@ import {
|
|||||||
ConflictResolutionComponentProps,
|
ConflictResolutionComponentProps,
|
||||||
} from "./SettingsSubComponents/ConflictResolutionComponent";
|
} from "./SettingsSubComponents/ConflictResolutionComponent";
|
||||||
import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent";
|
import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent";
|
||||||
|
import {
|
||||||
|
MaterializedViewComponent,
|
||||||
|
MaterializedViewComponentProps,
|
||||||
|
} from "./SettingsSubComponents/MaterializedViewComponent";
|
||||||
import {
|
import {
|
||||||
MongoIndexingPolicyComponent,
|
MongoIndexingPolicyComponent,
|
||||||
MongoIndexingPolicyComponentProps,
|
MongoIndexingPolicyComponentProps,
|
||||||
@ -162,6 +166,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
private shouldShowComputedPropertiesEditor: boolean;
|
private shouldShowComputedPropertiesEditor: boolean;
|
||||||
private shouldShowIndexingPolicyEditor: boolean;
|
private shouldShowIndexingPolicyEditor: boolean;
|
||||||
private shouldShowPartitionKeyEditor: boolean;
|
private shouldShowPartitionKeyEditor: boolean;
|
||||||
|
private isMaterializedView: boolean;
|
||||||
private isVectorSearchEnabled: boolean;
|
private isVectorSearchEnabled: boolean;
|
||||||
private isFullTextSearchEnabled: boolean;
|
private isFullTextSearchEnabled: boolean;
|
||||||
private totalThroughputUsed: number;
|
private totalThroughputUsed: number;
|
||||||
@ -179,6 +184,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
this.shouldShowComputedPropertiesEditor = userContext.apiType === "SQL";
|
this.shouldShowComputedPropertiesEditor = userContext.apiType === "SQL";
|
||||||
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
|
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
|
||||||
this.shouldShowPartitionKeyEditor = userContext.apiType === "SQL" && isRunningOnPublicCloud();
|
this.shouldShowPartitionKeyEditor = userContext.apiType === "SQL" && isRunningOnPublicCloud();
|
||||||
|
this.isMaterializedView =
|
||||||
|
!!this.collection?.materializedViewDefinition() || !!this.collection?.materializedViews();
|
||||||
this.isVectorSearchEnabled = isVectorSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
|
this.isVectorSearchEnabled = isVectorSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
|
||||||
this.isFullTextSearchEnabled = isFullTextSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
|
this.isFullTextSearchEnabled = isFullTextSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
|
||||||
|
|
||||||
@ -1272,6 +1279,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
explorer: this.props.settingsTab.getContainer(),
|
explorer: this.props.settingsTab.getContainer(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const materializedViewComponentProps: MaterializedViewComponentProps = {
|
||||||
|
collection: this.collection,
|
||||||
|
};
|
||||||
|
|
||||||
const tabs: SettingsV2TabInfo[] = [];
|
const tabs: SettingsV2TabInfo[] = [];
|
||||||
if (!hasDatabaseSharedThroughput(this.collection) && this.offer) {
|
if (!hasDatabaseSharedThroughput(this.collection) && this.offer) {
|
||||||
tabs.push({
|
tabs.push({
|
||||||
@ -1335,6 +1346,13 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.isMaterializedView) {
|
||||||
|
tabs.push({
|
||||||
|
tab: SettingsV2TabTypes.MaterializedViewTab,
|
||||||
|
content: <MaterializedViewComponent {...materializedViewComponentProps} />,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const pivotProps: IPivotProps = {
|
const pivotProps: IPivotProps = {
|
||||||
onLinkClick: this.onPivotChange,
|
onLinkClick: this.onPivotChange,
|
||||||
selectedKey: SettingsV2TabTypes[this.state.selectedTab],
|
selectedKey: SettingsV2TabTypes[this.state.selectedTab],
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
import { FontIcon, Link, Stack, Text } from "@fluentui/react";
|
||||||
|
import React from "react";
|
||||||
|
import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||||
|
import { MaterializedViewSourceComponent } from "./MaterializedViewSourceComponent";
|
||||||
|
import { MaterializedViewTargetComponent } from "./MaterializedViewTargetComponent";
|
||||||
|
|
||||||
|
export interface MaterializedViewComponentProps {
|
||||||
|
collection: ViewModels.Collection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MaterializedViewComponent: React.FC<MaterializedViewComponentProps> = ({ collection }) => {
|
||||||
|
const isTargetContainer = !!collection?.materializedViewDefinition();
|
||||||
|
const isSourceContainer = !!collection?.materializedViews();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack tokens={{ childrenGap: 8 }} styles={{ root: { maxWidth: 600 } }}>
|
||||||
|
<Stack horizontal verticalAlign="center" wrap tokens={{ childrenGap: 8 }}>
|
||||||
|
<Text styles={{ root: { fontWeight: 600 } }}>This container has the following views defined for it.</Text>
|
||||||
|
<Text>
|
||||||
|
<Link href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views#defining-materialized-views">
|
||||||
|
Learn more
|
||||||
|
<FontIcon iconName="NavigateExternalInline" style={{ marginLeft: "4px" }} />
|
||||||
|
</Link>{" "}
|
||||||
|
about how to define materialized views and how to use them.
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
{isSourceContainer && <MaterializedViewSourceComponent collection={collection} />}
|
||||||
|
{isTargetContainer && <MaterializedViewTargetComponent collection={collection} />}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,96 @@
|
|||||||
|
import { PrimaryButton } from "@fluentui/react";
|
||||||
|
import { loadMonaco } from "Explorer/LazyMonaco";
|
||||||
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
|
import * as monaco from "monaco-editor";
|
||||||
|
import React, { useEffect, useRef } from "react";
|
||||||
|
import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||||
|
|
||||||
|
export interface MaterializedViewSourceComponentProps {
|
||||||
|
collection: ViewModels.Collection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MaterializedViewSourceComponent: React.FC<MaterializedViewSourceComponentProps> = ({ collection }) => {
|
||||||
|
const editorContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>(null);
|
||||||
|
|
||||||
|
const materializedViews = collection?.materializedViews() ?? [];
|
||||||
|
|
||||||
|
// Helper function to fetch the definition and partition key of targetContainer by traversing through all collections and matching id from MaterializedViews[] with collection id.
|
||||||
|
const getViewDetails = (viewId: string): { definition: string; partitionKey: string[] } => {
|
||||||
|
let definition = "";
|
||||||
|
let partitionKey: string[] = [];
|
||||||
|
|
||||||
|
useDatabases.getState().databases.find((database) => {
|
||||||
|
const collection = database.collections().find((collection) => collection.id() === viewId);
|
||||||
|
if (collection) {
|
||||||
|
const materializedViewDefinition = collection.materializedViewDefinition();
|
||||||
|
materializedViewDefinition && (definition = materializedViewDefinition.definition);
|
||||||
|
collection.partitionKey?.paths && (partitionKey = collection.partitionKey.paths);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { definition, partitionKey };
|
||||||
|
};
|
||||||
|
|
||||||
|
//JSON value for the editor using the fetched id and definitions.
|
||||||
|
const jsonValue = JSON.stringify(
|
||||||
|
materializedViews.map((view) => {
|
||||||
|
const { definition, partitionKey } = getViewDetails(view.id);
|
||||||
|
return {
|
||||||
|
name: view.id,
|
||||||
|
partitionKey: partitionKey.join(", "),
|
||||||
|
definition,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initialize Monaco editor with the computed JSON value.
|
||||||
|
useEffect(() => {
|
||||||
|
let disposed = false;
|
||||||
|
const initMonaco = async () => {
|
||||||
|
const monacoInstance = await loadMonaco();
|
||||||
|
if (disposed || !editorContainerRef.current) return;
|
||||||
|
|
||||||
|
editorRef.current = monacoInstance.editor.create(editorContainerRef.current, {
|
||||||
|
value: jsonValue,
|
||||||
|
language: "json",
|
||||||
|
ariaLabel: "Materialized Views JSON",
|
||||||
|
readOnly: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
initMonaco();
|
||||||
|
return () => {
|
||||||
|
disposed = true;
|
||||||
|
editorRef.current?.dispose();
|
||||||
|
};
|
||||||
|
}, [jsonValue]);
|
||||||
|
|
||||||
|
// Update the editor when the jsonValue changes.
|
||||||
|
useEffect(() => {
|
||||||
|
if (editorRef.current) {
|
||||||
|
editorRef.current.setValue(jsonValue);
|
||||||
|
}
|
||||||
|
}, [jsonValue]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
ref={editorContainerRef}
|
||||||
|
style={{
|
||||||
|
height: 250,
|
||||||
|
border: "1px solid #ccc",
|
||||||
|
borderRadius: 4,
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<PrimaryButton
|
||||||
|
text="Add view"
|
||||||
|
styles={{ root: { width: "fit-content", marginTop: 12 } }}
|
||||||
|
onClick={() => console.log("Add view clicked")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,43 @@
|
|||||||
|
import { Stack, Text } from "@fluentui/react";
|
||||||
|
import * as React from "react";
|
||||||
|
import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||||
|
|
||||||
|
export interface MaterializedViewTargetComponentProps {
|
||||||
|
collection: ViewModels.Collection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MaterializedViewTargetComponent: React.FC<MaterializedViewTargetComponentProps> = ({ collection }) => {
|
||||||
|
const materializedViewDefinition = collection?.materializedViewDefinition();
|
||||||
|
|
||||||
|
const textHeadingStyle = {
|
||||||
|
root: { fontWeight: "600", fontSize: 16 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const valueBoxStyle = {
|
||||||
|
root: {
|
||||||
|
backgroundColor: "#f3f3f3",
|
||||||
|
padding: "5px 10px",
|
||||||
|
borderRadius: "4px",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack tokens={{ childrenGap: 15 }} styles={{ root: { maxWidth: 600 } }}>
|
||||||
|
<Text styles={textHeadingStyle}>Materialized View Settings</Text>
|
||||||
|
|
||||||
|
<Stack tokens={{ childrenGap: 5 }}>
|
||||||
|
<Text styles={{ root: { fontWeight: "600" } }}>Source container</Text>
|
||||||
|
<Stack styles={valueBoxStyle}>
|
||||||
|
<Text>{materializedViewDefinition?.sourceCollectionId}</Text>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack tokens={{ childrenGap: 5 }}>
|
||||||
|
<Text styles={{ root: { fontWeight: "600" } }}>Materialized view definition</Text>
|
||||||
|
<Stack styles={valueBoxStyle}>
|
||||||
|
<Text>{materializedViewDefinition?.definition}</Text>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
@ -57,6 +57,7 @@ export enum SettingsV2TabTypes {
|
|||||||
ComputedPropertiesTab,
|
ComputedPropertiesTab,
|
||||||
ContainerVectorPolicyTab,
|
ContainerVectorPolicyTab,
|
||||||
ThroughputBucketsTab,
|
ThroughputBucketsTab,
|
||||||
|
MaterializedViewTab,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ContainerPolicyTabTypes {
|
export enum ContainerPolicyTabTypes {
|
||||||
@ -171,6 +172,8 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => {
|
|||||||
return "Container Policies";
|
return "Container Policies";
|
||||||
case SettingsV2TabTypes.ThroughputBucketsTab:
|
case SettingsV2TabTypes.ThroughputBucketsTab:
|
||||||
return "Throughput Buckets";
|
return "Throughput Buckets";
|
||||||
|
case SettingsV2TabTypes.MaterializedViewTab:
|
||||||
|
return "Materialized Views (Preview)";
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown tab ${tab}`);
|
throw new Error(`Unknown tab ${tab}`);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { DatabaseRegular, DocumentMultipleRegular, SettingsRegular } from "@fluentui/react-icons";
|
import { DatabaseRegular, DocumentMultipleRegular, EyeRegular, SettingsRegular } from "@fluentui/react-icons";
|
||||||
import { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
import { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
||||||
import { collectionWasOpened } from "Explorer/MostRecentActivity/MostRecentActivity";
|
import { collectionWasOpened } from "Explorer/MostRecentActivity/MostRecentActivity";
|
||||||
import TabsBase from "Explorer/Tabs/TabsBase";
|
import TabsBase from "Explorer/Tabs/TabsBase";
|
||||||
@ -30,6 +30,7 @@ export const shouldShowScriptNodes = (): boolean => {
|
|||||||
const TreeDatabaseIcon = <DatabaseRegular fontSize={16} />;
|
const TreeDatabaseIcon = <DatabaseRegular fontSize={16} />;
|
||||||
const TreeSettingsIcon = <SettingsRegular fontSize={16} />;
|
const TreeSettingsIcon = <SettingsRegular fontSize={16} />;
|
||||||
const TreeCollectionIcon = <DocumentMultipleRegular fontSize={16} />;
|
const TreeCollectionIcon = <DocumentMultipleRegular fontSize={16} />;
|
||||||
|
const MaterializedViewCollectionIcon = <EyeRegular fontSize={16} />; //check icon
|
||||||
|
|
||||||
export const createSampleDataTreeNodes = (sampleDataResourceTokenCollection: ViewModels.CollectionBase): TreeNode[] => {
|
export const createSampleDataTreeNodes = (sampleDataResourceTokenCollection: ViewModels.CollectionBase): TreeNode[] => {
|
||||||
const updatedSampleTree: TreeNode = {
|
const updatedSampleTree: TreeNode = {
|
||||||
@ -81,7 +82,7 @@ export const createSampleDataTreeNodes = (sampleDataResourceTokenCollection: Vie
|
|||||||
return [updatedSampleTree];
|
return [updatedSampleTree];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createResourceTokenTreeNodes = (collection: ViewModels.CollectionBase): TreeNode[] => {
|
export const createResourceTokenTreeNodes = (collection: ViewModels.Collection): TreeNode[] => {
|
||||||
if (!collection) {
|
if (!collection) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@ -111,7 +112,7 @@ export const createResourceTokenTreeNodes = (collection: ViewModels.CollectionBa
|
|||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
children,
|
children,
|
||||||
className: "collectionNode",
|
className: "collectionNode",
|
||||||
iconSrc: TreeCollectionIcon,
|
iconSrc: collection.materializedViewDefinition() ? MaterializedViewCollectionIcon : TreeCollectionIcon,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
// Rewritten version of expandCollapseCollection
|
// Rewritten version of expandCollapseCollection
|
||||||
useSelectedNode.getState().setSelectedNode(collection);
|
useSelectedNode.getState().setSelectedNode(collection);
|
||||||
@ -229,7 +230,7 @@ export const buildCollectionNode = (
|
|||||||
children: children,
|
children: children,
|
||||||
className: "collectionNode",
|
className: "collectionNode",
|
||||||
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection),
|
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection),
|
||||||
iconSrc: TreeCollectionIcon,
|
iconSrc: collection.materializedViewDefinition() ? MaterializedViewCollectionIcon : TreeCollectionIcon,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
useSelectedNode.getState().setSelectedNode(collection);
|
useSelectedNode.getState().setSelectedNode(collection);
|
||||||
collection.openTab();
|
collection.openTab();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user