How to create a collapsible sidebar with tree structure of uploaded files

@andrew-hossack,

Yes, you should be able to display a sort of navigation based upon uploaded files.

Yes, the files would need to be saved, I believe you are doing this already. Are you saving the filepath somewhere?

How do you want the nav to look?

I have a list of fully qualified filepaths which I would like to display as a file tree, something like this:

I’ve found some css examples that are a bit useful but nothing plotly specific. Without having to do any heavy lifting I’m hoping to find an elegant solution here.

Edit: Viewing file contents is out of scope. I’m only concerned with a user being able to view a directory structure.

Hi @andrew-hossack

You can do something like that with an Accordion component. Here’s and example using dash-mantine-components:

from dash import Dash
import dash_mantine_components as dmc
from dash_iconify import DashIconify

app = Dash(__name__)

pages = ["home.py", "page1.py", "page2.py"]
assets = ["mycss.css", "app.png", "page1.png"]


def make_file(file_name):
    return dmc.Text(
        [DashIconify(icon="akar-icons:file"), " ", file_name], style={"paddingTop": 10}
    )


def make_folder(folder_name):
    return [DashIconify(icon="akar-icons:folder"), " ", folder_name]


file_dir = dmc.Accordion(
    [
        dmc.AccordionItem([make_file(f) for f in assets], label=make_folder("assets")),
        dmc.AccordionItem([make_file(f) for f in pages], label=make_folder("pages")),
    ],
    multiple=True,
)

app.layout = dmc.Container([file_dir, make_file("app.py")])

if __name__ == "__main__":
    app.run_server(debug=True)

file_tree

4 Likes

@AnnMarieW This is awesome and just what I am looking for, thank you! This is one of the reasons why I love this community so much, folks are willing to help out so readily to share information.

If you’re interested in knowing the motivation, I’m building a simple UI to allow users to upload their apps to Heroku / whatever. Baking this into dashtools.

1 Like

Glad I could help!
Sounds like an awesome addition to dashtools! Looking forward to checking out the next release :rocket:

I was able to build out a fairly easy class to generate trees given a filepath:

class FileTree:

    def __init__(self, filepath: os.PathLike):
        self.filepath = filepath

    def render(self) -> dmc.Accordion:
        return dmc.Accordion(
            self.build_tree(self.filepath, isRoot=True),
            multiple=True)

    def flatten(self, l):
        return [item for sublist in l for item in sublist]

    def make_file(self, file_name):
        return dmc.Text(
            [DashIconify(icon="akar-icons:file"), " ", file_name], style={"paddingTop": '5px'}
        )

    def make_folder(self, folder_name):
        return [DashIconify(icon="akar-icons:folder"), " ", folder_name]

    def build_tree(self, path, isRoot=False):
        d = []
        if os.path.isdir(path):
            children = self.flatten([self.build_tree(os.path.join(path, x))
                                    for x in os.listdir(path)])
            if isRoot:
                d.append(
                    dmc.AccordionItem(
                        children=children,
                        label=self.make_folder(os.path.basename(path)))
                )
            else:
                d.append(
                    dmc.Accordion(children=[
                        dmc.AccordionItem(
                            children=children,
                            label=self.make_folder(os.path.basename(path)))
                    ],
                        multiple=True)
                )
        else:
            d.append(self.make_file(os.path.basename(path)))
        return d

# Usage

import dash_mantine_components as dmc
import os
from dash import Dash
from dash_iconify import DashIconify

app = Dash()

filepath = 'Users/andrew'  # Any filepath here

app.layout = FileTree(filepath).render()

app.run_server()

I’m not sure how to make components but I feel that this could be useful to people. Might move this conversation to a new #show-and-tell thread if there is enough interest.

1 Like

This is awesome! It’s definitely worth it’s own show-and-tell post :+1:

1 Like

Moved to File Explorer Tree Generator

hi @andrew-hossack @AnnMarieW awesome work! Do you know how we can add a search bar at the top of the file tree and match the search string with any of the tree nodes as shown in the image below:

image

For this you could use an input event on a search bar, where it auto expands all and hides based upon matches. Or adds a class to the element. :grin:

1 Like

@jinnyzor I am not sure how to match the search string with the Accordion items. Any hints on how to do so?

Sure, here you go, this is a generic example and based off of dmc:

from dash import Dash, dcc, Input, Output
import dash_mantine_components as dmc
from dash_iconify import DashIconify

app = Dash(__name__, external_scripts=[
        "https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"
    ],)

pages = ["home.py", "page1.py", "page2.py"]
assets = ["mycss.css", "app.png", "page1.png"]


def make_file(file_name):
    return dmc.Text(
        [DashIconify(icon="akar-icons:file"), " ", file_name], style={"paddingTop": 10}
    )


def make_folder(folder_name):
    return [DashIconify(icon="akar-icons:folder"), " ", folder_name]


file_dir = dmc.Accordion(
    [
        dmc.AccordionItem([make_file(f) for f in assets], label=make_folder("assets")),
        dmc.AccordionItem([make_file(f) for f in pages], label=make_folder("pages")),
    ],
    multiple=True,
)

app.layout = dmc.Container([dcc.Input(id='search', placeholder='search directory'), file_dir, make_file("app.py")])

app.clientside_callback(
    """
        function search(v) {
            if (v) {
                $(".mantine-Accordion-item").addClass("mantine-Accordion-itemOpened").addClass("mantine-1xa9lqx")
                $(".mantine-Accordion-item .mantine-1avyp1d").attr("style", "box-sizing: border-box;")
                $(".mantine-Accordion-item .mantine-1avyp1d > div").attr("style", "opacity: 1; transition: opacity 200ms ease 0s;")
                $(".mantine-Accordion-item .mantine-1avyp1d").attr("aria-hidden", false)
                $(".mantine-Accordion-item").each( function() {
                    if ($(this).text().includes(v)) {
                        $(this).removeClass("hidden")
                    } else {
                        $(this).addClass("hidden")
                    }
                    }
                )
                $(".mantine-Accordion-item .mantine-Text-root").each( function () {
                    if ($(this).text().includes(v)) {
                        $(this).removeClass("hidden")
                    } else {
                        $(this).addClass("hidden")
                    }
                    })
            } else {
                $(".mantine-Accordion-item").removeClass("mantine-Accordion-itemOpened")
                $(".mantine-Accordion-item").removeClass("mantine-1xa9lqx")
                $(".mantine-Accordion-item").removeClass("hidden")
                $(".mantine-Accordion-item .mantine-Text-root").removeClass("hidden")
                $(".mantine-Accordion-item .mantine-1avyp1d").attr("style", "box-sizing: border-box; height: 0px; overflow: hidden; display: none;")
                $(".mantine-Accordion-item .mantine-1avyp1d").attr("aria-hidden", true)
                $(".mantine-Accordion-item .mantine-1avyp1d > div").attr("style", "opacity: 0; transition: opacity 200ms ease 0s;")
            }
            
            return window.dash_clientside.no_update
        }
    """,
    Output('search', 'id'),
    Input('search','value'),
    prevent_intial_call=True
)

if __name__ == "__main__":
    app.run_server(debug=True)

It uses jquery in order to be able to navigate quickly, imo. If you are using this for navigation, be sure to empty out the string on navigation click.

@jinnyzor Thanks for the code! I’m not very strong on JavaScript but the code you shared seems to expand every Accordion item regardless of whether the search string matches the item, as shown in the shared video. Ideally I would like to expand only the items that contain the search string. For example, if the search string is mycss then only the folder assets should be expanded and not both the assets and pages folders. If there is no match, no item should be expanded. Any help would be much appreciated!

2022-12-12_14h56_54

Hmm. Weird. I thought it was working for me…

It’s based upon the class names.

@jinnyzor is there anything I need to change before running the code? I simply copied and pasted and ran the code with no changes.

You shouldn’t have to, it is expanding… So part of the code is working…

What are your versions?

@jinnyzor these are my versions:

dash                    2.6.2
dash-core-components    2.0.0
dash-extensions         0.1.6
dash-html-components    2.0.0
dash-mantine-components 0.10.2

Oh!

I’m sorry. I have this is my css file.

.hidden {
 display: none
}

It’s just second nature I guess. I have that as a default in all of my websites. XD

1 Like

@jinnyzor Thanks a lot! That fixed the issue.

There’s a couple of minor issues however.

  1. When the tree elements expand the first time after typing something in the search bar, the little arrow icon next to each tree/Accordion item seems to be in the reverse orientation. The arrows have to be clicked to get them in the right orientation.
  2. The up arrow buttons of the Accordion items, except that of the first Accordion item, do not work the first time they are clicked. They have to be clicked twice in order for the items to contract. For example, when I type ‘a’ in the search bar, the Accordion items, “assets” and “pages”, expand. When I click the up arrow next to “assets”, “assets” contracts, which is the desired behavior. However, when I do the same with “pages”, i.e. click the up arrow next to “pages”, it does nothing. I have to click it again for “pages” to contract.

Here’s a little video showing the two issues:
2022-12-13_14h11_17