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

Add Mirroring and Credential UI #838

Open
wants to merge 3 commits into
base: new-webapp
Choose a base branch
from
Open

Conversation

ikhoon
Copy link
Contributor

@ikhoon ikhoon commented Apr 20, 2023

Motivation:

Creating and editing mirror settings require manual JSON editing which is time-consuming and error-prone. If a dedicated UI validates inputs and suggests default values such as mirrorable repositories and credentials, users will easily configure a mirror setting without making a mistake.

Related: #50

Modifications:

  • Server:
    • Added dedicated REST APIs for mirror configurations and credential information. - Creating, listing, and updating operaitons are supported. - An array index is used as a key to update the existing configuration. This is an inevitable choice since the existing settings stored in an array and JSON patch can update an element based on index.
    • Fixed AddOperation to create an JSON file if not exists.
    • Added enabled property to MirrorCredential so that users can disable unused credentials.
    • Added id property to Mirror so that users distinguish a mirror from others.
    • Removed support for multiple mirror that is currenly not used. Should we deprecate MultipleMirrorConfig? #647
    • Annotated @JsonProperties to the getters of MirrorCredential and its subclasses to serialize MirrorCredential to JSON as a REST API response.
  • Webapp:
    • The following web pages were newly added for mirroring and credentials.
    • Mirroring:
      • Create: /app/projects/{projectName}/mirrors/new
      • Update: /app/projects/{projectName}/mirrors/{index}/edit
      • List: /app/projects/{projectName}/mirrors
      • View: /app/projects/{projectName}/mirrors/{index}
    • Credential:
      • Create: /app/projects/{projectName}/credentials/new
      • Update: /app/projects/{projectName}/credentials/{index}/edit
      • List: /app/projects/{projectName}/credentials
      • View: /app/projects/{projectName}/credentials/{index}
    • Switched query-based tab URL for metadata to path-based to easily create breadcrumbs based on the path.
      • The old paths: /app/projects/metadata/{projectName}?tab=repositories|permissions}members|tokens|mirror
      • The new paths:
        • /app/projects/{projectName}/metadata
        • /app/projects/{projectName}/permissions - /app/projects/{projectName}/permissions/repos/{repoName} - /app/projects/{projectName}/members - /app/projects/{projectName}/tokens - /app/projects/{projectName}/mirrors - /app/projects/{projectName}/credentials
    • Updated Breadcrumbs to replace a specific path segment name with another to display more meaningful information for breadcrumbs rather than showing an index number.

Result:

Motivation:

Creating and editing mirror settings require manual JSON editing which
is time-consuming and error-prone. If a dedicated UI validates inputs
and suggests default values such as mirrorable repositories and
credentials, users will easily configure a mirror setting without making
a mistake.

Related: line#50

Modifications:

- Server:
  - Added dedicated REST APIs for mirror configurations and credential
    information.
    - Creating, listing, and updating operaitons are supported.
    - An array index is used as a key to update the existing
      configuration. This is an inevitable choice since the existing
      settings stored in an array and JSON patch can update an element
      based on index.
  - Fixed `AddOperation` to create an JSON file if not exists.
  - Added `enabled` property to `MirrorCredential` so that users can
    disable unused credentials.
  - Added `id` property to `Mirror` so that users distinguish a mirror
    from others.
  - Removed support for multiple mirror that is currenly not used. line#647
  - Annotated `@JsonProperties` to the getters of `MirrorCredential` and
    its subclasses to serialize `MirrorCredential` to JSON as a REST API
    response.
- Webapp:
  - The following web pages were newly added for mirroring and credential.
    - Mirroring:
      - Create: `/app/projects/{projectName}/mirrors/new`
      - Update: `/app/projects/{projectName}/mirrors/{index}/edit`
      - List: `/app/projects/{projectName}/mirrors`
      - View: `/app/projects/{projectName}/mirrors/{index}`
    - Credential:
      - Create: `/app/projects/{projectName}/credentials/new`
      - Update: `/app/projects/{projectName}/credentials/{index}/edit`
      - List: `/app/projects/{projectName}/credentials`
      - View: `/app/projects/{projectName}/credentials/{index}`
  - Switched query-based tab URL for metadata to path-based to easily
    create breadcrumbs based on the path.
    - The old paths: `/app/projects/metadata/{projectName}?tab=repositories|permissions}members|tokens|mirror`
    - The new paths:
      - `/app/projects/{projectName}/metadata`
      - `/app/projects/{projectName}/permissions`
      - `/app/projects/{projectName}/permissions/repos/{repoName}`
      - `/app/projects/{projectName}/members`
      - `/app/projects/{projectName}/tokens`
      - `/app/projects/{projectName}/mirrors`
      - `/app/projects/{projectName}/credentials`
  - Updated `Breadcrumbs` to replace a specific path segment name with
    another to display more meaning information to breadcrumbs rather
    than showing an index number.

Result:

- You can now use a web UI to set up a Git-to-CD mirror.
- Closes line#50
@ikhoon ikhoon added this to the revamp-webapp milestone Apr 20, 2023
@ikhoon
Copy link
Contributor Author

ikhoon commented Apr 20, 2023

Screenshots for the new UI

  • Credential form
    credential-form
  • Credential list
    credential-list
  • Credential view
    credential-view
  • Mirror form
    mirror-form
  • Mirror list
    mirror-list
  • Mirror view
    mirror-view

@Type(value = SingleMirrorConfig.class, name = "single"),
@Type(value = MultipleMirrorConfig.class, name = "multiple")
})
private abstract static class MirrorConfig {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

MultipleMirrorConfig is removed and SingleMirrorConfig is migrated to MirrorConfig.java

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd feel better if we migrate the internally used MultipleMirrorConfig before merging this PR (or at least before the next release so that the next release driver isn't suprised)

// Next.js uses a path pattern to dynamically match the request path to a specific HTML file.
// For example, `/app/projects/[projectNames]/index.html' is generated to render `/app/projects/myProj`
// page. `FileService` serving the static resources for Central Dogma webapp does not understand the
// path pattern syntax in the folder names. If `/app/projects/myProj` is requested, the `FileService` will
// try to find '/app/projects/myProj/index.html' as is and fails, which in turn 'index.html' is returned as
// a fallback. As a workaround, this triggers Next.js router to route to the desired page when a page is
// loaded for the first time.
if (router.asPath !== '/') {
if (router.asPath !== '/' && router.isReady) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

If router.isReady is false, router.asPath could include a path pattern such as [projectName]. If router.isReady becomes true, the path pattern is replaced with the actual value like myProj.

if (path.toString().isEmpty()) {
return valueCopy();
}
if (node.isNull() && Strings.isNullOrEmpty(path.head().getMatchingProperty()) &&
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 there no need to add such logic to other operations such as MoveOperation, CopyOperation for the sake of consistency?

Copy link
Contributor Author

@ikhoon ikhoon Apr 28, 2023

Choose a reason for hiding this comment

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

AddOperation could be performed on an empty node to add a new value.
However, MoveOperation and CopyOperation need an old value to perform. So I think it is abnormal behavior to run copy or move operation on an empty value.

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, makes sense 👍

* Returns whether this {@link MirrorCredential} is enabled.
*/
@JsonProperty("enabled")
boolean enabled();
Copy link
Contributor

Choose a reason for hiding this comment

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

Double checked that enabled is by default true if the value is null for both MirrorConfig, MirrorCredential

Comment on lines +70 to +71
// Web browsers may send an empty array `[]` as a value of hostnamePatterns
// which is converted into `[null]`.
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 it possible for browsers to clean the input before sending such parameters? This feels unintuitive since the Web UI isn't the only code-path which uses this logic.

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 possible. I can inject a custom logic to covert an empty array to null for `hostnamePatterns.

remoteRepoUri.getScheme(),
remoteRepoUri.getHost() + remoteRepoUri.getPath(),
mirror.remotePath(),
firstNonNull(mirror.remoteBranch(), "master"),
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure why we need to fallback to master. If I remember correctly, @trustin added a capability to detect the default branch by default if this value is null, and master is no longer the de-facto if due to main.

Is this necessary for the UI?

Copy link
Member

Choose a reason for hiding this comment

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

I guess we can leave it as null if a user specified null (= use default branch).

* - remoteBranch: master
*
* <p>e.g. dogma://foo.com/bar/qux.dogma is split into:
* - remoteRepoUri: dogma://foo.com/bar/qux.dogma
* - remotePath: / (default)
* - remoteBranch: {@code defaultBranch}
*/
static String[] split(URI remoteUri, String suffix) {
public static String[] splitRemoteUri(URI remoteUri, String suffix, @Nullable String defaultBranch) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Like mentioned in another comment, I'm unsure why this change is necessary 😅 Would you mind explaining/updating in the PR description?

Suggested change
public static String[] splitRemoteUri(URI remoteUri, String suffix, @Nullable String defaultBranch) {
public static String[] splitRemoteUri(URI remoteUri, String suffix) {

@ikhoon
Copy link
Contributor Author

ikhoon commented Apr 28, 2023

Please don't review the client part. The change is too huge that is hard to review.
It would be okay to q/a the features in a browser at the moment.

I will send a request to the main branch away. You can check the front-end code on the final PR if you want.

// TODO(ikhoon): Automatically inject the non-optional query parameters to Route.matchesParam().
@MatchesParam("id")
@Get("/projects/{projectName}/credentials")
public CompletableFuture<MirrorCredential> getCredentialById(@Param String projectName, @Param String id) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Question) Are credentialIds unique? I wasn't able to find any checks for duplicates, and I think users can still manually modify the credentials.json file anyways

I wasn't able to check the UI, but I'm wondering what happens if

  1. a credential doesn't have an id
  2. a credential has a duplicate id

if (c == null) {
throw new RepositoryMetadataException(PATH_MIRRORS + " contains null.");
}
final Mirror mirror = c.toMirror(parent(), credentials, index++);
Copy link
Contributor

Choose a reason for hiding this comment

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

Question) What do you think of just persistently assigning an id? I understand that previously we didn't have mirrors, but I think it's possible to migrate to an object based schema instead of a mirror based schema and enforce ids and create an arbitrary id if missing.

With the current setup, I fear that users might make a mistake if multiple users modify mirror for the same project. i.e. Two users may try to modify a mirror configuration concurrently, and the index mapping might change.

@Type(value = SingleMirrorConfig.class, name = "single"),
@Type(value = MultipleMirrorConfig.class, name = "multiple")
})
private abstract static class MirrorConfig {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd feel better if we migrate the internally used MultipleMirrorConfig before merging this PR (or at least before the next release so that the next release driver isn't suprised)

@ikhoon
Copy link
Contributor Author

ikhoon commented Apr 28, 2023

Minwoo also worked for MultipleMirrorConfig removal at
#836 (comment)
#836 may be released before this. Otherwise, I will handle it separately.

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.

Maybe I couldn't find it, but do we also provide a way to delete a mirror/credential? Or is enabled the equivalent of this?

final Map<String, Entry<?>> entries =
find(new Revision(rev), PATH_CREDENTIALS_AND_MIRRORS, Collections.emptyMap()).join();
private CompletableFuture<Map.Entry<List<Mirror>, List<MirrorCredential>>> mirrorsAndCredentials(int rev) {
return find(new Revision(rev), PATH_CREDENTIALS_AND_MIRRORS, Collections.emptyMap()).thenApply(
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
return find(new Revision(rev), PATH_CREDENTIALS_AND_MIRRORS, Collections.emptyMap()).thenApply(
return find(new Revision(rev, PATH_CREDENTIALS_AND_MIRRORS).thenApply(

@ikhoon
Copy link
Contributor Author

ikhoon commented Apr 28, 2023

I don't think we can provide an API for removal because of the limitation of JSON patch that can update an array with its index. An index for an item should be consistent to provide a deep link for a mirror view and edit. Additionally, the size of Git mirrors and credentials isn't growing up to 100 or 1000. The enabled flag would be enough to manage those scales of data.

As a following work, we will hide mirrors.json and credentials.json from the file navigator of web UI and REST API retrieval so that users don't modify mirrors.json and credentials.json manually.

If removal for the mirror and credential information is really required, we can choose one of the following:

  • Migrate the array data format to object key-value format.
  • Add a custom JSON patch operation to update an array by a key field.

As you know, key / ID fields for mirrors and credentials is optional. A migration strategy is necessary to fill nullable IDs to implement the ideas.

@trustin
Copy link
Member

trustin commented Apr 28, 2023

As you know, key / ID fields for mirrors and credentials is optional. A migration strategy is necessary to fill nullable IDs to implement the ideas.

Agreed. Maybe we could auto-generate IDs and commit the changes automatically when it's read first time?

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.

I see, I guess I didn't read the PR description carefully and didn't catch the intention 😅
I think there should be no problem with the current approach if the mirrors.json, credentials.json is not editable directly by end-users as a short-term solution 👍

I'm unsure how this will be rolled out with respect to the UI migration though (at the same time? vs. after UI migration is complete?)

throw new EntryNotFoundException(
"No such mirror at the index " + index + " in " + projectName);
}
return convertToMirrorDto(projectName, mirrors.get(index));
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems like we're just using the index and not actually comparing mirrors.index with index from the parameters.
What do you think of just removing the index from the DTO altogether for simplicity if we know it's not going to change?

I didn't think there is any sorting from any point (since this is an array of json objects, there probably isn't a natural sorting order anyways)

Copy link
Member

@trustin trustin left a comment

Choose a reason for hiding this comment

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

What do you think about changing the directory layout of mirrors and credentials? e.g.

- mirrors
  - <mirror-id>.json
  - ...
- credentials
  - <credentials-id>.json
  - ...

At startup, we could:

  1. perform automatic migration from mirrors.json and credentials.json to the new layout; and
  2. rename mirrors.json and credentials.json to mirrors.json.bak and credentials.json.bak (so that the migration doesn't repeat.)

Then, all mirrors and credentials will have their unique IDs and we don't need to use integral index which is confusing.

I think this can be done as a follow-up task, though. (or we can do the migration first?) What do you think?

Comment on lines +82 to 84
registerModules(new Jdk8Module(),
new SimpleModule().addSerializer(Instant.class, InstantSerializer.INSTANCE)
.addDeserializer(Instant.class, InstantDeserializer.INSTANT));
Copy link
Member

Choose a reason for hiding this comment

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

Not related to this PR, but should we do this in Armeria's DefaultJacksonObjectMapperProvider?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ProducesJson
@RequiresRole(roles = ProjectRole.OWNER)
@ExceptionHandler(HttpApiExceptionHandler.class)
public class CredentialServiceV1 extends AbstractService {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
public class CredentialServiceV1 extends AbstractService {
public final class CredentialServiceV1 extends AbstractService {

remoteRepoUri.getScheme(),
remoteRepoUri.getHost() + remoteRepoUri.getPath(),
mirror.remotePath(),
firstNonNull(mirror.remoteBranch(), "master"),
Copy link
Member

Choose a reason for hiding this comment

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

I guess we can leave it as null if a user specified null (= use default branch).

return metadataRepo.push(projectName, Project.REPO_META, author, summary, change);
}

private static MirrorConfig converterToMirrorConfig(MirrorDto mirrorDto) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
private static MirrorConfig converterToMirrorConfig(MirrorDto mirrorDto) {
private static MirrorConfig convertToMirrorConfig(MirrorDto mirrorDto) {

private static MirrorConfig converterToMirrorConfig(MirrorDto mirrorDto) {
final String remoteUri =
mirrorDto.remoteScheme() + "://" + mirrorDto.remoteUrl() +
MirrorUtil.normalizePath(mirrorDto.remotePath()) + '#' + mirrorDto.remoteBranch();
Copy link
Member

Choose a reason for hiding this comment

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

#branch should be omitted if remoteBranch is null

@ikhoon
Copy link
Contributor Author

ikhoon commented May 3, 2023

What do you think about changing the directory layout of mirrors and credentials? e.g.

The design looks good. One thing to consider is mirror ID or credential ID are optional fields. To adopt this idea, some random IDs should be used to fill the null IDs

I will work on the migration first before merging this PR. The change of this is already huge. It would be easier to review if it is handled separately in advance.

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.

None yet

3 participants