Skip to content

Commit 9b8caa7

Browse files
committed
LookML parsing WIP
1 parent 2a14f9e commit 9b8caa7

File tree

3 files changed

+115
-27
lines changed

3 files changed

+115
-27
lines changed

tools/rule-sandbox/components/app.jsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,16 @@ import TabPanel from '@mui/lab/TabPanel'
4040

4141
import AboutPage from './about-page.jsx'
4242
import ProjectPage from './project-page.jsx'
43+
import ProjectParsedPage from './project-parsed-page.jsx'
4344
import RulePage from './rule-page.jsx'
4445
import FnDocsPage from './fn-docs-page.jsx'
4546

4647
const App = () => {
47-
const [tab, setTab] = useState('project')
48-
const [project, setProject] = useState(undefined)
49-
const [match, setMatch] = useState(undefined)
50-
const [rule, setRule] = useState(undefined)
48+
const [tab, setTab] = useState('project')
49+
const [projectFiles,setProjectFiles] = useState([])
50+
const [project, setProject] = useState(undefined)
51+
const [match, setMatch] = useState(undefined)
52+
const [rule, setRule] = useState(undefined)
5153

5254
return (
5355
<div className="app">
@@ -59,6 +61,7 @@ const App = () => {
5961
<TabList onChange={changeTab} aria-label="Rule Sandbox tabs">
6062
<Tab label="About" value="about" />
6163
<Tab label="Project" value="project" />
64+
<Tab label="Parsed" value="project-parsed" />
6265
<Tab label="Rule" value="rule" />
6366
<Tab label="Fn Docs" value="fn-docs" />
6467
</TabList>
@@ -73,11 +76,17 @@ const App = () => {
7376
</TabPanel>
7477
<TabPanel value="project">
7578
<ProjectPage {...{
76-
project,
79+
projectFiles,
80+
setProjectFiles,
7781
setTab,
7882
setProject
7983
}}/>
8084
</TabPanel>
85+
<TabPanel value="project-parsed">
86+
<ProjectParsedPage {...{
87+
project
88+
}}/>
89+
</TabPanel>
8190
<TabPanel value="rule">
8291
<RulePage {...{
8392
project,

tools/rule-sandbox/components/project-page.jsx

Lines changed: 59 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626

2727
import React, {use, useEffect, useState} from 'react'
2828
import {useDebounce} from 'use-debounce'
29-
import lookmlParser from 'lookml-parser'
29+
import lookmlParser_parseFilesArrray from 'lookml-parser/lib/parse-files-array'
3030

3131
import Button from '@mui/material/Button'
3232
import Stack from '@mui/material/Stack'
@@ -44,28 +44,29 @@ import Typography from '@mui/material/Typography'
4444

4545
const ProjectPage = (props) => {
4646
const {
47-
initProjectFiles,
47+
projectFiles,
48+
setProjectFiles,
4849
setTab,
4950
setProject
5051
} = props
5152

5253
// Core state
53-
const [projectFiles, setProjectFiles] = useState(initProjectFiles ?? [])
5454
const [selectedFileContent, setSelectedFileContent] = useState("")
5555
const [selectedFileIndex, setSelectedFileIndex] = useState(projectFiles.length ? 0 : undefined)
5656
const [renaming, setRenaming] = useState({index: null, isNew: false, path: ''})
5757

5858
// Derived state
59-
const [debouncedSelectedFileContent] = useDebounce(selectedFileContent,1000)
59+
const [debouncedSelectedFileContent] = useDebounce(selectedFileContent,2000)
6060
const [projectStatus, setProjectStatus] = useState("")
61+
const [parseErrors, setParseErrors] = useState({})
6162
const [ctaDisabled, setCtaDisabled] = useState(true)
6263

6364
// Effects
64-
useEffect(updateSelectedFileContent, [selectedFileIndex, projectFiles])
65+
useEffect(updateSelectedFileContent, [selectedFileIndex])
6566
useEffect(updateProjectFile, [debouncedSelectedFileContent])
6667
useEffect(() => {
6768
parseProject()
68-
}, [projectFiles])
69+
}, [projectFiles]);
6970

7071
return (
7172
<Stack direction="column" spacing={2} className="project-page">
@@ -74,8 +75,8 @@ const ProjectPage = (props) => {
7475
<Stack direction="row" justifyContent="flex-end" alignItems="center" spacing={2}>
7576
<Typography>{projectStatus}</Typography>
7677
<Button
77-
variant="contained"
78-
onClick={()=>setTab("rule")}
78+
variant="contained"
79+
onClick={handleInspectRulesClick}
7980
value="rule"
8081
disabled={ctaDisabled}>
8182
Inspect Rule(s)
@@ -92,6 +93,7 @@ const ProjectPage = (props) => {
9293
<ListItem
9394
key={f}
9495
disablePadding
96+
sx={{backgroundColor: parseErrors[file.path] ? 'rgba(255, 0, 0, 0.1)' : 'transparent'}}
9597
secondaryAction={renaming.index !== f && (
9698
<Stack direction="row">
9799
<IconButton edge="end" aria-label="rename" onClick={() => handleRenameFile(f)}>
@@ -149,11 +151,23 @@ const ProjectPage = (props) => {
149151
onChange={handleFileContentsChange}
150152
disabled={selectedFileIndex === undefined}
151153
></TextField>
154+
{parseErrors[projectFiles[selectedFileIndex]?.path] && (
155+
<Typography color="error" variant="caption" sx={{mt: 1}}>
156+
{trunc(parseErrors[projectFiles[selectedFileIndex]?.path], 120)}
157+
</Typography>
158+
)}
152159
</Stack>
153160
</Stack>
154161
)
155162

163+
async function handleInspectRulesClick() {
164+
updateProjectFile(selectedFileContent)
165+
const success = await parseProject(projectFiles)
166+
if (success) setTab("rule")
167+
}
168+
156169
function handleFileSelect(index) {
170+
updateProjectFile(selectedFileContent)
157171
setSelectedFileIndex(index)
158172
}
159173

@@ -182,7 +196,7 @@ const ProjectPage = (props) => {
182196
setProjectFiles(newFiles);
183197
if (isNew) {
184198
setSelectedFileIndex(index);
185-
}
199+
}
186200
setRenaming({index: null, isNew: false, path: ''});
187201
}
188202

@@ -197,6 +211,7 @@ const ProjectPage = (props) => {
197211
// Add a temporary placeholder file and enter renaming mode for it
198212
const newIndex = projectFiles.length;
199213
setProjectFiles([...projectFiles, {path: '', contents: ''}]);
214+
updateProjectFile(selectedFileContent)
200215
setRenaming({index: newIndex, isNew: true, path: 'new_file.view.lkml'});
201216
}
202217

@@ -230,35 +245,57 @@ const ProjectPage = (props) => {
230245
}
231246

232247
function updateProjectFile() {
248+
if (selectedFileIndex === undefined) return;
249+
updateProjectFile(debouncedSelectedFileContent);
250+
}
251+
252+
function updateProjectFile(content) {
233253
if (selectedFileIndex === undefined) return;
234254
const currentFile = projectFiles[selectedFileIndex];
235-
if (!currentFile || currentFile.contents === debouncedSelectedFileContent) return;
255+
if (!currentFile || currentFile.contents === content) return;
236256

237257
const newFiles = [...projectFiles];
238-
newFiles[selectedFileIndex] = { ...currentFile, contents: debouncedSelectedFileContent };
239-
setProjectFiles(newFiles);
258+
newFiles[selectedFileIndex] = { ...currentFile, contents: content };
259+
setProjectFiles(newFiles)
240260
}
241261

242-
async function parseProject() {
243-
if (!projectFiles || projectFiles.length === 0) {
262+
async function parseProject(files = projectFiles) {
263+
if (!files || files.length === 0) {
244264
setProjectStatus("No files in project")
245265
setProject(undefined)
246266
setCtaDisabled(true)
247-
return
267+
return false;
248268
}
269+
249270
try {
250271
setProjectStatus("Parsing LookML...")
251-
const parsedProject = await lookmlParser.parseFiles({source: projectFiles})
252-
// CONTINUE HERE ^ Need to go update LookML parser to work better in browser environments
253-
// - Make sure that references to glob are conditional/isolated.
254-
// - Don't rely on fs/path to load PEG code, use import/require insteat
255-
setProject(parsedProject)
256-
setProjectStatus("✅ Project ready")
257-
setCtaDisabled(false)
272+
const parsedProject = await lookmlParser_parseFilesArrray(
273+
files.map(f=>({path:f.path, read: ()=>f.contents}))
274+
)
275+
if (parsedProject.errors && parsedProject.errors.length > 0) {
276+
const firstError = parsedProject.errors[0];
277+
setProjectStatus(`❌ Invalid LookML: ${trunc(firstError.error, 120)}`);
278+
setProject(parsedProject);
279+
setCtaDisabled(true);
280+
const errorMap = {};
281+
parsedProject.errors.forEach(err => {
282+
errorMap[err['$file_path']] = err.error;
283+
});
284+
setParseErrors(errorMap);
285+
return false;
286+
} else {
287+
setParseErrors({});
288+
setProject(parsedProject)
289+
setProjectStatus("✅ Project ready")
290+
setCtaDisabled(false)
291+
return true;
292+
}
258293
} catch (e) {
294+
setParseErrors({});
259295
setProjectStatus(`❌ Invalid LookML. ${trunc(e,120)}`)
260296
setProject(undefined)
261297
setCtaDisabled(true)
298+
return false;
262299
}
263300
}
264301
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
3+
MIT License
4+
5+
Copyright (c) 2025 Google LLC
6+
7+
Permission is hereby granted, free of charge, to any person obtaining a copy
8+
of this software and associated documentation files (the "Software"), to deal
9+
in the Software without restriction, including without limitation the rights
10+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
copies of the Software, and to permit persons to whom the Software is
12+
furnished to do so, subject to the following conditions:
13+
14+
The above copyright notice and this permission notice shall be included in all
15+
copies or substantial portions of the Software.
16+
17+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23+
SOFTWARE.
24+
25+
*/
26+
27+
import React from 'react';
28+
import Box from '@mui/material/Box';
29+
import Typography from '@mui/material/Typography';
30+
31+
const ProjectParsedPage = ({ project }) => {
32+
return (
33+
<Box>
34+
<Typography variant="h6">Parsed Project JSON</Typography>
35+
<pre style={{ border: '1px solid #ddd', padding: '10px', maxHeight: '60vh', overflowY: 'auto' }}>
36+
<code>{JSON.stringify(project, null, 2)}</code>
37+
</pre>
38+
</Box>
39+
);
40+
};
41+
42+
export default ProjectParsedPage;

0 commit comments

Comments
 (0)