diff --git a/README.rst b/README.rst index 1cb7322141..c35f811464 100644 --- a/README.rst +++ b/README.rst @@ -213,3 +213,39 @@ MLflow is currently maintained by the following core members with significant co - `Serena Ruan `_ - `Weichen Xu `_ - `Yuki Watanabe `_ + + +Running MLFLOW Locally +---------------------- + +**Steps** + +1. **Clone the Git Repository** + - Repository URL: [MLFlow GitHub Repo](https://github.com/oneconvergence/mlflow) + - Branch: `d3x-v2.15.1` + +2. **Navigate to the Project Directory** + ``cd mlflow/server/js`` +3. **Install Dependencies** + ``yarn install`` +4. **Add Proxy in package.json** + ``"proxy": "your.domain.and.port"`` +5. **Login to dkubex and add _oauth2_proxy in cookies** + ``_oauth2_proxy: <_oauth2_proxy cookie from dkubex user>`` +6. **Modify FetchUtils.js** + - Navigate to ``mlflow/server/js/src/common/utils/FetchUtils.js`` + - Update the getAjaxUrl function as follows: + +.. code-block:: ts + + export const getAjaxUrl = (relativeUrl: any) => { + // @ts-expect-error TS(4111): Property 'MLFLOW_USE_ABSOLUTE_AJAX_URLS' comes from an in... Remove this comment to see the full error message + if (process.env.MLFLOW_USE_ABSOLUTE_AJAX_URLS === 'true' && !relativeUrl.startsWith('/')) { + return '/mlflow/' + relativeUrl; + } + return '/mlflow/' + relativeUrl; + }; + +7. **Start the Application** + ``npm start`` + diff --git a/mlflow/server/js/package.json b/mlflow/server/js/package.json index 8ba13b80de..e05d986014 100644 --- a/mlflow/server/js/package.json +++ b/mlflow/server/js/package.json @@ -45,6 +45,7 @@ "dateformat": "3.0.3", "file-saver": "1.3.8", "font-awesome": "4.7.0", + "fuse.js": "^7.0.0", "graphql": "^15.5.0", "http-proxy-middleware": "^1.0.3", "immutable": "3.8.1", diff --git a/mlflow/server/js/public/lib/artifact-trace-viewer/trace_embedding.html b/mlflow/server/js/public/lib/artifact-trace-viewer/trace_embedding.html new file mode 100644 index 0000000000..120a6380e2 --- /dev/null +++ b/mlflow/server/js/public/lib/artifact-trace-viewer/trace_embedding.html @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + diff --git a/mlflow/server/js/public/lib/artifact-trace-viewer/trace_viewer_full.html b/mlflow/server/js/public/lib/artifact-trace-viewer/trace_viewer_full.html new file mode 100644 index 0000000000..15169a4572 --- /dev/null +++ b/mlflow/server/js/public/lib/artifact-trace-viewer/trace_viewer_full.html @@ -0,0 +1,10174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mlflow/server/js/src/common/utils/FileUtils.ts b/mlflow/server/js/src/common/utils/FileUtils.ts index d06500df46..491bb621d2 100644 --- a/mlflow/server/js/src/common/utils/FileUtils.ts +++ b/mlflow/server/js/src/common/utils/FileUtils.ts @@ -11,8 +11,15 @@ export const getBasename = (path: any) => { }; export const getExtension = (path: any) => { - const parts = path.split(/[./]/); - return parts[parts.length - 1]; + const tracefileRegex = /.*\.(pt\.trace\.json(?:\.gz|\.zip)?)$/; + const traceMatch = path.match(tracefileRegex); + + if (traceMatch) { + return traceMatch[1]; + } else { + const parts = path.split(/[./]/); + return parts[parts.length - 1]; + } }; export const getLanguage = (path: any) => { @@ -62,3 +69,4 @@ export const HTML_EXTENSIONS = new Set(['html']); export const MAP_EXTENSIONS = new Set(['geojson']); export const PDF_EXTENSIONS = new Set(['pdf']); export const DATA_EXTENSIONS = new Set(['csv', 'tsv']); +export const TRACE_EXTENSIONS = new Set(['pt.trace.json', 'pt.trace.json.gz']); diff --git a/mlflow/server/js/src/common/utils/Utils.tsx b/mlflow/server/js/src/common/utils/Utils.tsx index 6ae95fe4dc..a27c4ec811 100644 --- a/mlflow/server/js/src/common/utils/Utils.tsx +++ b/mlflow/server/js/src/common/utils/Utils.tsx @@ -781,7 +781,8 @@ class Utils { // @ts-expect-error TS(2345): Argument of type 'string | string[] | ParsedQs | P... Remove this comment to see the full error message const lineSmoothness = params['line_smoothness'] ? parseFloat(params['line_smoothness']) : 0; // @ts-expect-error TS(2345): Argument of type 'string | string[] | ParsedQs | P... Remove this comment to see the full error message - const layout = params['plot_layout'] ? JSON.parse(params['plot_layout']) : { autosize: true }; + const layoutStr = params['plot_layout'] ? params['plot_layout'].replaceAll(" ","") : undefined + const layout = layoutStr ? JSON.parse(layoutStr) : { autosize: true }; // Default to displaying all runs, i.e. to deselectedCurves being empty const deselectedCurves = params['deselected_curves'] ? // @ts-expect-error TS(2345): Argument of type 'string | string[] | ParsedQs | P... Remove this comment to see the full error message diff --git a/mlflow/server/js/src/experiment-tracking/components/ArtifactView.css b/mlflow/server/js/src/experiment-tracking/components/ArtifactView.css index ba805f63e8..f552536bcd 100644 --- a/mlflow/server/js/src/experiment-tracking/components/ArtifactView.css +++ b/mlflow/server/js/src/experiment-tracking/components/ArtifactView.css @@ -1,7 +1,9 @@ div.artifact-view { - height: 690px; + height: 100%; + width: 100%; display: flex; overflow: hidden; + position: relative; } .artifact-left { diff --git a/mlflow/server/js/src/experiment-tracking/components/ExperimentListView.tsx b/mlflow/server/js/src/experiment-tracking/components/ExperimentListView.tsx index 44f675fc91..7eb0a5d514 100644 --- a/mlflow/server/js/src/experiment-tracking/components/ExperimentListView.tsx +++ b/mlflow/server/js/src/experiment-tracking/components/ExperimentListView.tsx @@ -28,6 +28,7 @@ import { RenameExperimentModal } from './modals/RenameExperimentModal'; import { IconButton } from '../../common/components/IconButton'; import { withRouterNext } from '../../common/utils/withRouterNext'; import { ExperimentEntity } from '../types'; +import ProjectListView, { filterExperimentsByProject } from './ProjectListView'; type Props = { activeExperimentIds: string[]; @@ -39,11 +40,12 @@ type State = any; export class ExperimentListView extends Component { list: any; - + selectedExperiments: string[] = JSON.parse(localStorage.getItem('selected-experiments') || '[]'); state = { - checkedKeys: this.props.activeExperimentIds, + checkedKeys: JSON.parse(localStorage.getItem('selected-experiments') || '[]'), hidden: false, searchInput: '', + project: localStorage.getItem('mlflow-exp-project') || 'All', showCreateExperimentModal: false, showDeleteExperimentModal: false, showRenameExperimentModal: false, @@ -55,19 +57,23 @@ export class ExperimentListView extends Component { this.list = ref; }; - componentDidUpdate = () => { + componentDidUpdate = (prevProps: Props) => { // Ensure the filter is applied if (this.list) { this.list.forceUpdateGrid(); } - }; + const exps = JSON.parse(localStorage.getItem('selected-experiments') || '[]'); + if (prevProps.activeExperimentIds.length !== exps.length) { + this.pushExperimentRoute(); + } + } filterExperiments = (searchInput: any) => { - const { experiments } = this.props; + const experiments = filterExperimentsByProject(this.props.experiments, this.state.project) const lowerCasedSearchInput = searchInput.toLowerCase(); return lowerCasedSearchInput === '' - ? this.props.experiments - : experiments.filter(({ name }) => name.toLowerCase().includes(lowerCasedSearchInput)); + ? experiments + : experiments.filter(({ name }: any) => name.toLowerCase().includes(lowerCasedSearchInput)); }; handleSearchInputChange = (event: any) => { @@ -76,6 +82,14 @@ export class ExperimentListView extends Component { }); }; + handleProjectChange = (value: any) => { + const experiments = filterExperimentsByProject(this.props.experiments, value) + localStorage.setItem('mlflow-exp-project', value); + this.setState((prevState: any, props: any) => { + return {project: value, checkedKeys: experiments.length ?[experiments[0].experiment_id] : [] }; + }, this.pushExperimentRoute); + }; + updateSelectedExperiment = (experimentId: any, experimentName: any) => { this.setState({ selectedExperimentId: experimentId, @@ -125,6 +139,9 @@ export class ExperimentListView extends Component { this.updateSelectedExperiment('0', ''); }; + persistSelectedExperiemnts = (selected: any) => { + localStorage.setItem('selected-experiments', JSON.stringify(selected)); + } // Add a key if it does not exist, remove it if it does // Always keep at least one experiment checked if it is only the active one. handleCheck = (isChecked: any, key: any) => { @@ -136,11 +153,13 @@ export class ExperimentListView extends Component { if (isChecked === false && props.activeExperimentIds.length !== 1) { checkedKeys = props.activeExperimentIds.filter((i: any) => i !== key); } + this.persistSelectedExperiemnts(checkedKeys); return { checkedKeys: checkedKeys }; }, this.pushExperimentRoute); }; pushExperimentRoute = () => { + this.persistSelectedExperiemnts(this.state.checkedKeys); if (this.state.checkedKeys.length > 0) { const route = this.state.checkedKeys.length === 1 @@ -257,6 +276,8 @@ export class ExperimentListView extends Component { experimentId={this.state.selectedExperimentId} experimentName={this.state.selectedExperimentName} /> +
+
Experiments @@ -276,6 +297,7 @@ export class ExperimentListView extends Component { />
+ { - const sorted = [...experiments].sort(Utils.compareExperiments); - return sorted.find(({ lifecycleStage }) => lifecycleStage === 'active'); + const selectedProject = localStorage.getItem('mlflow-exp-project'); + let filteredExperiments = experiments + if(selectedProject){ + filteredExperiments = filterExperimentsByProject(experiments, selectedProject); + } + const sorted = [...filteredExperiments].sort(Utils.compareExperiments); + return sorted.find(({ lifecycleStage }) => lifecycleStage === 'active' ); }; const HomePage = () => { diff --git a/mlflow/server/js/src/experiment-tracking/components/MetricsPlotPanel.tsx b/mlflow/server/js/src/experiment-tracking/components/MetricsPlotPanel.tsx index 69d45a2f24..66fa13acc0 100644 --- a/mlflow/server/js/src/experiment-tracking/components/MetricsPlotPanel.tsx +++ b/mlflow/server/js/src/experiment-tracking/components/MetricsPlotPanel.tsx @@ -32,7 +32,7 @@ export const METRICS_PLOT_POLLING_INTERVAL_MS = 10 * 1000; // 10 seconds // A run is considered as 'hanging' if its status is 'RUNNING' but its latest metric was logged // prior to this threshold. The metrics plot doesn't automatically update hanging runs. export const METRICS_PLOT_HANGING_RUN_THRESHOLD_MS = 3600 * 24 * 7 * 1000; // 1 week -const MAXIMUM_METRIC_DATA_POINTS = 100_000; +const MAXIMUM_METRIC_DATA_POINTS = 1_000_000; const GET_METRIC_HISTORY_MAX_RESULTS = 25000; export const convertMetricsToCsv = (metrics: any) => { diff --git a/mlflow/server/js/src/experiment-tracking/components/ProjectListView.tsx b/mlflow/server/js/src/experiment-tracking/components/ProjectListView.tsx new file mode 100644 index 0000000000..5830b5e6d8 --- /dev/null +++ b/mlflow/server/js/src/experiment-tracking/components/ProjectListView.tsx @@ -0,0 +1,96 @@ +import 'react-virtualized/styles.css'; + +import { + DialogCombobox, + DialogComboboxContent, + DialogComboboxOptionList, + DialogComboboxOptionListSelectItem, + DialogComboboxTrigger, + Typography, +} from '@databricks/design-system'; +import React, { Component } from 'react'; + +import { ExperimentEntity } from '../types'; +import { css } from '@emotion/react'; + +type Props = { + experiments: ExperimentEntity[]; + project: string; + handleProjectChange: any; +}; + +type State = any; + +export const filterExperimentsByProject = (experiments:any, selectedProject:any) =>{ + if (selectedProject === "All") { + return experiments; + } else if (selectedProject === "Default") { + return experiments.filter( + (experiment:any) => !experiment.tags || !experiment.tags.some((tag:any) => tag.key.toLowerCase() === "project") + ); + } else { + return experiments.filter( + (experiment:any) => experiment.tags && experiment.tags.some((tag:any) => tag.key.toLowerCase() === "project" && tag.value === selectedProject) + ); + } +} +export class ProjectListView extends Component { + + listProjects = () => { + const { experiments } = this.props; + const projects = experiments + .filter(experiment => { + const projectTag = experiment.tags && experiment.tags.find((tag :any) => tag.key.toLowerCase() === "project"); + return projectTag !== undefined; + }) + .map(experiment => { + const projectTag = experiment.tags.find((tag:any) => tag.key.toLowerCase() === "project"); + return projectTag ? projectTag.value : null; + }); + return ['All','Default', ...new Set(projects)]; + }; + + render() { + const projects = this.listProjects(); + return ( +
+ + Projects + + + + + + {projects.map((project:any) => ( + this.props.handleProjectChange(project)} + > + {project} + + ))} + + + +
+ ); + } +} + +const classNames = { + projectsContainer: { + marginBottom: '8px', + width:'100%' + }, +}; + +export default ProjectListView; diff --git a/mlflow/server/js/src/experiment-tracking/components/artifact-view-components/ShowArtifactPage.tsx b/mlflow/server/js/src/experiment-tracking/components/artifact-view-components/ShowArtifactPage.tsx index 8451e6ea6b..97006125d3 100644 --- a/mlflow/server/js/src/experiment-tracking/components/artifact-view-components/ShowArtifactPage.tsx +++ b/mlflow/server/js/src/experiment-tracking/components/artifact-view-components/ShowArtifactPage.tsx @@ -14,6 +14,7 @@ import { HTML_EXTENSIONS, PDF_EXTENSIONS, DATA_EXTENSIONS, + TRACE_EXTENSIONS, } from '../../../common/utils/FileUtils'; import { getLoggedModelPathsFromTags, getLoggedTablesFromTags } from '../../../common/utils/TagUtils'; import { ONE_MB } from '../../constants'; @@ -21,6 +22,7 @@ import ShowArtifactImageView from './ShowArtifactImageView'; import ShowArtifactTextView from './ShowArtifactTextView'; import { LazyShowArtifactMapView } from './LazyShowArtifactMapView'; import ShowArtifactHtmlView from './ShowArtifactHtmlView'; +import ShowArtifactTraceView from './ShowArtifactTraceView'; import { LazyShowArtifactPdfView } from './LazyShowArtifactPdfView'; import { LazyShowArtifactTableView } from './LazyShowArtifactTableView'; import ShowArtifactLoggedModelView from './ShowArtifactLoggedModelView'; @@ -77,7 +79,9 @@ class ShowArtifactPage extends Component { } else if (this.props.showArtifactLoggedTableView) { return ; } else if (normalizedExtension) { - if (IMAGE_EXTENSIONS.has(normalizedExtension.toLowerCase())) { + if (TRACE_EXTENSIONS.has(normalizedExtension.toLowerCase())) { + return ; + } else if (IMAGE_EXTENSIONS.has(normalizedExtension.toLowerCase())) { return ; } else if (DATA_EXTENSIONS.has(normalizedExtension.toLowerCase())) { return ; diff --git a/mlflow/server/js/src/experiment-tracking/components/artifact-view-components/ShowArtifactTraceView.css b/mlflow/server/js/src/experiment-tracking/components/artifact-view-components/ShowArtifactTraceView.css new file mode 100644 index 0000000000..de635ffa42 --- /dev/null +++ b/mlflow/server/js/src/experiment-tracking/components/artifact-view-components/ShowArtifactTraceView.css @@ -0,0 +1,9 @@ +.trace-iframe { + border: none; +} + +.artifact-trace-view { + width: 100%; + height: 100%; + overflow: auto; +} diff --git a/mlflow/server/js/src/experiment-tracking/components/artifact-view-components/ShowArtifactTraceView.tsx b/mlflow/server/js/src/experiment-tracking/components/artifact-view-components/ShowArtifactTraceView.tsx new file mode 100644 index 0000000000..9fa1956d5b --- /dev/null +++ b/mlflow/server/js/src/experiment-tracking/components/artifact-view-components/ShowArtifactTraceView.tsx @@ -0,0 +1,129 @@ +/** + * NOTE: this code file was automatically migrated to TypeScript using ts-migrate and + * may contain multiple `any` type annotations and `@ts-expect-error` directives. + * If possible, please improve types while making changes to this file. If the type + * annotations are already looking good, please remove this comment. + */ + +import pako from 'pako'; +import React, { Component } from 'react'; +import { getArtifactContent, getArtifactLocationUrl } from '../../../common/utils/ArtifactUtils'; +import './ShowArtifactTraceView.css'; +import { ArtifactViewSkeleton } from './ArtifactViewSkeleton'; + +type ShowArtifactTraceViewState = { + loading: boolean; + error?: any; + path: string; + tracedata: string; +}; + +type ShowArtifactTraceViewProps = { + runUuid: string; + path: string; + getArtifact: (artifactLocation: string, isBinary: boolean) => Promise; +}; + +class ShowArtifactTraceView extends Component { + iframeRef: any; + constructor(props: ShowArtifactTraceViewProps) { + super(props); + this.fetchArtifacts = this.fetchArtifacts.bind(this); + this.traceViewDataHandler = this.traceViewDataHandler.bind(this) + this.iframeRef = React.createRef(); + } + + static defaultProps = { + getArtifact: getArtifactContent, + }; + + state = { + loading: true, + error: undefined, + path: '', + tracedata: '', + }; + + componentDidMount() { + this.fetchArtifacts(); + window.addEventListener('message', this.traceViewDataHandler, true); + } + + componentDidUpdate(prevProps: ShowArtifactTraceViewProps) { + if (this.props.path !== prevProps.path || this.props.runUuid !== prevProps.runUuid) { + this.fetchArtifacts(); + window.addEventListener('message', this.traceViewDataHandler, true); + } + } + + componentWillUnmount() { + // Avoid registering `traceViewDataHandler` every time this component mounts + window.removeEventListener('message', this.traceViewDataHandler, true); + } + + render() { + if (this.state.loading || this.state.path !== this.props.path) { + return ; + } + if (this.state.error) { + console.error('Unable to load Trace artifact, got error ' + this.state.error); + return
Oops we couldn't load your file because of an error.
; + } else { + return ( +
+ + +
+ ); + } + } + + /** Fetches artifacts and updates component state with the result */ + fetchArtifacts() { + const artifactLocation = getArtifactLocationUrl(this.props.path, this.props.runUuid); + this.props + .getArtifact(artifactLocation, true) + .then((tracebindata: ArrayBufferLike) => { + const uint8Array = new Uint8Array(tracebindata); + let data = ''; + // Gzip files start with the magic number 0x1f 0x8b + if (uint8Array[0] === 0x1f && uint8Array[1] === 0x8b) { + try { + data = pako.ungzip(uint8Array, { to: 'string' }); + } catch (error) { + console.error('Decompression error:', error); + } + } else { + data = new TextDecoder().decode(uint8Array); + } + this.setState({ tracedata: data, loading: false, path: this.props.path }); + }) + .catch((error: Error) => { + this.setState({ error: error, loading: false, path: this.props.path }); + }); + } + + traceViewDataHandler(event: MessageEvent) { + const data = event.data || {} + if (data.msg === 'ready') { + if (this.iframeRef.current && this.iframeRef.current.contentWindow) { + this.iframeRef.current.focus(); + this.iframeRef.current.contentWindow.postMessage( + { msg: 'data', data: this.state.tracedata }, + '*' + ); + } + } + } +} + +export default ShowArtifactTraceView; diff --git a/mlflow/server/js/src/experiment-tracking/components/run-page/RunViewMetricChart.tsx b/mlflow/server/js/src/experiment-tracking/components/run-page/RunViewMetricChart.tsx index c6f4b428a1..9f68a96e5f 100644 --- a/mlflow/server/js/src/experiment-tracking/components/run-page/RunViewMetricChart.tsx +++ b/mlflow/server/js/src/experiment-tracking/components/run-page/RunViewMetricChart.tsx @@ -57,6 +57,8 @@ export interface RunViewMetricChartProps { * Reference to a overarching refresh manager (entity that will trigger refresh of subscribed charts) */ chartRefreshManager: ChartRefreshManager; + maxResults: number; + showPoint: boolean; } /** @@ -72,6 +74,8 @@ export const RunViewMetricChart = ({ onMoveUp, onMoveDown, chartRefreshManager, + maxResults, + showPoint }: RunViewMetricChartProps) => { const { dragHandleRef, dragPreviewRef, dropTargetRef, isDragging, isOver } = useDragAndDropElement({ dragGroupKey, @@ -97,7 +101,7 @@ export const RunViewMetricChart = ({ metricKeys, enabled: isInViewport, range: stepRange, - maxResults: 320, + maxResults: maxResults, }); const { metricsHistory, error } = resultsByRunUuid[runInfo.runUuid ?? '']?.[metricKey] || {}; @@ -128,7 +132,7 @@ export const RunViewMetricChart = ({ }); } return () => {}; - }, [chartRefreshManager, refresh, isInViewport]); + }, [chartRefreshManager, refresh, isInViewport, maxResults]); const yRange = useRef<[number, number] | undefined>(undefined); @@ -197,6 +201,7 @@ export const RunViewMetricChart = ({ // cases the plotly will use last known range. xRange={xRange} yRange={yRange.current} + showPoint={showPoint} /> ); }; diff --git a/mlflow/server/js/src/experiment-tracking/components/run-page/RunViewMetricCharts.tsx b/mlflow/server/js/src/experiment-tracking/components/run-page/RunViewMetricCharts.tsx index 54706b7598..b9f14b97a6 100644 --- a/mlflow/server/js/src/experiment-tracking/components/run-page/RunViewMetricCharts.tsx +++ b/mlflow/server/js/src/experiment-tracking/components/run-page/RunViewMetricCharts.tsx @@ -7,9 +7,18 @@ import { Spacer, Spinner, useDesignSystemTheme, + DialogCombobox, + DialogComboboxContent, + DialogComboboxOptionList, + DialogComboboxOptionListCheckboxItem, + DialogComboboxOptionListSelectItem, + DialogComboboxOptionListSearch, + DialogComboboxTrigger, + Switch, } from '@databricks/design-system'; +import Fuse from 'fuse.js'; import { compact, mapValues, values } from 'lodash'; -import { useMemo, useState } from 'react'; +import { useMemo, useState, useEffect } from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; import { useSelector } from 'react-redux'; import { getGridColumnSetup } from '../../../common/utils/CssGrid.utils'; @@ -45,29 +54,33 @@ const EmptyMetricsFiltered = () => ( const EmptyMetricsNotRecorded = ({ label }: { label: React.ReactNode }) => ; const metricKeyMatchesFilter = (filter: string, metricKey: string) => - metricKey.toLowerCase().startsWith(filter.toLowerCase()) || - normalizeChartMetricKey(metricKey).toLowerCase().startsWith(filter.toLowerCase()); + metricKey.toLowerCase().includes(filter.toLowerCase()) || + normalizeChartMetricKey(metricKey).toLowerCase().includes(filter.toLowerCase()); /** * Internal component that displays a single collapsible section with charts */ const RunViewMetricChartsSection = ({ metricKeys, + filteredMetricKeys, search, runInfo, chartRefreshManager, onReorderChart, + maxResults, + showPoint, }: { metricKeys: string[]; + filteredMetricKeys: string[]; search: string; runInfo: RunInfoEntity | UseGetRunQueryResponseRunInfo; onReorderChart: (sourceChartKey: string, targetChartKey: string) => void; chartRefreshManager: ChartRefreshManager; + maxResults: number; + showPoint: boolean; }) => { const { theme } = useDesignSystemTheme(); - const filteredMetricKeys = metricKeys.filter((metricKey) => metricKeyMatchesFilter(search, metricKey)); - const { canMoveDown, canMoveUp, moveChartDown, moveChartUp } = useChartMoveUpDownFunctions( filteredMetricKeys, onReorderChart, @@ -76,13 +89,13 @@ const RunViewMetricChartsSection = ({ const gridSetup = useMemo( () => ({ ...getGridColumnSetup({ - maxColumns: 3, + maxColumns: maxResults > 320 ? 1 : 3, gap: theme.spacing.lg, additionalBreakpoints: [{ breakpointWidth: 3 * 720, minColumnWidthForBreakpoint: 720 }], }), overflow: 'hidden', }), - [theme], + [theme, maxResults], ); return filteredMetricKeys.length ? ( @@ -101,6 +114,8 @@ const RunViewMetricChartsSection = ({ onMoveDown={() => moveChartDown(metricKey)} onMoveUp={() => moveChartUp(metricKey)} chartRefreshManager={chartRefreshManager} + maxResults={maxResults} + showPoint={showPoint} /> ))} @@ -137,13 +152,35 @@ export const RunViewMetricCharts = ({ }); const [search, setSearch] = useState(''); + const prevSample = localStorage.getItem('mlflow-run-chart-default-samples') || "320" + const [maxSteps, setMaxSteps] = useState(parseInt(prevSample)); + const [showPoint, setShowPoint] = useState(false); const { formatMessage } = useIntl(); - + const maxSamples = [320, 500, 1000, 2500] const { orderedMetricKeys, onReorderChart } = useOrderedCharts(metricKeys, 'RunView' + mode, runInfo.runUuid ?? ''); + // Setting up Fuse for Fuzzy Searching + const fuseOptions = { + includeScore: true, + minMatchCharLength: 1, // Allows matching on single characters + threshold: 0.6, // Adjust for stricter or more lenient matching + shouldSort: true, // Prioritizes results by relevance + matchAllTokens: true, // Ensures each keyword is matched somewhere in the string + findAllMatches: true, // Matches even partial matches anywhere in the string + useExtendedSearch: true // Allows partial matches within substrings + }; + + const fuse = new Fuse(metricKeys, fuseOptions); + + // Prepare the extended search pattern + const searchTerms = search.split(" ").filter(Boolean); // Split by spaces and remove empty items + const extendedSearchPattern = searchTerms.map(term => `"'${term}"`).join(" "); + + const filteredMetricKeys = search && search !== '' ? fuse.search(extendedSearchPattern).map((item) => item.item) : metricKeys; + const noMetricsRecorded = !metricKeys.length; - const allMetricsFilteredOut = - !noMetricsRecorded && !metricKeys.some((metricKey) => metricKeyMatchesFilter(search, metricKey)); + const allMetricsFilteredOut = !filteredMetricKeys.length; + const showConfigArea = !noMetricsRecorded; const { theme } = useDesignSystemTheme(); const showCharts = !noMetricsRecorded && !allMetricsFilteredOut; @@ -156,6 +193,11 @@ export const RunViewMetricCharts = ({ ); }); + // on samples to render change, refresh all charts + useEffect(() => { + chartRefreshManager.refreshAllCharts(); + }, [maxSteps]); + return (
@@ -173,6 +215,49 @@ export const RunViewMetricCharts = ({ description: 'Run page > Charts tab > Filter metric charts input > placeholder', })} /> + + + + + {maxSamples.map((sample) => { + return ( + { + setMaxSteps(sample); + localStorage.setItem('mlflow-run-chart-default-samples', sample.toString()); + }} + > + {sample} + + ); + })} + + + +
+
+ +
+ setShowPoint(!showPoint)} + /> +