marcenacp commited on
Commit
6a31b9a
1 Parent(s): 695cb46
app.py CHANGED
@@ -20,6 +20,7 @@ col1.header("Croissant Editor")
20
  init_state()
21
 
22
  user = get_cached_user()
 
23
 
24
  if OAUTH_CLIENT_ID and not user:
25
  query_params = st.experimental_get_query_params()
@@ -56,6 +57,8 @@ def _back_to_menu():
56
  def _logout():
57
  """Logs the user out."""
58
  st.cache_data.clear()
 
 
59
  _back_to_menu()
60
 
61
 
@@ -70,7 +73,9 @@ if timestamp:
70
  col3.button("Menu", on_click=_back_to_menu)
71
 
72
 
73
- if st.session_state.get(CurrentProject):
 
 
74
  render_editor()
75
  else:
76
  render_splash()
 
20
  init_state()
21
 
22
  user = get_cached_user()
23
+ print("USER", user)
24
 
25
  if OAUTH_CLIENT_ID and not user:
26
  query_params = st.experimental_get_query_params()
 
57
  def _logout():
58
  """Logs the user out."""
59
  st.cache_data.clear()
60
+ get_cached_user.clear()
61
+ st.session_state[User] = None
62
  _back_to_menu()
63
 
64
 
 
73
  col3.button("Menu", on_click=_back_to_menu)
74
 
75
 
76
+ should_display_editor = bool(st.session_state.get(CurrentProject))
77
+
78
+ if should_display_editor:
79
  render_editor()
80
  else:
81
  render_splash()
components/tabs/__init__.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ import streamlit.components.v1 as components
4
+
5
+ from core.constants import OVERVIEW
6
+
7
+ # Create a _RELEASE constant. We'll set this to False while we're developing
8
+ # the component, and True when we're ready to package and distribute it.
9
+ _RELEASE = True
10
+
11
+ if not _RELEASE:
12
+ _component_func = components.declare_component(
13
+ "tabs_component",
14
+ url="http://localhost:3001",
15
+ )
16
+ else:
17
+ parent_dir = os.path.dirname(os.path.abspath(__file__))
18
+ build_dir = os.path.join(parent_dir, "frontend/build")
19
+ _component_func = components.declare_component("tabs_component", path=build_dir)
20
+
21
+
22
+ def render_tabs(tabs: list[str], selected_tab: int, json: str | None, key=None):
23
+ """Create a new instance of "tabs_component".
24
+
25
+ Args:
26
+ tabs: The tabs to render in the component.
27
+ selected_tab: The selected tab.
28
+ key: An optional key that uniquely identifies this component. If this is
29
+ None, and the component's arguments are changed, the component will
30
+ be re-mounted in the Streamlit frontend and lose its current state.
31
+
32
+ Returns:
33
+ The number of times the component's "Click Me" button has been clicked.
34
+ (This is the value passed to `Streamlit.setComponentValue` on the
35
+ frontend.)
36
+ """
37
+ component_value = _component_func(
38
+ tabs=tabs,
39
+ selected_tab=selected_tab,
40
+ json=json,
41
+ key=key,
42
+ default=OVERVIEW,
43
+ )
44
+ return component_value
components/tabs/frontend/.env ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ # Run the component's dev server on :3001
2
+ # (The Streamlit dev server already runs on :3000)
3
+ PORT=3001
4
+
5
+ # Don't automatically open the web browser on `npm run start`.
6
+ BROWSER=none
components/tabs/frontend/.prettierrc ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "endOfLine": "lf",
3
+ "semi": false,
4
+ "trailingComma": "es5"
5
+ }
components/tabs/frontend/build/asset-manifest.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "files": {
3
+ "main.js": "./static/js/main.716a0ab4.js",
4
+ "index.html": "./index.html",
5
+ "main.716a0ab4.js.map": "./static/js/main.716a0ab4.js.map"
6
+ },
7
+ "entrypoints": [
8
+ "static/js/main.716a0ab4.js"
9
+ ]
10
+ }
components/tabs/frontend/build/index.html ADDED
@@ -0,0 +1 @@
 
 
1
+ <!doctype html><html lang="en"><head><title>Streamlit Tabs Component</title><meta charset="UTF-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Streamlit Tree Component"/><script defer="defer" src="./static/js/main.716a0ab4.js"></script></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
components/tabs/frontend/build/static/js/main.716a0ab4.js ADDED
The diff for this file is too large to render. See raw diff
 
components/tabs/frontend/build/static/js/main.716a0ab4.js.LICENSE.txt ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ object-assign
3
+ (c) Sindre Sorhus
4
+ @license MIT
5
+ */
6
+
7
+ /**
8
+ * @license React
9
+ * react-dom.production.min.js
10
+ *
11
+ * Copyright (c) Facebook, Inc. and its affiliates.
12
+ *
13
+ * This source code is licensed under the MIT license found in the
14
+ * LICENSE file in the root directory of this source tree.
15
+ */
16
+
17
+ /**
18
+ * @license React
19
+ * react-is.production.min.js
20
+ *
21
+ * Copyright (c) Facebook, Inc. and its affiliates.
22
+ *
23
+ * This source code is licensed under the MIT license found in the
24
+ * LICENSE file in the root directory of this source tree.
25
+ */
26
+
27
+ /**
28
+ * @license React
29
+ * react-jsx-runtime.production.min.js
30
+ *
31
+ * Copyright (c) Facebook, Inc. and its affiliates.
32
+ *
33
+ * This source code is licensed under the MIT license found in the
34
+ * LICENSE file in the root directory of this source tree.
35
+ */
36
+
37
+ /**
38
+ * @license React
39
+ * react.production.min.js
40
+ *
41
+ * Copyright (c) Facebook, Inc. and its affiliates.
42
+ *
43
+ * This source code is licensed under the MIT license found in the
44
+ * LICENSE file in the root directory of this source tree.
45
+ */
46
+
47
+ /**
48
+ * @license React
49
+ * scheduler.production.min.js
50
+ *
51
+ * Copyright (c) Facebook, Inc. and its affiliates.
52
+ *
53
+ * This source code is licensed under the MIT license found in the
54
+ * LICENSE file in the root directory of this source tree.
55
+ */
56
+
57
+ /** @license React v16.13.1
58
+ * react-is.production.min.js
59
+ *
60
+ * Copyright (c) Facebook, Inc. and its affiliates.
61
+ *
62
+ * This source code is licensed under the MIT license found in the
63
+ * LICENSE file in the root directory of this source tree.
64
+ */
65
+
66
+ /** @license React v16.14.0
67
+ * react.production.min.js
68
+ *
69
+ * Copyright (c) Facebook, Inc. and its affiliates.
70
+ *
71
+ * This source code is licensed under the MIT license found in the
72
+ * LICENSE file in the root directory of this source tree.
73
+ */
components/tabs/frontend/build/static/js/main.716a0ab4.js.map ADDED
The diff for this file is too large to render. See raw diff
 
components/tabs/frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
components/tabs/frontend/package.json ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "tree_component",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "dependencies": {
6
+ "@emotion/react": "^11.11.1",
7
+ "@emotion/styled": "^11.11.0",
8
+ "@mui/material": "^5.14.17",
9
+ "react": "^18.2.0",
10
+ "react-dom": "^18.2.0",
11
+ "streamlit-component-lib": "^2.0.0"
12
+ },
13
+ "scripts": {
14
+ "start": "react-scripts start",
15
+ "build": "react-scripts build",
16
+ "test": "react-scripts test",
17
+ "eject": "react-scripts eject"
18
+ },
19
+ "eslintConfig": {
20
+ "extends": "react-app"
21
+ },
22
+ "browserslist": {
23
+ "production": [
24
+ ">0.2%",
25
+ "not dead",
26
+ "not op_mini all"
27
+ ],
28
+ "development": [
29
+ "last 1 chrome version",
30
+ "last 1 firefox version",
31
+ "last 1 safari version"
32
+ ]
33
+ },
34
+ "homepage": ".",
35
+ "devDependencies": {
36
+ "@types/node": "^20.9.0",
37
+ "@types/react": "^18.2.37",
38
+ "@types/react-dom": "^18.2.15",
39
+ "react-scripts": "^5.0.1",
40
+ "typescript": "^5.2.2"
41
+ },
42
+ "overrides": {
43
+ "react-scripts": {
44
+ "typescript": "^5"
45
+ }
46
+ }
47
+ }
components/tabs/frontend/public/index.html ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <title>Streamlit Tabs Component</title>
6
+ <meta charset="UTF-8" />
7
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
8
+ <meta name="theme-color" content="#000000" />
9
+ <meta name="description" content="Streamlit Tree Component" />
10
+ </head>
11
+
12
+ <body>
13
+ <noscript>You need to enable JavaScript to run this app.</noscript>
14
+ <div id="root"></div>
15
+ <!--
16
+ This HTML file is a template.
17
+ If you open it directly in the browser, you will see an empty page.
18
+
19
+ You can add webfonts, meta tags, or analytics to this file.
20
+ The build step will place the bundled scripts into the <body> tag.
21
+
22
+ To begin the development, run `npm start` or `yarn start`.
23
+ To create a production bundle, use `npm run build` or `yarn build`.
24
+ -->
25
+ </body>
26
+
27
+ </html>
components/tabs/frontend/src/Tabs.tsx ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ Streamlit,
3
+ StreamlitComponentBase,
4
+ withStreamlitConnection,
5
+ } from "streamlit-component-lib"
6
+ import React, { ReactNode } from "react"
7
+ import Button from "@mui/material/Button"
8
+ import Tabs from "@mui/material/Tabs"
9
+ import Tab from "@mui/material/Tab"
10
+ import Box from "@mui/material/Box"
11
+ import { ThemeProvider, createTheme } from "@mui/material"
12
+ import { orange } from "@mui/material/colors"
13
+
14
+ const theme = createTheme({
15
+ palette: {
16
+ primary: orange,
17
+ },
18
+ })
19
+
20
+ function BasicTabs({
21
+ tabs,
22
+ selectedTab,
23
+ json,
24
+ }: {
25
+ tabs: string[]
26
+ selectedTab: number
27
+ json?: { name: string; content: string }
28
+ }) {
29
+ const [value, setValue] = React.useState(selectedTab)
30
+ const handleChange = (event: React.SyntheticEvent, newValue: number) => {
31
+ Streamlit.setComponentValue(tabs[newValue])
32
+ setValue(newValue)
33
+ }
34
+
35
+ return (
36
+ <div
37
+ style={{
38
+ display: "flex",
39
+ flexDirection: "row",
40
+ justifyContent: "center",
41
+ alignItems: "center",
42
+ marginTop: -8,
43
+ }}
44
+ >
45
+ <Box sx={{ width: "100%", margin: -1, padding: 0 }}>
46
+ <Box sx={{ borderBottom: 1, borderColor: "divider" }}>
47
+ <Tabs
48
+ value={value}
49
+ onChange={handleChange}
50
+ aria-label="navigation-tabs"
51
+ >
52
+ {tabs.map((tab) => (
53
+ <Tab key={`custom-tab-${tab}`} label={tab} />
54
+ ))}
55
+ </Tabs>
56
+ </Box>
57
+ </Box>
58
+ <Button
59
+ disabled={!json}
60
+ variant="outlined"
61
+ href={
62
+ json
63
+ ? `data:text/json;charset=utf-8,${encodeURIComponent(json.content)}`
64
+ : ""
65
+ }
66
+ download={json ? json.name : ""}
67
+ >
68
+ Export
69
+ </Button>
70
+ </div>
71
+ )
72
+ }
73
+
74
+ class StreamlitTabs extends StreamlitComponentBase<{}> {
75
+ public render = (): ReactNode => {
76
+ const tabs = this.props.args["tabs"]
77
+ const selectedTab = this.props.args["selected_tab"]
78
+ const json = this.props.args["json"]
79
+ return (
80
+ <ThemeProvider theme={theme}>
81
+ <BasicTabs tabs={tabs} selectedTab={selectedTab} json={json} />
82
+ </ThemeProvider>
83
+ )
84
+ }
85
+ }
86
+
87
+ export default withStreamlitConnection(StreamlitTabs)
components/tabs/frontend/src/index.tsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react"
2
+ import ReactDOM from "react-dom"
3
+ import Tabs from "./Tabs.tsx"
4
+
5
+ ReactDOM.render(
6
+ <React.StrictMode>
7
+ <Tabs />
8
+ </React.StrictMode>,
9
+ document.getElementById("root")
10
+ )
components/tabs/frontend/src/react-app-env.d.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ /// <reference types="react-scripts" />
core/files.py CHANGED
@@ -132,7 +132,13 @@ def file_from_upload(
132
 
133
 
134
  def file_from_form(
135
- file_type: FileType, type: str, name, description, sha256: str, names: set[str]
 
 
 
 
 
 
136
  ) -> FileObject | FileSet:
137
  """Creates a file based on manually added fields."""
138
  if type == FILE_OBJECT:
@@ -143,12 +149,14 @@ def file_from_form(
143
  encoding_format=file_type.encoding_format,
144
  sha256=sha256,
145
  df=None,
 
146
  )
147
  elif type == FILE_SET:
148
  return FileSet(
149
  name=find_unique_name(names, name),
150
  description=description,
151
  encoding_format=file_type.encoding_format,
 
152
  )
153
  else:
154
  raise ValueError("type has to be one of FILE_OBJECT, FILE_SET")
 
132
 
133
 
134
  def file_from_form(
135
+ file_type: FileType,
136
+ type: str,
137
+ name,
138
+ description,
139
+ sha256: str,
140
+ contained_in: list[str],
141
+ names: set[str],
142
  ) -> FileObject | FileSet:
143
  """Creates a file based on manually added fields."""
144
  if type == FILE_OBJECT:
 
149
  encoding_format=file_type.encoding_format,
150
  sha256=sha256,
151
  df=None,
152
+ contained_in=contained_in,
153
  )
154
  elif type == FILE_SET:
155
  return FileSet(
156
  name=find_unique_name(names, name),
157
  description=description,
158
  encoding_format=file_type.encoding_format,
159
+ contained_in=contained_in,
160
  )
161
  else:
162
  raise ValueError("type has to be one of FILE_OBJECT, FILE_SET")
core/past_projects.py CHANGED
@@ -30,11 +30,11 @@ def save_current_project():
30
  st.session_state[CurrentProject] = project
31
  project.path.mkdir(parents=True, exist_ok=True)
32
  set_project(project)
33
- with _pickle_file(project.path).open("wb") as file:
34
- try:
35
- pickle.dump(metadata, file)
36
- except pickle.PicklingError:
37
- logging.error("Could not pickle metadata.")
38
 
39
 
40
  def open_project(path: epath.Path) -> Metadata:
 
30
  st.session_state[CurrentProject] = project
31
  project.path.mkdir(parents=True, exist_ok=True)
32
  set_project(project)
33
+ try:
34
+ pickled = pickle.dumps(metadata)
35
+ _pickle_file(project.path).write_bytes(pickled)
36
+ except pickle.PicklingError as e:
37
+ logging.error("Could not pickle metadata.", exc_info=True)
38
 
39
 
40
  def open_project(path: epath.Path) -> Metadata:
core/query_params.py CHANGED
@@ -4,7 +4,6 @@ from typing import Any
4
 
5
  import streamlit as st
6
 
7
- from core.constants import TABS
8
  from core.state import CurrentProject
9
  from core.state import RecordSet
10
 
@@ -14,7 +13,6 @@ class QueryParams:
14
 
15
  OPEN_PROJECT = "project"
16
  OPEN_RECORD_SET = "recordSet"
17
- OPEN_TAB = "tab"
18
 
19
 
20
  def _get_query_param(params: dict[str, Any], name: str) -> str | None:
@@ -28,46 +26,14 @@ def _get_query_param(params: dict[str, Any], name: str) -> str | None:
28
 
29
  def _set_query_param(param: str, new_value: str) -> str | None:
30
  params = st.experimental_get_query_params()
 
 
 
31
  new_params = {k: v for k, v in params.items() if k != param}
32
  new_params[param] = new_value
33
  st.experimental_set_query_params(**new_params)
34
 
35
 
36
- def go_to_tab(tabs: list[str]):
37
- params = st.experimental_get_query_params()
38
- if QueryParams.OPEN_TAB in params:
39
- try:
40
- index = int(params[QueryParams.OPEN_TAB][0])
41
- if 0 <= index and index < len(TABS):
42
- tab = TABS[index]
43
- # Click on the tab.
44
- js = f"""
45
- <script>
46
- function contains(selector, text) {{
47
- const document = window.parent.document;
48
- const elements = document.querySelectorAll(selector);
49
- return Array.from(elements).filter(function(element) {{
50
- return RegExp(text).test(element.innerText);
51
- }});
52
- }}
53
- const tab = contains('button', '{tab}');
54
- if (tab.length) {{
55
- tab[0].click();
56
- }}
57
- </script>
58
- """
59
- st.components.v1.html(js)
60
- except ValueError:
61
- pass
62
-
63
-
64
- def set_tab(tab: str):
65
- if tab not in TABS:
66
- return
67
- index = TABS.index(tab)
68
- _set_query_param(QueryParams.OPEN_TAB, index)
69
-
70
-
71
  def is_record_set_expanded(record_set: RecordSet) -> bool:
72
  params = st.experimental_get_query_params()
73
  open_record_set_name = _get_query_param(params, QueryParams.OPEN_RECORD_SET)
 
4
 
5
  import streamlit as st
6
 
 
7
  from core.state import CurrentProject
8
  from core.state import RecordSet
9
 
 
13
 
14
  OPEN_PROJECT = "project"
15
  OPEN_RECORD_SET = "recordSet"
 
16
 
17
 
18
  def _get_query_param(params: dict[str, Any], name: str) -> str | None:
 
26
 
27
  def _set_query_param(param: str, new_value: str) -> str | None:
28
  params = st.experimental_get_query_params()
29
+ if params.get(param) == [new_value]:
30
+ # The value already exists in the query params.
31
+ return
32
  new_params = {k: v for k, v in params.items() if k != param}
33
  new_params[param] = new_value
34
  st.experimental_set_query_params(**new_params)
35
 
36
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  def is_record_set_expanded(record_set: RecordSet) -> bool:
38
  params = st.experimental_get_query_params()
39
  open_record_set_name = _get_query_param(params, QueryParams.OPEN_RECORD_SET)
core/state.py CHANGED
@@ -20,6 +20,7 @@ from core.constants import OAUTH_CLIENT_SECRET
20
  from core.constants import PAST_PROJECTS_PATH
21
  from core.constants import PROJECT_FOLDER_PATTERN
22
  from core.constants import REDIRECT_URI
 
23
  from core.names import find_unique_name
24
  import mlcroissant as mlc
25
 
@@ -327,3 +328,22 @@ class Metadata:
327
  def names(self) -> set[str]:
328
  nodes = self.distribution + self.record_sets
329
  return set([node.name for node in nodes])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  from core.constants import PAST_PROJECTS_PATH
21
  from core.constants import PROJECT_FOLDER_PATTERN
22
  from core.constants import REDIRECT_URI
23
+ from core.constants import TABS
24
  from core.names import find_unique_name
25
  import mlcroissant as mlc
26
 
 
328
  def names(self) -> set[str]:
329
  nodes = self.distribution + self.record_sets
330
  return set([node.name for node in nodes])
331
+
332
+
333
+ class OpenTab:
334
+ pass
335
+
336
+
337
+ def get_tab():
338
+ tab = st.session_state.get(OpenTab)
339
+ if tab is None:
340
+ return 0
341
+ else:
342
+ return tab
343
+
344
+
345
+ def set_tab(tab: str):
346
+ if tab not in TABS:
347
+ return
348
+ index = TABS.index(tab)
349
+ st.session_state[OpenTab] = index
cypress/e2e/createManually.cy.js CHANGED
@@ -10,13 +10,15 @@ describe('Create a resource manually', () => {
10
  cy.visit('http://localhost:8501')
11
  cy.get('button').contains('Create').click()
12
  cy.get('input[aria-label="Name:red[*]"]').type('MyDataset').blur()
13
- cy.get('[data-testid="stMarkdownContainer"]')
14
- .contains('Metadata')
15
- .click()
16
  cy.get('input[aria-label="URL:red[*]"]').type('https://mydataset.com', {force: true})
17
 
18
  // Create a resource manually.
19
- cy.get('[data-testid="stMarkdownContainer"]').contains('Resources').click()
 
 
20
  cy.get('[data-testid="stMarkdownContainer"]').contains('Add manually').click()
21
 
22
  cy.get('input[aria-label="File name:red[*]"]').type('test.csv').blur()
 
10
  cy.visit('http://localhost:8501')
11
  cy.get('button').contains('Create').click()
12
  cy.get('input[aria-label="Name:red[*]"]').type('MyDataset').blur()
13
+ cy.enter('[title="components.tabs.tabs_component"]').then(getBody => {
14
+ getBody().contains('Metadata').click()
15
+ })
16
  cy.get('input[aria-label="URL:red[*]"]').type('https://mydataset.com', {force: true})
17
 
18
  // Create a resource manually.
19
+ cy.enter('[title="components.tabs.tabs_component"]').then(getBody => {
20
+ getBody().contains('Resources').click()
21
+ })
22
  cy.get('[data-testid="stMarkdownContainer"]').contains('Add manually').click()
23
 
24
  cy.get('input[aria-label="File name:red[*]"]').type('test.csv').blur()
cypress/e2e/displayErrors.cy.js CHANGED
@@ -1,6 +1,7 @@
1
  /// <reference types="cypress" />
2
 
3
  import 'cypress-file-upload';
 
4
 
5
  describe('load existing errored croissant', () => {
6
  it('should display errors', () => {
@@ -21,10 +22,15 @@ describe('load existing errored croissant', () => {
21
  })
22
  cy.get('[data-testid="stMarkdownContainer"]').contains("Errors").should('not.exist')
23
  // Empty the `name` field to create an error:
24
- cy.get('[data-testid="stMarkdownContainer"]').contains('RecordSets').click()
 
 
25
  cy.contains('split_enums (2 fields)').click()
26
  cy.get('input[aria-label="Name:red[*]"][value="split_enums"]').should('be.visible').type('{selectall}{backspace}{enter}')
27
- cy.get('[data-testid="stMarkdownContainer"]').contains('Overview').click()
 
 
 
28
  cy.get('[data-testid="stMarkdownContainer"]').contains("Errors").should('exist')
29
  })
30
  })
 
1
  /// <reference types="cypress" />
2
 
3
  import 'cypress-file-upload';
4
+ import 'cypress-iframe';
5
 
6
  describe('load existing errored croissant', () => {
7
  it('should display errors', () => {
 
22
  })
23
  cy.get('[data-testid="stMarkdownContainer"]').contains("Errors").should('not.exist')
24
  // Empty the `name` field to create an error:
25
+ cy.enter('[title="components.tabs.tabs_component"]').then(getBody => {
26
+ getBody().contains('RecordSets').click()
27
+ })
28
  cy.contains('split_enums (2 fields)').click()
29
  cy.get('input[aria-label="Name:red[*]"][value="split_enums"]').should('be.visible').type('{selectall}{backspace}{enter}')
30
+ cy.timeout(2000)
31
+ cy.enter('[title="components.tabs.tabs_component"]').then(getBody => {
32
+ getBody().contains('Overview').click({force: true})
33
+ })
34
  cy.get('[data-testid="stMarkdownContainer"]').contains("Errors").should('exist')
35
  })
36
  })
cypress/e2e/loadCroissant.cy.js CHANGED
@@ -1,6 +1,7 @@
1
  /// <reference types="cypress" />
2
 
3
  import 'cypress-file-upload';
 
4
  import * as path from 'path';
5
 
6
  describe('Editor loads Croissant without Error', () => {
@@ -20,7 +21,9 @@ describe('Editor loads Croissant without Error', () => {
20
  events: ["dragenter", "drop"],
21
  })
22
  })
23
- cy.get('button').contains('Metadata').click()
 
 
24
 
25
  cy
26
  .get("[data-testid='element-container']")
@@ -47,7 +50,9 @@ describe('Editor loads Croissant without Error', () => {
47
 
48
  cy.get('[data-testid="stException"]').should('not.exist')
49
 
50
- cy.get('button').contains('Export').should('exist').should('be.visible').click({force: true})
 
 
51
  cy.fixture('titanic.json').then((fileContent) => {
52
  const downloadsFolder = Cypress.config("downloadsFolder");
53
  cy.readFile(path.join(downloadsFolder, "croissant-titanic.json"))
 
1
  /// <reference types="cypress" />
2
 
3
  import 'cypress-file-upload';
4
+ import 'cypress-iframe';
5
  import * as path from 'path';
6
 
7
  describe('Editor loads Croissant without Error', () => {
 
21
  events: ["dragenter", "drop"],
22
  })
23
  })
24
+ cy.enter('[title="components.tabs.tabs_component"]').then(getBody => {
25
+ getBody().contains('Metadata').click()
26
+ })
27
 
28
  cy
29
  .get("[data-testid='element-container']")
 
50
 
51
  cy.get('[data-testid="stException"]').should('not.exist')
52
 
53
+ cy.enter('[title="components.tabs.tabs_component"]').then(getBody => {
54
+ getBody().contains('Export').click()
55
+ })
56
  cy.fixture('titanic.json').then((fileContent) => {
57
  const downloadsFolder = Cypress.config("downloadsFolder");
58
  cy.readFile(path.join(downloadsFolder, "croissant-titanic.json"))
cypress/e2e/renameDistribution.cy.js CHANGED
@@ -21,20 +21,18 @@ describe('Renaming of FileObjects/FileSets/RecordSets/Fields.', () => {
21
  events: ["dragenter", "drop"],
22
  })
23
  })
24
- cy.get('button').contains('Resources').click()
25
- cy.enter('[title="components.tree.tree_component"]').then(getBody => {
26
- // Click on genders.csv
27
- getBody().contains('genders.csv').click()
28
  })
29
- // TODO(marcenacp): There is a bug where this action has to be performed twice.
30
- cy.get('button').contains('Resources').click()
31
  cy.enter('[title="components.tree.tree_component"]').then(getBody => {
32
  // Click on genders.csv
33
  getBody().contains('genders.csv').click()
34
  })
35
  cy.get('input[aria-label="Name:red[*]"][value="genders.csv"]').type('{selectall}{backspace}the-new-name{enter}')
36
 
37
- cy.get('button').contains('RecordSets').click()
 
 
38
  cy.contains('genders').click()
39
  cy.contains('Edit fields details').click()
40
  cy.contains('the-new-name')
 
21
  events: ["dragenter", "drop"],
22
  })
23
  })
24
+ cy.enter('[title="components.tabs.tabs_component"]').then(getBody => {
25
+ getBody().contains('Resources').click()
 
 
26
  })
 
 
27
  cy.enter('[title="components.tree.tree_component"]').then(getBody => {
28
  // Click on genders.csv
29
  getBody().contains('genders.csv').click()
30
  })
31
  cy.get('input[aria-label="Name:red[*]"][value="genders.csv"]').type('{selectall}{backspace}the-new-name{enter}')
32
 
33
+ cy.enter('[title="components.tabs.tabs_component"]').then(getBody => {
34
+ getBody().contains('RecordSets').click()
35
+ })
36
  cy.contains('genders').click()
37
  cy.contains('Edit fields details').click()
38
  cy.contains('the-new-name')
cypress/e2e/uploadCsv.cy.js CHANGED
@@ -11,12 +11,14 @@ describe('Editor loads a local CSV as a resource', () => {
11
  cy.get('button').contains('Create').click()
12
 
13
  cy.get('input[aria-label="Name:red[*]"]').type('MyDataset').blur()
14
- cy.get('[data-testid="stMarkdownContainer"]')
15
- .contains('Metadata')
16
- .click()
17
  cy.get('input[aria-label="URL:red[*]"]').type('https://mydataset.com', {force: true})
18
 
19
- cy.get('[data-testid="stMarkdownContainer"]').contains('Resources').click()
 
 
20
  // Drag and drop mimicking: streamlit/e2e/specs/st_file_uploader.spec.js.
21
  cy.fixture('base.csv').then((fileContent) => {
22
  const file = {
@@ -43,7 +45,9 @@ describe('Editor loads a local CSV as a resource', () => {
43
  cy.contains('First rows of data:')
44
 
45
  // On the record set page, we see the record set.
46
- cy.get('[data-testid="stMarkdownContainer"]').contains('RecordSets').click()
 
 
47
  cy.contains('base.csv_record_set (2 fields)').click()
48
  // We also see the fields with the proper types.
49
  cy.get('[data-testid="stDataFrameResizable"]').contains("column1")
 
11
  cy.get('button').contains('Create').click()
12
 
13
  cy.get('input[aria-label="Name:red[*]"]').type('MyDataset').blur()
14
+ cy.enter('[title="components.tabs.tabs_component"]').then(getBody => {
15
+ getBody().contains('Metadata').click()
16
+ })
17
  cy.get('input[aria-label="URL:red[*]"]').type('https://mydataset.com', {force: true})
18
 
19
+ cy.enter('[title="components.tabs.tabs_component"]').then(getBody => {
20
+ getBody().contains('Resources').click()
21
+ })
22
  // Drag and drop mimicking: streamlit/e2e/specs/st_file_uploader.spec.js.
23
  cy.fixture('base.csv').then((fileContent) => {
24
  const file = {
 
45
  cy.contains('First rows of data:')
46
 
47
  // On the record set page, we see the record set.
48
+ cy.enter('[title="components.tabs.tabs_component"]').then(getBody => {
49
+ getBody().contains('RecordSets').click()
50
+ })
51
  cy.contains('base.csv_record_set (2 fields)').click()
52
  // We also see the fields with the proper types.
53
  cy.get('[data-testid="stDataFrameResizable"]').contains("column1")
events/fields.py CHANGED
@@ -3,8 +3,6 @@ from typing import Any
3
 
4
  import streamlit as st
5
 
6
- from core.constants import RECORD_SETS
7
- from core.query_params import set_tab
8
  from core.state import Field
9
  from core.state import Metadata
10
  import mlcroissant as mlc
@@ -79,7 +77,6 @@ def handle_field_change(
79
  key: str,
80
  **kwargs,
81
  ):
82
- set_tab(RECORD_SETS)
83
  value = st.session_state[key]
84
  if change == FieldEvent.NAME:
85
  old_name = field.name
 
3
 
4
  import streamlit as st
5
 
 
 
6
  from core.state import Field
7
  from core.state import Metadata
8
  import mlcroissant as mlc
 
77
  key: str,
78
  **kwargs,
79
  ):
 
80
  value = st.session_state[key]
81
  if change == FieldEvent.NAME:
82
  old_name = field.name
events/metadata.py CHANGED
@@ -2,8 +2,6 @@ import enum
2
 
3
  import streamlit as st
4
 
5
- from core.constants import METADATA
6
- from core.query_params import set_tab
7
  from core.state import Metadata
8
 
9
 
@@ -18,7 +16,6 @@ class MetadataEvent(enum.Enum):
18
 
19
 
20
  def handle_metadata_change(event: MetadataEvent, metadata: Metadata, key: str):
21
- set_tab(METADATA)
22
  if event == MetadataEvent.NAME:
23
  metadata.name = st.session_state[key]
24
  elif event == MetadataEvent.DESCRIPTION:
 
2
 
3
  import streamlit as st
4
 
 
 
5
  from core.state import Metadata
6
 
7
 
 
16
 
17
 
18
  def handle_metadata_change(event: MetadataEvent, metadata: Metadata, key: str):
 
19
  if event == MetadataEvent.NAME:
20
  metadata.name = st.session_state[key]
21
  elif event == MetadataEvent.DESCRIPTION:
events/record_sets.py CHANGED
@@ -2,9 +2,7 @@ import enum
2
 
3
  import streamlit as st
4
 
5
- from core.constants import RECORD_SETS
6
  from core.query_params import expand_record_set
7
- from core.query_params import set_tab
8
  from core.state import Metadata
9
  from core.state import RecordSet
10
 
@@ -18,7 +16,6 @@ class RecordSetEvent(enum.Enum):
18
 
19
 
20
  def handle_record_set_change(event: RecordSetEvent, record_set: RecordSet, key: str):
21
- set_tab(RECORD_SETS)
22
  value = st.session_state[key]
23
  if event == RecordSetEvent.NAME:
24
  old_name = record_set.name
 
2
 
3
  import streamlit as st
4
 
 
5
  from core.query_params import expand_record_set
 
6
  from core.state import Metadata
7
  from core.state import RecordSet
8
 
 
16
 
17
 
18
  def handle_record_set_change(event: RecordSetEvent, record_set: RecordSet, key: str):
 
19
  value = st.session_state[key]
20
  if event == RecordSetEvent.NAME:
21
  old_name = record_set.name
events/resources.py CHANGED
@@ -2,8 +2,6 @@ import enum
2
 
3
  import streamlit as st
4
 
5
- from core.constants import RESOURCES
6
- from core.query_params import set_tab
7
  from core.state import FileObject
8
  from core.state import FileSet
9
  from core.state import Metadata
@@ -23,7 +21,6 @@ class ResourceEvent(enum.Enum):
23
 
24
 
25
  def handle_resource_change(event: ResourceEvent, resource: Resource, key: str):
26
- set_tab(RESOURCES)
27
  value = st.session_state[key]
28
  if event == ResourceEvent.NAME:
29
  old_name = resource.name
 
2
 
3
  import streamlit as st
4
 
 
 
5
  from core.state import FileObject
6
  from core.state import FileSet
7
  from core.state import Metadata
 
21
 
22
 
23
  def handle_resource_change(event: ResourceEvent, resource: Resource, key: str):
 
24
  value = st.session_state[key]
25
  if event == ResourceEvent.NAME:
26
  old_name = resource.name
utils.py CHANGED
@@ -4,6 +4,7 @@ from core.past_projects import open_project
4
  from core.query_params import get_project_timestamp
5
  from core.state import CurrentProject
6
  from core.state import Metadata
 
7
  from core.state import SelectedRecordSet
8
  from core.state import SelectedResource
9
  import mlcroissant as mlc
@@ -17,17 +18,14 @@ def init_state(force=False):
17
  """Initializes the session state. `force=True` to force re-initializing it."""
18
 
19
  timestamp = get_project_timestamp()
20
- if timestamp and not force:
21
- project = CurrentProject.from_timestamp(timestamp)
22
- if (
23
- project
24
- and CurrentProject not in st.session_state
25
- and Metadata not in st.session_state
26
- ):
27
- st.session_state[CurrentProject] = project
28
- st.session_state[Metadata] = open_project(project.path)
29
- else:
30
- st.session_state[CurrentProject] = None
31
 
32
  if Metadata not in st.session_state or force:
33
  st.session_state[Metadata] = Metadata()
@@ -41,6 +39,9 @@ def init_state(force=False):
41
  if SelectedResource not in st.session_state or force:
42
  st.session_state[SelectedRecordSet] = None
43
 
 
 
 
44
  # Uncomment those lines if you work locally in order to avoid clicks at each reload.
45
  # And comment all previous lines in `init_state`.
46
  # if mlc.Dataset not in st.session_state or force:
 
4
  from core.query_params import get_project_timestamp
5
  from core.state import CurrentProject
6
  from core.state import Metadata
7
+ from core.state import OpenTab
8
  from core.state import SelectedRecordSet
9
  from core.state import SelectedResource
10
  import mlcroissant as mlc
 
18
  """Initializes the session state. `force=True` to force re-initializing it."""
19
 
20
  timestamp = get_project_timestamp()
21
+ if CurrentProject not in st.session_state or force:
22
+ if timestamp:
23
+ project = CurrentProject.from_timestamp(timestamp)
24
+ if project:
25
+ st.session_state[CurrentProject] = project
26
+ st.session_state[Metadata] = open_project(project.path)
27
+ else:
28
+ st.session_state[CurrentProject] = None
 
 
 
29
 
30
  if Metadata not in st.session_state or force:
31
  st.session_state[Metadata] = Metadata()
 
39
  if SelectedResource not in st.session_state or force:
40
  st.session_state[SelectedRecordSet] = None
41
 
42
+ if OpenTab not in st.session_state or force:
43
+ st.session_state[OpenTab] = None
44
+
45
  # Uncomment those lines if you work locally in order to avoid clicks at each reload.
46
  # And comment all previous lines in `init_state`.
47
  # if mlc.Dataset not in st.session_state or force:
views/files.py CHANGED
@@ -2,14 +2,12 @@ import streamlit as st
2
 
3
  from components.tree import render_tree
4
  from core.constants import DF_HEIGHT
5
- from core.constants import RESOURCES
6
  from core.files import file_from_form
7
  from core.files import file_from_upload
8
  from core.files import file_from_url
9
  from core.files import FILE_OBJECT
10
  from core.files import FILE_TYPES
11
  from core.files import RESOURCE_TYPES
12
- from core.query_params import set_tab
13
  from core.record_sets import infer_record_sets
14
  from core.state import FileObject
15
  from core.state import FileSet
@@ -27,6 +25,7 @@ _MANUAL_RESOURCE_TYPE_KEY = "create_manually_type"
27
  _MANUAL_NAME_KEY = "manual_object_name"
28
  _MANUAL_DESCRIPTION_KEY = "manual_object_description"
29
  _MANUAL_SHA256_KEY = "manual_object_sha256"
 
30
 
31
 
32
  def render_files():
@@ -69,7 +68,6 @@ def _render_resources_panel(files: list[Resource]) -> Resource | None:
69
  if not name:
70
  return None
71
  file = filename_to_file[name]
72
- set_tab(RESOURCES)
73
  return file
74
 
75
 
@@ -104,9 +102,13 @@ def _render_upload_panel():
104
  "SHA256",
105
  key=_MANUAL_SHA256_KEY,
106
  )
107
- st.text_input(
 
 
 
108
  "Parent",
109
- key="manual_parent",
 
110
  )
111
 
112
  def handle_on_click():
@@ -126,6 +128,7 @@ def _render_upload_panel():
126
  name = st.session_state[_MANUAL_NAME_KEY]
127
  description = st.session_state[_MANUAL_DESCRIPTION_KEY]
128
  sha256 = st.session_state[_MANUAL_SHA256_KEY] if needs_sha256 else None
 
129
  errorMessage = (
130
  "Please import either a local file, provide a download URL or fill"
131
  " in all required fields: name"
@@ -141,7 +144,7 @@ def _render_upload_panel():
141
  )
142
  return
143
  file = file_from_form(
144
- file_type, resource_type, name, description, sha256, names
145
  )
146
 
147
  st.session_state[Metadata].add_distribution(file)
 
2
 
3
  from components.tree import render_tree
4
  from core.constants import DF_HEIGHT
 
5
  from core.files import file_from_form
6
  from core.files import file_from_upload
7
  from core.files import file_from_url
8
  from core.files import FILE_OBJECT
9
  from core.files import FILE_TYPES
10
  from core.files import RESOURCE_TYPES
 
11
  from core.record_sets import infer_record_sets
12
  from core.state import FileObject
13
  from core.state import FileSet
 
25
  _MANUAL_NAME_KEY = "manual_object_name"
26
  _MANUAL_DESCRIPTION_KEY = "manual_object_description"
27
  _MANUAL_SHA256_KEY = "manual_object_sha256"
28
+ _MANUAL_PARENT_KEY = "manual_object_parents"
29
 
30
 
31
  def render_files():
 
68
  if not name:
69
  return None
70
  file = filename_to_file[name]
 
71
  return file
72
 
73
 
 
102
  "SHA256",
103
  key=_MANUAL_SHA256_KEY,
104
  )
105
+ parent_options = [
106
+ file.name for file in st.session_state[Metadata].distribution
107
+ ]
108
+ st.multiselect(
109
  "Parent",
110
+ options=parent_options,
111
+ key=_MANUAL_PARENT_KEY,
112
  )
113
 
114
  def handle_on_click():
 
128
  name = st.session_state[_MANUAL_NAME_KEY]
129
  description = st.session_state[_MANUAL_DESCRIPTION_KEY]
130
  sha256 = st.session_state[_MANUAL_SHA256_KEY] if needs_sha256 else None
131
+ parents = st.session_state[_MANUAL_PARENT_KEY]
132
  errorMessage = (
133
  "Please import either a local file, provide a download URL or fill"
134
  " in all required fields: name"
 
144
  )
145
  return
146
  file = file_from_form(
147
+ file_type, resource_type, name, description, sha256, parents, names
148
  )
149
 
150
  st.session_state[Metadata].add_distribution(file)
views/overview.py CHANGED
@@ -1,3 +1,5 @@
 
 
1
  import streamlit as st
2
 
3
  from core.state import Metadata
@@ -7,6 +9,13 @@ from views.metadata import handle_metadata_change
7
  from views.metadata import MetadataEvent
8
 
9
 
 
 
 
 
 
 
 
10
  def render_overview():
11
  metadata: Metadata = st.session_state[Metadata]
12
  col1, col2 = st.columns([1, 1], gap="medium")
@@ -39,8 +48,12 @@ def render_overview():
39
  args=(MetadataEvent.DESCRIPTION, metadata, key),
40
  )
41
 
42
- st.subheader(f"{len(metadata.distribution)} Files")
43
- st.subheader(f"{len(metadata.record_sets)} Record Sets")
 
 
 
 
44
  with col2:
45
  user_started_editing = metadata.record_sets or metadata.distribution
46
  if user_started_editing:
 
1
+ from typing import Any
2
+
3
  import streamlit as st
4
 
5
  from core.state import Metadata
 
9
  from views.metadata import MetadataEvent
10
 
11
 
12
+ def _plural(array: list[Any]):
13
+ if array:
14
+ return "s"
15
+ else:
16
+ return ""
17
+
18
+
19
  def render_overview():
20
  metadata: Metadata = st.session_state[Metadata]
21
  col1, col2 = st.columns([1, 1], gap="medium")
 
48
  args=(MetadataEvent.DESCRIPTION, metadata, key),
49
  )
50
 
51
+ st.subheader(
52
+ f"{len(metadata.distribution)} File" + _plural(metadata.distribution)
53
+ )
54
+ st.subheader(
55
+ f"{len(metadata.record_sets)} Record Set" + _plural(metadata.distribution)
56
+ )
57
  with col2:
58
  user_started_editing = metadata.record_sets or metadata.distribution
59
  if user_started_editing:
views/record_sets.py CHANGED
@@ -92,9 +92,7 @@ def _handle_create_record_set():
92
  metadata.add_record_set(RecordSet(name="new-record-set", description=""))
93
 
94
 
95
- def _handle_fields_change(
96
- record_set_key: int, record_set: RecordSet, params: dict[str, Any]
97
- ):
98
  expand_record_set(record_set=record_set)
99
  data_editor_key = _data_editor_key(record_set_key, record_set)
100
  result = st.session_state[data_editor_key]
 
92
  metadata.add_record_set(RecordSet(name="new-record-set", description=""))
93
 
94
 
95
+ def _handle_fields_change(record_set_key: int, record_set: RecordSet):
 
 
96
  expand_record_set(record_set=record_set)
97
  data_editor_key = _data_editor_key(record_set_key, record_set)
98
  result = st.session_state[data_editor_key]
views/wizard.py CHANGED
@@ -3,10 +3,16 @@ import json
3
  import streamlit as st
4
  import streamlit_nested_layout # Do not remove this allows nesting columns.
5
 
 
 
 
 
 
6
  from core.constants import TABS
7
  from core.past_projects import save_current_project
8
- from core.query_params import go_to_tab
9
  from core.state import Metadata
 
10
  import mlcroissant as mlc
11
  from views.files import render_files
12
  from views.metadata import render_metadata
@@ -14,31 +20,33 @@ from views.overview import render_overview
14
  from views.record_sets import render_record_sets
15
 
16
 
17
- def render_export_button(col):
18
  metadata: Metadata = st.session_state[Metadata]
19
  try:
20
- col.download_button(
21
- "Export",
22
- file_name=f"croissant-{metadata.name.lower()}.json",
23
- type="primary",
24
- data=json.dumps(metadata.to_canonical().to_json()),
25
- help="Export the Croissant JSON-LD",
26
- )
27
  except mlc.ValidationError as exception:
28
- col.download_button("Export", disabled=True, data="", help=str(exception))
29
 
30
 
31
  def render_editor():
32
- col1, col2 = st.columns([10, 1])
33
- render_export_button(col2)
34
- tab1, tab2, tab3, tab4 = col1.tabs(TABS)
35
 
36
- with tab1:
 
 
 
 
 
 
37
  render_overview()
38
- with tab2:
39
  render_metadata()
40
- with tab3:
41
  render_files()
42
- with tab4:
43
  render_record_sets()
44
  save_current_project()
 
 
3
  import streamlit as st
4
  import streamlit_nested_layout # Do not remove this allows nesting columns.
5
 
6
+ from components.tabs import render_tabs
7
+ from core.constants import METADATA
8
+ from core.constants import OVERVIEW
9
+ from core.constants import RECORD_SETS
10
+ from core.constants import RESOURCES
11
  from core.constants import TABS
12
  from core.past_projects import save_current_project
13
+ from core.state import get_tab
14
  from core.state import Metadata
15
+ from core.state import set_tab
16
  import mlcroissant as mlc
17
  from views.files import render_files
18
  from views.metadata import render_metadata
 
20
  from views.record_sets import render_record_sets
21
 
22
 
23
+ def _export_json() -> str | None:
24
  metadata: Metadata = st.session_state[Metadata]
25
  try:
26
+ return {
27
+ "name": f"croissant-{metadata.name.lower()}.json",
28
+ "content": json.dumps(metadata.to_canonical().to_json()),
29
+ }
 
 
 
30
  except mlc.ValidationError as exception:
31
+ return None
32
 
33
 
34
  def render_editor():
35
+ export_json = _export_json()
 
 
36
 
37
+ # Warning: the custom component cannot be nested in a st.columns or it is forced to
38
+ # re-render even if a `key` is set.
39
+ selected_tab = get_tab()
40
+ tab = render_tabs(
41
+ tabs=TABS, selected_tab=selected_tab, json=export_json, key="tabs"
42
+ )
43
+ if tab == OVERVIEW:
44
  render_overview()
45
+ elif tab == METADATA:
46
  render_metadata()
47
+ elif tab == RESOURCES:
48
  render_files()
49
+ elif tab == RECORD_SETS:
50
  render_record_sets()
51
  save_current_project()
52
+ set_tab(tab)