Skip to content

[dashboard] lint warnings: NewProject.tsx #8995

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitpod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,5 @@ vscode:
- timonwong.shellcheck
- vscjava.vscode-java-pack
- fwcd.kotlin
- dbaeumer.vscode-eslint
- esbenp.prettier-vscode
33 changes: 23 additions & 10 deletions components/dashboard/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
@tailwind utilities;

@layer base {
html, body {
html,
body {
@apply h-full;
}
body {
Expand Down Expand Up @@ -61,30 +62,42 @@
@apply cursor-default opacity-50 pointer-events-none;
}

a.gp-link {
button.gp-link {
@apply bg-transparent hover:bg-transparent p-0 rounded-none;
}

a.gp-link,
button.gp-link {
@apply text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-500;
}

input[type=text], input[type=search], input[type=password], select {
input[type="text"],
input[type="search"],
input[type="password"],
select {
@apply block w-56 text-gray-600 dark:text-gray-400 bg-white dark:bg-gray-800 rounded-md border border-gray-300 dark:border-gray-500 focus:border-gray-400 dark:focus:border-gray-400 focus:ring-0;
}
input[type=text]::placeholder, input[type=search]::placeholder, input[type=password]::placeholder {
input[type="text"]::placeholder,
input[type="search"]::placeholder,
input[type="password"]::placeholder {
@apply text-gray-400 dark:text-gray-500;
}
input[type=text].error, input[type=password].error, select.error {
input[type="text"].error,
input[type="password"].error,
select.error {
@apply border-gitpod-red dark:border-gitpod-red focus:border-gitpod-red dark:focus:border-gitpod-red;
}
input[disabled] {
@apply bg-gray-100 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 text-gray-400 dark:text-gray-500;
}
input[type=radio] {
input[type="radio"] {
@apply border border-gray-300 focus:border-gray-400 focus:bg-white focus:ring-0;
}
input[type=search] {
input[type="search"] {
@apply border-0 dark:bg-transparent;
}
input[type=checkbox] {
@apply disabled:opacity-50
input[type="checkbox"] {
@apply disabled:opacity-50;
}

progress {
Expand All @@ -99,4 +112,4 @@
progress::-moz-progress-bar {
@apply rounded-md bg-green-500;
}
}
}
128 changes: 69 additions & 59 deletions components/dashboard/src/projects/NewProject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* See License-AGPL.txt in the project root for license information.
*/

import { useContext, useEffect, useState } from "react";
import { useCallback, useContext, useEffect, useState } from "react";
import { getGitpodService, gitpodHostUrl } from "../service/service";
import { iconForAuthProvider, openAuthorizeWindow, simplifyProviderName } from "../provider-utils";
import { AuthProviderInfo, Project, ProviderRepository, Team, TeamMemberInfo, User } from "@gitpod/gitpod-protocol";
Expand Down Expand Up @@ -42,6 +42,29 @@ export default function NewProject() {

const [authProviders, setAuthProviders] = useState<AuthProviderInfo[]>([]);

const updateReposInAccounts = useCallback(
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it really necessary to use useCallback here? I believe we haven't used it elsewhere in the dashboard yet, and if I understand it correctly, calling updateReposInAccounts from the Effect with the dependency selectedProviderHost already ensures that we call it only once every time the dependency changes.

I think I'd prefer not using this feature here, to limit the "magic" in our code. What do you think? 🙂

Copy link
Contributor Author

@trumbitta trumbitta Mar 31, 2022

Choose a reason for hiding this comment

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

Oh it was actually a cascading effect 🙂

  • Without the useCallback, we have this linting warning: "The 'updateReposInAccounts' function makes the dependencies of useEffect Hook (at line 203) change on every render. To fix this, wrap the definition of 'updateReposInAccounts' in its own useCallback() Hook"
  • Leaving updateReposInAccounts out of the dependencies of that useEffect to silence the previous warning, we have this other warning: "React Hook useEffect has a missing dependency: 'updateReposInAccounts'. Either include it or remove the dependency array."
  • Removing the dependency array altogether to silence the previous warning, of course makes the hook execute on every render, while keeping it empty generates the linting warning about not having empty dependency arrays when we are using some dependencies inside the hook.

The only alternative seems to be silencing the ESLint rule for this line, but as long as the code still works as intended I'd rather follow the lead of the linting rules and move on.

Let me know what you want me to do!

Copy link
Contributor

Choose a reason for hiding this comment

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

If I'm not mistaken, you've already added the updateReposInAccounts closure as a dependency to the useEffect. Is that not enough to resolve the warning? (I.e. without needing the extra useCallback?)

Copy link
Contributor

@jankeromnes jankeromnes Mar 31, 2022

Choose a reason for hiding this comment

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

Or, actually I really don't like this pattern of implementing functions that are only called once (EDIT: to clarify, this is a pre-existing problem from before your Pull Request, not something you did here). What about moving all the updateReposInAccounts code back inline into the useEffect? (Maybe as an in-line async function if needed.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What about moving all the updateReposInAccounts code back inline into the useEffect? (Maybe as an in-line async function if needed.)

Oh that's also my usual first move 🤝 but in this case updateReposInAccounts is also used elsewhere outside the useEffect.
Avoidable? Probably yes, but I think it will come with future refactorings 🔮

Copy link
Contributor Author

@trumbitta trumbitta Mar 31, 2022

Choose a reason for hiding this comment

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

If I'm not mistaken, you've already added the updateReposInAccounts closure as a dependency to the useEffect. Is that not enough to resolve the warning? (I.e. without needing the extra useCallback?)

Yes, but we have another warning :D look at the first of my bullet points above:

Without the useCallback, we have this linting warning: "The 'updateReposInAccounts' function makes the dependencies of useEffect Hook (at line 203) change on every render. To fix this, wrap the definition of 'updateReposInAccounts' in its own useCallback() Hook"

Copy link
Contributor Author

@trumbitta trumbitta Mar 31, 2022

Choose a reason for hiding this comment

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

That's because without useCallback the function gets created on every render, and that triggers the dependency array of the useEffect

async (installationId?: string) => {
setLoaded(false);
setReposInAccounts([]);
if (!selectedProviderHost) {
return [];
}
try {
const repos = await getGitpodService().server.getProviderRepositoriesForUser({
provider: selectedProviderHost,
hints: { installationId },
});
setReposInAccounts(repos);
setLoaded(true);
return repos;
} catch (error) {
console.log(error);
}
return [];
},
[selectedProviderHost],
);

useEffect(() => {
if (user && selectedProviderHost === undefined) {
if (user.identities.find((i) => i.authProviderId === "Public-GitLab")) {
Expand All @@ -55,7 +78,7 @@ export default function NewProject() {
setAuthProviders(await getGitpodService().server.getAuthProviders());
})();
}
}, [user]);
}, [selectedProviderHost, user]);

useEffect(() => {
const params = new URLSearchParams(location.search);
Expand All @@ -69,7 +92,7 @@ export default function NewProject() {
window.history.replaceState({}, "", window.location.pathname);
setSelectedTeamOrUser(user);
}
}, []);
}, [location.search, teams, user]);
Copy link
Contributor

@jankeromnes jankeromnes Mar 31, 2022

Choose a reason for hiding this comment

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

I'm not 100% sure whether this change makes sense:

  • Currently, we only call this effect once, and we immediately "consume" the search parameter if found (i.e. if you visit /new?team=1234, we immediately select team 1234 and overwrite the URL to /new without the query parameter). This means that, if we call this code multiple times, only the first time can have any effect.
  • Calling it again if location.search changes sort of makes sense, but can it really happen that location.search changes without a reload? (I guess it could happen if we have something like <Link to="/new?team=1234"/> 🤔)
  • Can teams or user really change here? If teams or user can be something unexpected, I guess we'd need to check for that in the beginning of the effect, and return early if the values are unexpected (e.g. undefined or empty array?)

All in all, since this code currently seems to work in all cases, I'm a bit hesitant to change the effect logic here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I totally get it: this is a classic situation and it happens frequently when you start paying attention to ESLint rules.

My current stance about it, as you can infer from the proposed changes, is to just let ESLint do its thing and use its auto-fix feature: since the resulting code is basically always equivalent to what it was before the auto-fix, the auto-fixed dependency array quickly becomes like an implementation detail of sorts, I'm positive that ESLint has my back, and I can focus on the real features I need to work on.

Again, let me know what you want me to do :)
These are decisions I'm going to respect in all the future PRs about the linting warnings ✅

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks! I generally agree with you, but I believe I had one bad experience where I just followed ESLint and it broke my component 😬 (or, maybe the bug was my fault and I now wrongly blame ESLint for it). I guess this calls for more testing. 👀

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it happened to me once!
I don't remember the specifics but like you said turns out it was my "fault" meaning I then applied "more React" to the problem (IIRC more composition and maybe a custom hook 🤔 ) and that fixed both the warning and the bug 🐞


const [teamMembers, setTeamMembers] = useState<Record<string, TeamMemberInfo[]>>({});
useEffect(() => {
Expand Down Expand Up @@ -123,10 +146,34 @@ export default function NewProject() {
}, [selectedRepo]);

useEffect(() => {
const createProject = async (teamOrUser: Team | User, repo: ProviderRepository) => {
if (!selectedProviderHost) {
return;
}
const repoSlug = repo.path || repo.name;

try {
const project = await getGitpodService().server.createProject({
name: repo.name,
slug: repoSlug,
cloneUrl: repo.cloneUrl,
account: repo.account,
provider: selectedProviderHost,
...(User.is(teamOrUser) ? { userId: teamOrUser.id } : { teamId: teamOrUser.id }),
appInstallationId: String(repo.installationId),
});

setProject(project);
} catch (error) {
const message = (error && error?.message) || "Failed to create new project.";
window.alert(message);
}
};

if (selectedTeamOrUser && selectedRepo) {
createProject(selectedTeamOrUser, selectedRepo);
}
}, [selectedTeamOrUser, selectedRepo]);
}, [selectedTeamOrUser, selectedRepo, selectedProviderHost]);

useEffect(() => {
if (reposInAccounts.length === 0) {
Expand Down Expand Up @@ -155,7 +202,7 @@ export default function NewProject() {
(async () => {
await updateReposInAccounts();
})();
}, [selectedProviderHost]);
}, [selectedProviderHost, updateReposInAccounts]);

useEffect(() => {
if (project && sourceOfConfig) {
Expand All @@ -166,30 +213,10 @@ export default function NewProject() {
await getGitpodService().server.triggerPrebuild(project.id, null);
})();
}
}, [project, sourceOfConfig]);
}, [guessedConfigString, project, sourceOfConfig]);
Copy link
Contributor

Choose a reason for hiding this comment

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

Reading the code, I think depending on guessedConfigString in addition to sourceOfConfig has the same effect. No objection here if it makes the linter happy. 🙂

Copy link
Contributor Author

Choose a reason for hiding this comment

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


const isGitHub = () => selectedProviderHost === "github.com";

const updateReposInAccounts = async (installationId?: string) => {
setLoaded(false);
setReposInAccounts([]);
if (!selectedProviderHost) {
return [];
}
try {
const repos = await getGitpodService().server.getProviderRepositoriesForUser({
provider: selectedProviderHost,
hints: { installationId },
});
setReposInAccounts(repos);
setLoaded(true);
return repos;
} catch (error) {
console.log(error);
}
return [];
};

const reconfigure = () => {
openReconfigureWindow({
account: selectedAccount,
Expand All @@ -203,30 +230,6 @@ export default function NewProject() {
});
};

const createProject = async (teamOrUser: Team | User, repo: ProviderRepository) => {
if (!selectedProviderHost) {
return;
}
const repoSlug = repo.path || repo.name;

try {
const project = await getGitpodService().server.createProject({
name: repo.name,
slug: repoSlug,
cloneUrl: repo.cloneUrl,
account: repo.account,
provider: selectedProviderHost,
...(User.is(teamOrUser) ? { userId: teamOrUser.id } : { teamId: teamOrUser.id }),
appInstallationId: String(repo.installationId),
});

setProject(project);
} catch (error) {
const message = (error && error?.message) || "Failed to create new project.";
window.alert(message);
}
};

const toSimpleName = (fullName: string) => {
const splitted = fullName.split("/");
if (splitted.length < 2) {
Expand All @@ -243,7 +246,7 @@ export default function NewProject() {
const getDropDownEntries = (accounts: Map<string, { avatarUrl: string }>) => {
const renderItemContent = (label: string, icon: string, addClasses?: string) => (
<div className="w-full flex">
<img src={icon} className="rounded-full w-6 h-6 my-auto" />
<img src={icon} className="rounded-full w-6 h-6 my-auto" alt="" />
<span className={"pl-2 text-gray-600 dark:text-gray-100 text-base " + (addClasses || "")}>{label}</span>
</div>
);
Expand Down Expand Up @@ -311,9 +314,9 @@ export default function NewProject() {
<p className="text-gray-500 text-center text-base mt-12">
{loaded && noReposAvailable ? "Select account on " : "Select a Git repository on "}
<b>{selectedProviderHost}</b> (
<a className="gp-link cursor-pointer" onClick={() => setShowGitProviders(true)}>
<button className="gp-link cursor-pointer" onClick={() => setShowGitProviders(true)}>
change
</a>
</button>
)
</p>
<div className={`mt-2 flex-col ${noReposAvailable && isGitHub() ? "w-96" : ""}`}>
Expand All @@ -325,6 +328,7 @@ export default function NewProject() {
<img
src={user?.avatarUrl}
className="rounded-full w-6 h-6 absolute my-2.5 left-3"
alt=""
/>
<input
className="w-full px-12 cursor-pointer font-semibold"
Expand All @@ -339,6 +343,7 @@ export default function NewProject() {
<img
src={icon ? icon : ""}
className="rounded-full w-6 h-6 absolute my-2.5 left-3"
alt=""
/>
<input
className="w-full px-12 cursor-pointer font-semibold"
Expand All @@ -352,12 +357,18 @@ export default function NewProject() {
src={CaretDown}
title="Select Account"
className="filter-grayscale absolute top-1/2 right-3"
alt=""
/>
</div>
</ContextMenu>
{showSearchInput && (
<div className="w-full relative ">
<img src={search} title="Search" className="filter-grayscale absolute top-1/3 left-3" />
<img
src={search}
title="Search"
alt="Search"
className="filter-grayscale absolute top-1/3 left-3"
/>
<input
className="w-96 pl-10 border-0"
type="text"
Expand Down Expand Up @@ -430,13 +441,12 @@ export default function NewProject() {
<div>
<div className="text-gray-500 text-center w-96 mx-8">
Repository not found?{" "}
<a
href="javascript:void(0)"
<button
onClick={(e) => reconfigure()}
className="text-gray-400 underline underline-thickness-thin underline-offset-small hover:text-gray-600"
className="gp-link text-gray-400 underline underline-thickness-thin underline-offset-small hover:text-gray-600"
>
Reconfigure
</a>
</button>
</div>
</div>
)}
Expand Down Expand Up @@ -682,7 +692,7 @@ function GitProviders(props: {
{errorMessage && (
<div className="mt-16 flex space-x-2 py-6 px-6 w-96 justify-between bg-gitpod-kumquat-light rounded-xl">
<div className="pr-3 self-center w-6">
<img src={exclamation} />
<img src={exclamation} alt="Heads up!" />
</div>
<div className="flex-1 flex flex-col">
<p className="text-gitpod-red text-sm">{errorMessage}</p>
Expand Down
Loading