Skip to content

Conversation

gribnoysup
Copy link
Collaborator

@gribnoysup gribnoysup commented Nov 19, 2021

Description

This PR extracts sidebar navigation into its own component that is fully detached from the application state and rewritten with keyboard support and using leafygreen components and styles where possible. Here's how it looks:

Mouse Navigation Keyboard Navigation Async Collections
sidebar1 sidebar2 sidebar-async

This PR doesn't flip the switch on the global overlay removal yet as we need the databases-collections change to also be ready before this happens, so if you want to try this new sidenav locally with async you need to set the COMPASS_NO_GLOBAL_OVERLAY env var to true

Direct link to the navigation tree component because github collapsed it (tell me if you think I should split it into more parts, so far I extracted only things that we might use outside of the component)

Checklist

  • Add unit tests for new components
  • Check the checks
  • Update e2e tests for the new sidebar
  • Animation for the placeholder (will do in a separate PR so that I can have a shared style between sidebar and databases-collections)
  • All namespace actions should work as before
    • select-database
    • drop-database
    • select-collection
    • create-collection
    • drop-collection
    • open-in-new-tab
    • duplicate-view
    • modify-view

Motivation and Context

Types of changes

  • Backport Needed
  • Patch (non-breaking change which fixes an issue)
  • Minor (non-breaking change which adds functionality)
  • Major (fix or feature that would cause existing functionality to change)

});

const databaseItemContainer = css({
contentVisibility: 'auto',
Copy link
Collaborator Author

@gribnoysup gribnoysup Nov 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we are now on a pretty modern version of Chromium I though that we might try using content-visibility instead of the react-virtualized. I didn't notice any significant performance degradation, even the opposite as we are doing much less re-layouts now and don't need to watch for the container size changes, but please try it out locally and tell me if you see any weird behaviors

@@ -0,0 +1,369 @@
/* eslint-disable @typescript-eslint/no-empty-function */
Copy link
Collaborator

@mcasimir mcasimir Nov 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having this component here implies a broader architectural decision, would be great if compass-components could only contain domain-free components and raw building blocks, that's how we are approaching this so far at least.

Breaking this pattern has a good potential to create confusion and we won't have anymore a clear cut as for when to put something in components and when not. For example by the same token the connection form could have ended up here.

Is there a reason (other than ts) why this should not be in the sidebar?

Copy link
Collaborator Author

@gribnoysup gribnoysup Nov 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was assuming the only goal with this package is not to have any direct connection to compass internals (which some hadron components have for example), thanks for clarifying that! The main reason was indeed that converting the whole sidebar plugin codebase to the new ts, code formatting, linting, and testing setup would be too much work upfront and as this side navigation is just a react component, putting it here made sense to me. I'm happy to move all this to the separate new monorepo package or work on converting the plugin to our new tooling, whatever makes more sense in your opinion.

Breaking this pattern has a good potential to create confusion and we won't have anymore a clear cut as for when to put something in components and when not. For example by the same token the connection form could have ended up here.

FWIW just the fact that we would need to make a decision when creating a new component already makes it more confusing than just "put it in compass-components"

I'm totally okay with following the "only contain domain-free components and raw building blocks", but not really sure though why we are doing it. From my POV if keeping plugins (and tbh most of the other packages in the monorepo) in separate packages was more or less a decision based on how they were published before, consumed by Compass, and the fact that they have slightly different build/test setups, for the new components I don't really see a reason to create multiple packages like that, so I wonder what might be the motivation that I'm missing.

Copy link
Collaborator

@mcasimir mcasimir Nov 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I totally get your point, if is not a big deal for you I'd stick to the pattern where compass-components just domain-free for now. Practically speaking honestly is fine to do this in a separate PR.

I don't mean to be too annoying here, and we can change this rule, but this is my rationale: despite of how good this convention is, and how fit is the current "plugin-based" design with the monorepo, for me we should stick with it until we consciously decide to change, and if and when we do, we should change things everywhere and consistently.

To me is really important that we don't take this decision lightly, and we don't make it happen by accident or as a byproduct of something else (ie ts) cause essentially it would create a situation where 2 different "frontends" would exist, one in the plugins and one in compass-components, and since it won't be fun to undo that, if that's the direction we want to take we should also make a commitment and a plan to move to only one, at least not leaving it to the case.

I'm totally okay with following the "only contain domain-free components and raw building blocks", but not really sure though why we are doing it. From my POV if keeping plugins (and tbh most of the other packages in the monorepo) in separate packages was more or less a decision based on how they were published before, consumed by Compass, and the fact that they have slightly different build/test setups, for the new components I don't really see a reason to create multiple packages like that, so I wonder what might be the motivation that I'm missing.

I think here there are 2 design decisions involved though:

  • The first decision is for compass-components: this one is basically a central place for reusable UI components, is a way to change in one place how the application looks, kinda a re-export from LG + the components we miss from LG, but that's the level and purpose, just a way to factor together duplicate styles / interactions.

  • The second one is about the plugins: i don't think the separation they create is so terrible, but i get that we don't need to have them in 100 packages. Also we have many that do not make so much sense. The general fact that we have things with significantly different state / doing different actions and with an independent set of functionalities separated makes sense to me, but yeah those could be just different folders in a package rather then different packages and i'd be ok with that.

Personally i think those should be 2 different decisions, for example if we remove the packages then we could make everything just a folder inside the same package, but compass-components could still be a subfolder, we would import <Button> and colors.gray1 from there and sidebar could be another one.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if is not a big deal for you I'd stick to the pattern where compass-components just domain-free for now

Yeah, absolutely, will move it out, thanks for sharing your thoughts on this.

Copy link
Collaborator Author

@gribnoysup gribnoysup Nov 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 22c421e, but tbh I'm not sure if I should've moved more or less stuff, in theory useTree hooks are pretty generic, but then also would we really have another navigation tree view in the app? Idk, anyway, tell me what you think and if this matches what you were expecting

- Provide new app registry events that instance-store can handle to resolve collections metadata and show collection screens
- Move collection submenu calls to one place that closest to showing collections on the screen so that we don't need to do it all over the place
Copy link
Member

@Anemy Anemy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is real cool. Sick cleanup and adding of accessibility. Feels nice to use/open/close databases and collections. I like the nav tree component and hooks, solid tests too.
Added a couple comments and noticed a few spots we can remove some old code.

I ran into two things playing around with this branch on my computer:
One was scrolling with a lot of collections in the sidebar was making the scroll jump unexpectedly. This can be seen with the test db in the compass-data-sets deployment.

many.collections.scroll.issue.mp4

The other issue I noticed was when I searched for dbs/collections with the filter they weren't loading. Is that work planned elsewhere?

Do you have a database with a lot of collections to test the content-visibility? I think the main reason why we used react-virtualized in the collection/db list was for cases where the user has hundreds or thousands of dbs/collections. Probably would be good to test that manually before merging.


const letter = evt.key.toLocaleLowerCase();
const maybeNode = nodes
.slice(currentFocusElementIndex + 1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something from the aria tree view docs:
Search wraps to first node if a matching name is not found among the nodes that follow the focused node.
Maybe on this check we can add the preceding elements after the slice?

I like how we're using the Letter regex here - I couldn't really find good docs for it though, does it match more than just A-Za-z? I'm wondering if we're supporting more languages by using it. Nice.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Missed this part of the spec, will fix.

Yeah, it's basically catching all the unicode symbols that have LETTER property. This \p is called unicode property escape and it allows you to match by unicode properties. There is a ton of cases where it can simplify targeting your regex, for example you can catch all emojis with {Emoji_Presentation} group

sourcePipeline
})
);
showCollectionSubmenu({ isReadOnly: isReadonly });
Copy link
Member

@Anemy Anemy Nov 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we also want to add this to createNewTab or somewhere so that this show collection submenu gets called when the user clicks the Open in New Tab menu item when they're on a database page.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This selectOrCreateTab method uses both createNewTab and replaceTabContent, it also the only method that is always called when tabs are switching, this is why I put it here, I can either move the call out to those methods separately, or keep it here, what do you prefer?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking it probably makes sense to move it to them into both createNewTab and replaceTabContent since we can open up a collection view from

which uses createNewTab without calling selectOrCreateTab.

import { toggleIsModalVisible } from '../../modules/is-modal-visible';
import { saveFavorite } from '../../modules/connection-model';

import { TOOLTIP_IDS } from '../../constants/sidebar-constants';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove this variable? I think it's unused now.


renderCreateDatabaseButton() {
if (!this.isReadonlyDistro() && !this.props.isDataLake) {
const tooltipText = this.props.description;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove this description prop now? Looks like it's unused.

});

const databaseItem = css({
height: DATABASE_ROW_HEIGHT,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently in Compass the database and collections tree items have a cursor: pointer on hover, do we want to keep that functionality? I have no preference.

@gribnoysup
Copy link
Collaborator Author

gribnoysup commented Nov 23, 2021

Do you have a database with a lot of collections to test the content-visibility? I think the main reason why we used react-virtualized in the collection/db list was for cases where the user has hundreds or thousands of dbs/collections. Probably would be good to test that manually before merging.

So content-visibility is a property that does same thing that react-virtualized does just natively by the browser. Yes, as I mentioned I tested it with both a database that has 10k collections and a server that has ~1k databases (my local setup couldn't handle creating more than that), the performance looked similar to me (the rendering of the sidebar is mostly fine, but generally it's bad both in this branch and in current Compass, for a different reason though, see COMPASS-5316), but I am not getting the scroll jitter so I'll change it back to react-virtualized (or whatever it was deprecated in favor of, seems like it's react-window now?)

@gribnoysup
Copy link
Collaborator Author

The other issue I noticed was when I searched for dbs/collections with the filter they weren't loading. Is that work planned elsewhere?

Is this with or without global overlay? If without then it's kinda expected for now, but I might just fix it in this branch so it's not confusing

@Anemy
Copy link
Member

Anemy commented Nov 23, 2021

The other issue I noticed was when I searched for dbs/collections with the filter they weren't loading. Is that work planned elsewhere?

Is this with or without global overlay? If without then it's kinda expected for now, but I might just fix it in this branch so it's not confusing

Without. Cool, no worries, just making sure we're aware of it.

…rtualized

Keyboard support is not implemented yet, it also needs full rewrite to work with
virtual lists (and some tests are definitely broken now)
… for virtual list

* and first letter are still not implemented and the tests need a re-write now
…ify types

Still need to fix tests and remove stuff from compass-components as it is tightly
coupled to the virtual list implementation now
@gribnoysup
Copy link
Collaborator Author

@Anemy this turned out to be an almost full rewrite to make it accessible with react-virtualized so even though I'm still cleaning up stuff and writing tests, maybe you already want to take a look if you have a moment?

Copy link
Member

@Anemy Anemy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good stuff. Tested it out a bit and works so smooth. Feels solid to the touch. Nice work on the accessibility improvements. Good tests and nice comments on some of the trickier things with relevant links 👍
Couple questions/ideas/suggestions, but no blockers. Looks good to go once tests are passing!

window.cancelAnimationFrame = function (id) {
clearTimeout(id);
};
require('global-jsdom')(undefined, { pretendToBeVisual: true });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

if (db.collectionsStatus === 'initial') {
if (
db.collectionsStatus === 'initial' ||
db.collectionsStatus === 'fetching'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small thing, but how do you feel about having these states as constants/an enum we can refer to?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does changing that we perform the queries here and in fetchCollectionDetails when the status is fetching have any performance implications? Could we potentially be redoing the same query many times? Just asking for my own understanding, not sure I understand why we're adding this extra status check.

Copy link
Collaborator Author

@gribnoysup gribnoysup Nov 30, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small thing, but how do you feel about having these states as constants/an enum we can refer to?

I would like to, but I legit don't know where to put them and I don't want to move the models in one package at least in the scope of this work if that's okay (I might do this as a separate PR or something, there is enough reasons for that already, sounds alright?)

Could we potentially be redoing the same query many times?

All inflight requests for the model data are currently debounced (for all the instance related models, but I'll link only to one code example), so if multiple parts of the application are calling instance.fetch, they will be awaiting on the same request promise:

const Inflight = new Map();
function debounceInflight(fn) {
return function (...args) {
const callId = this.isCollection
? `${this.parent.cid}$$coll$$${fn.name}`
: `${this.cid}$$${fn.name}`;
if (Inflight.has(callId)) {
return Inflight.get(callId);
}
const promise = fn.call(this, ...args).finally(() => {
Inflight.delete(callId);
});
Inflight.set(callId, promise);
return promise;
};
}
function debounceActions(actions) {
return {
initialize() {
actions.forEach((key) => {
if (key in this && typeof this[key] === 'function') {
const origFn = this[key];
this[key] = debounceInflight(origFn);
}
});
},
};
}

(side note: this type of debouncing probably belongs more on a transport layer, which is data service in our case, but this is a bigger architectural change, we discussed this in one of the previous PRs and decided that it's okay to keep it at the data model level for now)

This particular check here doesn't mean that we start another fetch while the current one is in progress, it just guarantees we wait for the in-progress one to finish before starting to fetch collection info.

Giving it another look I am thinking that I'll probably move this to the models, similar how the whole instance.refresh flow is now part of the model so that the stores are not doing much except for just calling model methods directly and at least that way the statuses are leaking less outside

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 5548eec

sourcePipeline
})
);
showCollectionSubmenu({ isReadOnly: isReadonly });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking it probably makes sense to move it to them into both createNewTab and replaceTabContent since we can open up a collection view from

which uses createNewTab without calling selectOrCreateTab.

>
<AutoSizer
disableWidth={Boolean(process.env.TEST_AUTOSIZER_DEFAULT_WIDTH)}
disableHeight={Boolean(process.env.TEST_AUTOSIZER_DEFAULT_HEIGHT)}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the interest of making Compass more web compatible, would it make sense to have these as optional props or have them polluting the global namespace a bit for tests instead of process env variables? Fine how it is for Compass at the moment, just a thought.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, totally, I can move them to props

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in a2405d7

@gribnoysup
Copy link
Collaborator Author

gribnoysup commented Nov 30, 2021

I'm thinking it probably makes sense to move it to them into both createNewTab and replaceTabContent since we can open up a collection view from

Oh, I totally missed that spot, will fix!

Done in 3eeaddc. Although it's not all possible ways to change active tab and thus doesn't work correctly always, it matches the current behavior, so I'm not fixing some issues in this PR, instead I opened a few tickets (COMPASS-5330 and COMPASS-5331) to sort out these existing issues with the menu

@gribnoysup gribnoysup merged commit b284d99 into main Nov 30, 2021
@gribnoysup gribnoysup deleted the compass-5211-async-sidebar branch November 30, 2021 13:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants