go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/ui/README.md (about) 1 # LUCI Test Single UI 2 LUCI Test Single UI is the web UI for all LUCI Test systems. 3 4 ## Local Development Workflows 5 ### Prerequisites 6 You need to run the following commands to setup the environment. 7 ```sh 8 # Activate the infra env (via the infra.git checkout): 9 cd /path/to/infra/checkout 10 eval infra/go/env.py 11 12 # Install the dependencies. 13 cd /path/to/this/directory 14 npm ci 15 ``` 16 17 ### Start a local AppEngine server 18 TODO: add instructions 19 20 For the simple case of testing an rpc using `/rpcexplorer`: 21 ``` 22 go run main.go -cloud-project luci-milo-dev -auth-service-host chrome-infra-auth-dev.appspot.com 23 ``` 24 25 ### Start a local UI server 26 To start a [Vite](https://vitejs.dev) local dev server, run 27 ``` 28 npm run dev 29 ``` 30 31 The local dev server only serves the SPA assets. It sends pRPC and HTTP REST 32 requests to staging servers (typically hosted on GCP). Check 33 [.env.development](.env.development) for instructions to configure the target 34 servers and other local development settings. 35 36 37 #### Login on a local UI server 38 Currently, there's no easy way to perform a login flow on a local UI server. To 39 test the page with a logged in session, you can 40 41 - deploy to a personal staging server with the following command, or 42 43 1. `npm run build && yes | gae.py upload -p ../ -A luci-milo-dev default` 44 45 - use an auth state obtained from staging or prod environment with the 46 following steps. 47 48 1. open `` `https://${singleUiHost}/ui/search` `` in a browser tab, then 49 2. perform a login flow, then 50 3. run `copy(JSON.stringify(__STORE.authState.value))` in the browser devtool 51 console to copy the auth state, then 52 4. paste the output to [auth_state.local.json](auth_state.local.json), then 53 5. visit any pages under http://localhost:8080/ui/. 54 55 note that the auth state obtained from a dev environment cannot be used to 56 query prod services and vice versa. 57 58 ### Deploy a demo to GAE dev instance 59 This deploys your local code to GAE as a dev instance that uses real auth 60 and can be accessed by other people. 61 62 ``` 63 cd ui 64 npm run build 65 gae.py upload -p ../ -A luci-milo-dev default 66 ``` 67 68 ### Add a new npm package 69 You can use [npm install](https://docs.npmjs.com/cli/v8/commands/npm-install) to 70 add a new npm package. 71 72 LUCI Test Single UI uses a private npm registry (defined in [.npmrc](.npmrc)). 73 By default, it rejects packages that are less than 7 days old (with an 74 HTTP 451 Unknown error). 75 76 You can avoid this issue by temporarily switching to the public npm registry 77 with the following steps. 78 79 1. comment out the registry setting in [.npmrc](.npmrc), then 80 2. install the package, then 81 3. replace the registry in all the URLs in the "resolved" field in 82 [package-lock.json](package-lock.json) with the private registry, then 83 4. uncomment the registry setting in [.npmrc](.npmrc), then 84 5. wait 7 days before submitting your CL. 85 86 ### Others 87 Check the [Makefile](Makefile), the [parent Makefile](../Makefile), and the 88 `"scripts"` section in [package.json](package.json) for more available commands. 89 90 ## Design Decisions 91 ### Service Workers 92 To improve the loading time, the service workers are added to 93 * redirect users from the old URLs to the new URLs (e.g. from 94 `` `/b/${build_id}` `` to `` `/ui/b/${build_id}` ``) without hitting the 95 server, and 96 * cache and serve static assets, including the entry file (index.html), and 97 * prefetch resources if the URL matches certain pattern. 98 99 It's very hard (if not impossible) to achieve the results above without service 100 workers because most page visits hit a dynamic (e.g. 101 `` `/p/${project}/...` ``), and possibly constantly changing (e.g. 102 `` `/b/${build_id}` ``) URL path, which means cache layers that rely on HTTP 103 cache headers will not be very effective. 104 105 ### [Src Directory](./src) Structure 106 #### Goals 107 The directory structure is designed to achieve the following goals: 108 * Fit to host all LUCI Single UI projects. 109 * Discovering/locating existing modules should be straightforward. 110 * Deciding where a new module should be placed should not be confusing. 111 * Managing (first party) modules dependencies should be straightforward, which 112 requires 113 * making the dependency relationship between modules more obvious, and 114 * making it harder to accidentally introducing circular dependencies. 115 * Support different levels of encapsulation, which includes the abilities to 116 * limit the surface of a module, and 117 * enforce module-level invariants. 118 119 #### Rules 120 To achieve those goals, the [./src](./src) directory are generally structured 121 with the following rules: 122 * Modules are grouped into packages. Packages are all located at the top level 123 directory in the ./src directory. It can be one of the followings: 124 * A business domain package (e.g. [@/build](./src/build), 125 [@/bisection](./src/bisection)). 126 * The contained modules are specific to the business domain. 127 * The business domain typically matches a top-level navigation/feature 128 area. 129 * A domain package may import from another domain package 130 * This is supported so natural dependencies between domains can be 131 expressed (e.g. builds depends on test results). 132 * The usage should still be limited. Consider lifting the shared module 133 to the @/common package. 134 * Circular dependencies between domain packages must be avoided. 135 * Grouping modules by business domains makes enforcing encapsulation/ 136 isolation easier. 137 * Placing modules under a package named after their domain helps 138 discovering/locating modules as the project grows larger. 139 * The [@/common](./src/common) package. 140 * The contained modules can have business/application logic. 141 * The contained modules must not make assumptions on the business domains 142 it's used in. 143 * The contained modules should be reusable across business domains. 144 * Must not import from domain packages. 145 * This helps capturing modules that cross domains. 146 * The name gives a clear signal that the modules should stay reusable. 147 * The [@/generic_libs](./src/generic_libs) package. 148 * The contained modules must not contain any business logic. 149 * The contained modules should be highly generic and reusable (akin to a 150 published module). 151 * The contained modules (excluding unit tests) must not depend on any first 152 party module outside of [@/generic_libs](./src/generic_libs). 153 * Comparing to @/common, [@/generic_libs](./src/generic_libs) must not 154 contain business/application logic. 155 * The name gives a clear signal that the modules should stay generic. 156 * Separating from @/common makes it harder to accidentally 157 add business logic to a generic module. 158 * The [@/core](./src/core) package. 159 * The contained modules are for core functionality such as login or 160 the landing page. 161 * The [@/testing_tools](./src/testing_tools) package. 162 * Contains utilities for writing unit tests. 163 * Can import from other packages or be imported to other packages. 164 * Must only be used in unit tests. 165 * Other directories may have their own `./testing_tools` subdirectory to 166 contain testing tools specific to those domains. 167 * It helps separating test utilities from production code. 168 * Modules are usually further divided into groups by their functional category 169 (e.g. `./components/*`, `./hooks/*`, `./tools/*`). 170 * The purpose is to make locating/discovering existing modules easier as the 171 module list grows larger. 172 * The divide is purely aesthetical. 173 * There's no logical boundary between those groups and the division does 174 not signal or enforce encapsulation. 175 * It's perfectly fine to have "circular imports" between groups since they 176 are merely aesthetical groups. 177 * Circular dependencies between the actual underlying modules should still 178 be avoided. 179 * This rule is enforced loosely. 180 * Modules belong to multiple functional categories can simply pick a group 181 with the best fit (e.g. a module that exports React components, hooks and 182 utility functions may simply be placed under `./components/`). 183 * Modules that don't fit any of the categories may be placed directly in 184 the parent directory or in a catch all group (e.g. `./tools/`). This 185 should be used sparingly. 186 * Modules may declare entry files (e.g. `index.ts`) that reexport symbols. 187 Symbols reexported by the entry file are considered the public surface of the 188 module, while other symbols are considered internal to the module (i.e. no 189 deep imports when there's an entry file). 190 * This helps reducing the public surface of a module. Makes it easier to 191 implement encapsulation and enforce invariants. 192 * Modules can themselves have different internal structures to implement 193 different layers of encapsulation. 194 195 Note: At the moment (2023-09-14), some packages are in an inconsistent state. 196 Some modules should be moved to other packages. Notable items include but not 197 limited to 198 * Some modules in @/common should be moved to business domain packages. 199 * [@/bisection](./src/bisection) and other recently merged in projects should 200 have common modules lifted to @/common. 201 202 #### Graph illustration of the package relationships: 203 ```ascii 204 @/core ─┬─> @/build ─┬─> @/common ─┬─> @/generic_libs 205 ├─■ ./pages │ ├─■ ./pages │ ├─■ ./components │ ├─■ ./components 206 ├─■ ...other groups... │ ├─■ ./components │ ├─■ ./hooks │ ├─■ ./hooks 207 ├─■ ...entry files... │ ├─■ ./hooks │ ├─■ ./tools │ ├─■ ./tools 208 └─■ ... │ ├─■ ./tools │ └─■ ... │ └─■ ... 209 │ └─■ ... │ │ 210 │ │ ├─> ...third party libs... 211 ├─> @/bisection ─┤ │ 212 │ ├─■ ./pages │ │ 213 │ ├─■ ./components │ │ 214 │ ├─■ ./hooks │ │ 215 │ ├─■ ./tools │ │ 216 │ └─■ ... │ │ 217 │ │ │ 218 ├─> @/analysis ─┤ │ 219 │ └─■ ... │ │ 220 │ │ │ 221 ├─> @/...other business domains... ─┤ │ 222 │ └─■ ... │ │ 223 │ │ │ 224 └─> ────────────────────────────────┴─> ────────────────────┘ 225 226 A ─> B: A depends on B. 227 A ─■ B: A contains B. 228 ```