Skip to content
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

Include the project role of a request token or user in project API #989

Merged
merged 11 commits into from
Aug 8, 2024

Conversation

ikhoon
Copy link
Contributor

@ikhoon ikhoon commented Jul 20, 2024

Motivation:

I think it is necessary to filter projects to which users belong or display their roles on the screen.

Currently, user information can't be obtained from /api/v1/projects API. So we need to call /api/v1/projects/${projectName} to get the metadata. That means 20 API calls should be made to list 20 projects in /app/projects. It will cause additional loads to servers and increase page loading time.

To resolve the N+1 query problem, I propose to add the role of a requested user to ProjectDto.

Modifications:

  • Add RepositoryListener abstraction that can be continuously updated when a new change is pushed to a Repository. Repository.watch() has a similar function but it can be updated only once.
  • Add ProjectMetadata to DefaultProject and use RepositoryListener to update ProjectMetadata in a project when new changes are made in metadata.json
  • Expose user roles on the Projects page of the new web application.
  • Refactor ControlPlanePlugin with RepositoryListener to simply the code.
  • Miscellaneous) Show a loading indicator when a page is loading.

Result:

  • The project role of a request token or user is now exposed in the project API.
  • Users can see their roles on the projects page.
  • Users can filter projects by their roles on the project page.

Motivation:

I think it is necessary to filter projecs to which users belong or
display their roles in the screen.

Currently, a user information couldn't be obtained from
`/api/v1/projects` API. So we need to call `/api/v1/projects/${projectName}`
to get the metadata. That means 20 API calls should be made to list 20
projects in `/app/projects`. It will cause additional loads to servers
and increase page loading time.

To resolve the N+1 query problem, I propose to add a role of the
requested user to `ProjectDto`.

Modifications:

- Add `RepositoryListener` abstraction that can be continuously updated
  when a new change is pushed to a `Repository`. `Repository.watch()`
  has a similar function but it can be updated only once.
- Add `ProjectMetadata` to `DefaultProject` and use `RepositoryListener`
  to update `ProjectMetadata` in a project when new changes in
  `metadata.json`
- Expose user roles in the Projects page of the new web application.
- Refactor `ControlPlanePlugin` with `RepositoryListener` to simply the
  code

Result:

- The project role of a request token or user are now exposed in the
  project API.
- Users can see their roles in the projects page.
- Users can filter projects by their roles in the project page.
@ikhoon ikhoon added this to the 0.67.1 milestone Jul 20, 2024
@ikhoon
Copy link
Contributor Author

ikhoon commented Jul 20, 2024

image image

@minwoox minwoox modified the milestones: 0.67.1, 0.67.2 Jul 23, 2024
@ikhoon ikhoon modified the milestones: 0.67.2, 0.68.0 Jul 23, 2024
@minwoox minwoox modified the milestones: 0.68.0, 0.69.0 Jul 23, 2024
Copy link
Contributor

@minwoox minwoox left a comment

Choose a reason for hiding this comment

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

Looks promising! 👍

webapp/src/dogma/features/project/Projects.tsx Outdated Show resolved Hide resolved
<MenuItemOption value="me">Created by me</MenuItemOption>
<MenuItemOption value="ALL">All</MenuItemOption>
<MenuItemOption value="MEMBER">I am a member</MenuItemOption>
<MenuItemOption value="CREATOR">Created by me</MenuItemOption>
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you have any easy way to test the UI in local?

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 prefer the following steps:

  • Run ShiroCentralDogmaTestServer.main() using IntelliJ which is faster than ./gradlew runTestShiroServer.
  • If the server starts successfully, execute npm run develop and access http://127.0.0.1:3000.

If test is meant to run a unit test, we can run npm run test. I temporarily disable tests that fetch data from a mock server after upgrading dependencies. Because there were compatibility issues that I haven't solved yet.

Copy link
Contributor

Choose a reason for hiding this comment

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

Run ShiroCentralDogmaTestServer.main() using IntelliJ which is faster than ./gradlew runTestShiroServer.
If the server starts successfully, execute npm run develop and access http://127.0.0.1:3000.

That's what I was looking for. 😉

watchDogmaRepository(repositoryManager, entry.revision());
return null;
}, CONTROL_PLANE_EXECUTOR);
dogmaRepository.addListener(RepositoryListener.of(MetadataService.METADATA_JSON, entries -> {
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

Copy link
Contributor

@jrhee17 jrhee17 left a comment

Choose a reason for hiding this comment

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

Left some minor comments but looks good to me 👍 👍 👍

import com.linecorp.centraldogma.common.Entry;
import com.linecorp.centraldogma.common.Query;

import io.netty.util.internal.shaded.org.jctools.queues.MessagePassingQueue.Consumer;
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess you meant to use jdk's consumer

Copy link
Contributor Author

Choose a reason for hiding this comment

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

😱

Comment on lines +62 to +65
@JsonProperty("creator") @Nullable Author creator,
@JsonProperty("url") @Nullable String url,
@JsonProperty("userRole") @Nullable ProjectRole userRole,
@JsonProperty("createdAt") @Nullable String createdAt) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Question) Even in tests, is there a need for the nullable contract since server-side always seems to fill in these fields?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This constructor was added for deserializing the API responses in tests.

.asJson(new TypeReference<List<ProjectDto>>() {})
.execute();

It wasn't used for ?status=removed responses but made them nullable for future usage.

Copy link
Contributor

@jrhee17 jrhee17 Jul 30, 2024

Choose a reason for hiding this comment

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

This constructor was added for deserializing the API responses in tests.

I understand that. I meant even in the test you linked aren't the fields always being filled in (non-null) anyways?

If it is indeed nullable, I guess it's fine to leave as-is

Copy link
Contributor Author

Choose a reason for hiding this comment

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

you linked aren't the fields always being filled in (non-null) anyways?

Right. They are non-null for normal projects. The values are nullable for removed projects.

return CompletableFuture.supplyAsync(() -> projectApiManager.listRemovedProjects().keySet()
.stream()
.map(ProjectDto::new)

@@ -74,9 +96,9 @@ public String url() {
}

@Nullable
@JsonProperty("reposUrl")
Copy link
Contributor

Choose a reason for hiding this comment

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

note: checked that this field doesn't seem to be used in the previous UI also, which guarantees backwards compatibility

@@ -103,7 +104,7 @@ public CompletableFuture<Collection<Token>> listTokens(User loginUser) {
@StatusCode(201)
@ResponseConverter(CreateApiResponseConverter.class)
public CompletableFuture<HttpResult<Token>> createToken(@Param String appId,
@Param boolean isAdmin,
@Param @Default("false") boolean isAdmin,
Copy link
Contributor

Choose a reason for hiding this comment

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

Question) Is this related to changes in this PR? Or is this a separate improvement?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is not related to this PR.
I've fixed it in this PR together since it looked like a trivial change.

return ProjectRole.OWNER;
}

final ProjectMetadata metadata = project.metadata();
Copy link
Contributor

Choose a reason for hiding this comment

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

Question) I believe Project#metadata is updated asynchronously. This means if a user changes his/her metadata and directly navigates to /projects, I think it is possible that the change may not be reflected depending on whether the find in the listener has been invoked or not.

I think it should be fine since listing "my projects" can be considered non-critical, but just wanted to make sure if this is an intentional design choice as opposed to just using a write-through cache. (I haven't checked if there are any UI paths which update metadata and navigate to /projects immediately)

Copy link
Contributor Author

@ikhoon ikhoon Jul 27, 2024

Choose a reason for hiding this comment

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

Good point. As you know, the consistent model of Central Dogma is eventual consistency and does not provide a transaction mechanism.

Even before this change, users may see old content right after a commit is pushed successfully. ReplicationLagTolerantCentralDogma is a workaround to make up for the downside of eventual consistency.

Copy link
Contributor

Choose a reason for hiding this comment

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

I see, I understood that even if a user creates a project and directly navigates to the main page he/she may not see her project highlighted.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

When I tested, I was able to see newly created projects in most cases. But it may happen if there is a significant delay. Let's leave it a known issue for now.

Copy link
Contributor

@minwoox minwoox left a comment

Choose a reason for hiding this comment

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

Thanks! 👍 👍 👍

this.name = requireNonNull(name, "name");
this.creator = requireNonNull(creator, "creator");
createdAt = ISO_INSTANT.format(Instant.ofEpochMilli(creationTimeMillis));
url = PROJECTS_PREFIX + '/' + name;
this.userRole = userRole;
Copy link
Contributor

Choose a reason for hiding this comment

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

rnn?

Change.ofJsonUpsert(METADATA_JSON, Jackson.valueToTree(metadata)))
.join();
lastMetadataRevision = result.revision();
this.projectMetadata = metadata;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
this.projectMetadata = metadata;
projectMetadata = metadata;

} else {
creationTimeMillis = repos.get(REPO_DOGMA).creationTimeMillis();
author = repos.get(REPO_DOGMA).author();
}
success = true;
} catch (Exception e) {
throw new CentralDogmaException("failed to initialize internal repositories", e);
Copy link
Contributor

Choose a reason for hiding this comment

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

nit:

Suggested change
throw new CentralDogmaException("failed to initialize internal repositories", e);
throw new CentralDogmaException("failed to initialize internal repositories of " + name, e);

Please also add the project name to the exception message below.

}

final Revision lastRevision = entry.revision();
if (lastRevision.compareTo(lastMetadataRevision) <= 0) {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: assert lastMetadataRevision != null;
We might want to assign it to a local variable not to access the field twice.

@minwoox minwoox merged commit c615ba5 into line:main Aug 8, 2024
10 checks passed
@ikhoon ikhoon deleted the project-user-role branch August 30, 2024 09:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants