From e620598b3e48628e6b0fffd7fc57e63907ae9408 Mon Sep 17 00:00:00 2001 From: Presley Date: Tue, 15 Mar 2022 01:51:04 +0000 Subject: [PATCH 01/42] Install and configure XState --- .gitignore | 1 + site/app.tsx | 3 +++ site/package.json | 10 ++++++++-- site/yarn.lock | 46 +++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 57 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 7bf2c49778065..dba8147adbae6 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ site/storybook-static/ site/test-results/ site/yarn-error.log coverage/ +site/**/*.typegen # Build dist/ diff --git a/site/app.tsx b/site/app.tsx index 51e0872b09eb5..21e9c70cf7b33 100644 --- a/site/app.tsx +++ b/site/app.tsx @@ -14,6 +14,9 @@ import { ProjectsPage } from "./pages/projects" import { ProjectPage } from "./pages/projects/[organization]/[project]" import { CreateWorkspacePage } from "./pages/projects/[organization]/[project]/create" import { WorkspacePage } from "./pages/workspaces/[workspace]" +import { Interpreter } from "xstate/lib/interpreter" + +Interpreter.defaultOptions.devTools = process.env.NODE_ENV === "development" export const App: React.FC = () => { return ( diff --git a/site/package.json b/site/package.json index 8fe9fb48ec262..dfe959cc20412 100644 --- a/site/package.json +++ b/site/package.json @@ -17,7 +17,8 @@ "storybook": "start-storybook -p 6006 -s ./static", "storybook:build": "build-storybook", "test": "jest --selectProjects test", - "test:coverage": "jest --selectProjects test --collectCoverage" + "test:coverage": "jest --selectProjects test --collectCoverage", + "typegen": "xstate typegen '**/*.ts'" }, "devDependencies": { "@material-ui/core": "4.9.4", @@ -79,5 +80,10 @@ "firefox 63", "edge 79", "safari 13.1" - ] + ], + "dependencies": { + "@xstate/cli": "^0.1.4", + "@xstate/react": "^2.0.1", + "xstate": "^4.30.6" + } } diff --git a/site/yarn.lock b/site/yarn.lock index f9c1eaa3febb4..2b2b37da538b5 100644 --- a/site/yarn.lock +++ b/site/yarn.lock @@ -3605,6 +3605,38 @@ resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.6.1.tgz#0de2875ac31b46b6c5bb1ae0a7d7f0ba5678dffe" integrity sha512-gNGTiTrjEVQ0OcVnzsRSqTxaBSr+dmTfm+qJsCDluky8uhdLWep7Gcr62QsAKHTMxjCS/8nEITsmFAhfIx+QSw== +"@xstate/cli@^0.1.4": + version "0.1.4" + resolved "https://registry.yarnpkg.com/@xstate/cli/-/cli-0.1.4.tgz#5d909b980a5e62744f90b2790be3aa2717cfb4f8" + integrity sha512-+MkeFGi+gouY8o+/GWG7/62c4WZAtsMlABi5NFBN2owIbwohJQbflc9jBMzN+U3Ho0QGC2gHBwsretsSsNkuuA== + dependencies: + "@babel/core" "^7.12.10" + "@xstate/machine-extractor" "0.6.2" + "@xstate/tools-shared" "1.1.2" + chokidar "^3.5.3" + commander "^8.0.0" + xstate "^4.29.0" + +"@xstate/machine-extractor@0.6.2": + version "0.6.2" + resolved "https://registry.yarnpkg.com/@xstate/machine-extractor/-/machine-extractor-0.6.2.tgz#2fe5edb6b965fd1f45fa68644a4ef69f125c4fc0" + integrity sha512-zyDrBMDCpPestEpnWHwmJ42qLIVOqRVUKa491kmdix/vT8z/3P3Ib6MOSbD8lp2yaF49kIUDUCCkvQA6TcLRyA== + +"@xstate/react@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@xstate/react/-/react-2.0.1.tgz#2b4717369d419e78a6c67f2dfcd1a3be9abce2d9" + integrity sha512-sT3hxyzNBw+bm7uT3BP+uXzN0MnRqiaj/U9Yl4OYaMAUJXWsRvSA/ipL7EDf0gVLRGrRhJTCsC0cjWaduAAqnw== + dependencies: + use-isomorphic-layout-effect "^1.0.0" + use-subscription "^1.3.0" + +"@xstate/tools-shared@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@xstate/tools-shared/-/tools-shared-1.1.2.tgz#84660e1ff9ba48612af2d471a21905705fa62e16" + integrity sha512-/A0/3vI2N9Rr3uWKGpUkIv1GhVvQFsXB+vbf8RuSCcj5lnztnC2XEKCqzkric+T3BBViGVXuY7eh8UZw8OVPDw== + dependencies: + "@xstate/machine-extractor" "0.6.2" + "@xtuc/ieee754@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" @@ -5051,7 +5083,7 @@ comma-separated-tokens@^1.0.0: resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea" integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw== -commander@8.3.0, commander@^8.3.0: +commander@8.3.0, commander@^8.0.0, commander@^8.3.0: version "8.3.0" resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== @@ -12903,6 +12935,13 @@ use-latest@^1.0.0: dependencies: use-isomorphic-layout-effect "^1.0.0" +use-subscription@^1.3.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/use-subscription/-/use-subscription-1.5.1.tgz#73501107f02fad84c6dd57965beb0b75c68c42d1" + integrity sha512-Xv2a1P/yReAjAbhylMfFplFKj9GssgTwN7RlcTxBujFQcloStWNDQdc4g4NRWH9xS4i/FDk04vQBptAXoF3VcA== + dependencies: + object-assign "^4.1.1" + use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" @@ -13494,6 +13533,11 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== +xstate@^4.29.0, xstate@^4.30.6: + version "4.30.6" + resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.30.6.tgz#62b6dea37a500e0e1c0ff7c553a801eea5119554" + integrity sha512-V7liK1cjkZRh6R/MSneG8S5VLGRatpOUcnNieiYJX4LbwKi9eUVUH5V04ugJYVcJ+2oKDKvEFvzk0VnSC7lTag== + xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" From c3a143b87a7030a73a51c7d814371a592ae3c545 Mon Sep 17 00:00:00 2001 From: Presley Date: Tue, 15 Mar 2022 02:35:18 +0000 Subject: [PATCH 02/42] userXService - typegen not working yet --- site/api.ts | 20 ++++- site/xServices/user/userXService.ts | 120 ++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 site/xServices/user/userXService.ts diff --git a/site/api.ts b/site/api.ts index 179bc1ce9abf9..5c9fa58761f28 100644 --- a/site/api.ts +++ b/site/api.ts @@ -1,9 +1,16 @@ import { mutate } from "swr" -interface LoginResponse { +export interface LoginResponse { session_token: string } +export interface UserResponse { + readonly id: string + readonly username: string + readonly email: string + readonly created_at: string +} + /** * `Organization` must be kept in sync with the go struct in organizations.go */ @@ -140,6 +147,17 @@ export const logout = async (): Promise => { return } +export const getUser = async (): Promise => { + const response = await fetch("/api/v2/users/me") + const body = await response.json() + + if(!response.ok) { + throw new Error(body.message) + } + + return body +} + export const getApiKey = async (): Promise<{ key: string }> => { const response = await fetch("/api/v2/users/me/keys", { method: "POST", diff --git a/site/xServices/user/userXService.ts b/site/xServices/user/userXService.ts new file mode 100644 index 0000000000000..4c0a0b106c96c --- /dev/null +++ b/site/xServices/user/userXService.ts @@ -0,0 +1,120 @@ +import { createMachine, interpret, assign } from "xstate" +import { UserResponse } from "../../api" +import * as API from "../../api" + +export interface UserContext { + error?: Error + me?: UserResponse +} + +export type UserEvent = { type: "SIGN_OUT" } | { type: "SIGN_IN"; email: string; password: string } + +const userMachine = + /** @xstate-layout N4IgpgJg5mDOIC5QFdZgE4GUAuBDbYAdLAJZQB2kA8stgMSYCSA4gHID6jrioADgPalsJfuR4gAHogAcAJkIBGAJxKArAoDMqpRoAsAdn0bZAGhABPRBulLCs2XLUaNABk0vVAX09nUGHPhEpBQk5FCM5HQQokShAG78ANZBZOQR4gJCImJIklYu8voFctKqGgoAbPraZpYIFdIudm66SroVeqoOFd6+aFh4BMSpoeGRGOj86IS8ADb4AGZTALbDFOm5mSTCouJSCHryunpKLrrHurL6yrWIZRWEFQpuJRr6ci76vSB+A4FrlAgEUIJAgszAdAAogARRgAFQygm22T2iAqSgeCme1zKXSUCmOt3qskxBPUBP0bUaum+vwCQ2CgOBkGRYSiMRB5ASyUILOwAFV+oisrtcvtpO1CNZdAp9LoPIdVBUiaoPIoye53tJZQTaf16SkKJAIgwWBwqPyEZskTscqB9uV9IptdI5Zp1KVqkTnqpCDpPm9WspjNY9f5BobyKMaPRopROdzIzHhcjRfbELJTs15a62mp5R4ibJVLpCDYVApVNdLsoHGG-gyRmEY3QJlMZvNsEt0KtGcnrSK7Xl6hUHi5tRV7LICy50d61QobHKMWS3IvvD4QOR+BA4OI6RGAdRaCnbaj6k0XEpZHoio0tG05fPfQVKq4NPj0RUCvWDQDRhsfA2iiYoZk6KiqK6zhVKqzymBYdxaIQBjLlcug2AS0i-oejLGuQIJgmAp4gemCBVtI6oBtqlZKtIyoIcSFFYro6hnMoLGtNh-y4UC+F8qMxFpsOlK2Bo35ysWWp6AoRKTk0zHkjehiyD+m4HtxqR4YJQ77KoJaUUY1F6Q09F1Mc8gKTmJzHFhan6jhTZQP2QGDuejROrK7Qfm8s5vEWFSlsWj5FLKrpeHZ4aBNp54aH6ahQWJ1RuG4bREsYhCvh0V6GCW0gaBunhAA */ + createMachine( + { + tsTypes: {} as import("./userXService.typegen").Typegen0, + schema: { + context: {} as UserContext, + events: {} as UserEvent, + services: {} as { + getMe: { + data: API.UserResponse + }, + signIn: { + data: API.LoginResponse | undefined + }, + signOut: { + data: void + } + } + }, + id: "userState", + initial: "signedOut", + states: { + signedOut: { + on: { + SIGN_IN: { + target: "#userState.signingIn", + }, + }, + }, + signingIn: { + tags: "loading", + invoke: { + src: "signIn", + id: "signIn", + onDone: "#userState.gettingUser", + onError: { + actions: "assignError", + target: "#userState.signedOut", + }, + }, + }, + gettingUser: { + tags: "loading", + invoke: { + src: "getMe", + id: "getMe", + onDone: { + target: "signedIn", + actions: "assignMe" + }, + onError: { + actions: "assignError" + } + } + }, + signedIn: { + on: { + SIGN_OUT: { + target: "#userState.signingOut", + }, + }, + }, + signingOut: { + tags: "loading", + invoke: { + src: "signOut", + id: "signOut", + onDone: [ + { + actions: "unassignMe", + target: "#userState.signedOut", + }, + ], + onError: [ + { + actions: ["assignError"], + target: "#userState.signedOut", + }, + ], + }, + }, + }, + }, + { + services: { + signIn: async (context: UserContext, event: UserEvent) => { + if (event.type === 'SIGN_IN') { + return await API.login(event.email, event.password) + } + }, + signOut: API.logout, + getMe: API.getUser + }, + actions: { + assignMe: assign({ + me: (_, event) => event.data + }), + unassignMe: assign({ + me: () => undefined, + }), + assignError: assign({ + error: (_, event) => event.data, + }), + } + }, + ) + +export const userService = interpret(userMachine).start() From 561cd6b1c77f52b1a52881ecea6b06400de274bb Mon Sep 17 00:00:00 2001 From: Presley Date: Tue, 15 Mar 2022 02:55:17 +0000 Subject: [PATCH 03/42] Lint, fix error transitions --- site/api.ts | 2 +- site/xServices/user/userXService.ts | 65 +++++++++++++++++------------ 2 files changed, 39 insertions(+), 28 deletions(-) diff --git a/site/api.ts b/site/api.ts index 5c9fa58761f28..918181ba4ab0c 100644 --- a/site/api.ts +++ b/site/api.ts @@ -151,7 +151,7 @@ export const getUser = async (): Promise => { const response = await fetch("/api/v2/users/me") const body = await response.json() - if(!response.ok) { + if (!response.ok) { throw new Error(body.message) } diff --git a/site/xServices/user/userXService.ts b/site/xServices/user/userXService.ts index 4c0a0b106c96c..ff384ed05ef51 100644 --- a/site/xServices/user/userXService.ts +++ b/site/xServices/user/userXService.ts @@ -10,7 +10,7 @@ export interface UserContext { export type UserEvent = { type: "SIGN_OUT" } | { type: "SIGN_IN"; email: string; password: string } const userMachine = - /** @xstate-layout N4IgpgJg5mDOIC5QFdZgE4GUAuBDbYAdLAJZQB2kA8stgMSYCSA4gHID6jrioADgPalsJfuR4gAHogAcAJkIBGAJxKArAoDMqpRoAsAdn0bZAGhABPRBulLCs2XLUaNABk0vVAX09nUGHPhEpBQk5FCM5HQQokShAG78ANZBZOQR4gJCImJIklYu8voFctKqGgoAbPraZpYIFdIudm66SroVeqoOFd6+aFh4BMSpoeGRGOj86IS8ADb4AGZTALbDFOm5mSTCouJSCHryunpKLrrHurL6yrWIZRWEFQpuJRr6ci76vSB+A4FrlAgEUIJAgszAdAAogARRgAFQygm22T2iAqSgeCme1zKXSUCmOt3qskxBPUBP0bUaum+vwCQ2CgOBkGRYSiMRB5ASyUILOwAFV+oisrtcvtpO1CNZdAp9LoPIdVBUiaoPIoye53tJZQTaf16SkKJAIgwWBwqPyEZskTscqB9uV9IptdI5Zp1KVqkTnqpCDpPm9WspjNY9f5BobyKMaPRopROdzIzHhcjRfbELJTs15a62mp5R4ibJVLpCDYVApVNdLsoHGG-gyRmEY3QJlMZvNsEt0KtGcnrSK7Xl6hUHi5tRV7LICy50d61QobHKMWS3IvvD4QOR+BA4OI6RGAdRaCnbaj6k0XEpZHoio0tG05fPfQVKq4NPj0RUCvWDQDRhsfA2iiYoZk6KiqK6zhVKqzymBYdxaIQBjLlcug2AS0i-oejLGuQIJgmAp4gemCBVtI6oBtqlZKtIyoIcSFFYro6hnMoLGtNh-y4UC+F8qMxFpsOlK2Bo35ysWWp6AoRKTk0zHkjehiyD+m4HtxqR4YJQ77KoJaUUY1F6Q09F1Mc8gKTmJzHFhan6jhTZQP2QGDuejROrK7Qfm8s5vEWFSlsWj5FLKrpeHZ4aBNp54aH6ahQWJ1RuG4bREsYhCvh0V6GCW0gaBunhAA */ + /** @xstate-layout N4IgpgJg5mDOIC5QFdZgE4GUAuBDbYAdLAJZQB2kA8stgMSYCSA4gHID6jrioADgPalsJfuR4gAHogDs0gIyF5ADmkBmAJyyALACY5W6QBoQAT0QBWdecIAGOzZ2qlANhtznlgL6fjqDDnwiUgoScihGcjoIUSJQgDd+AGsgsnII8QEhETEkSUQddQUC5xUHG2cK1TlVYzMELX1CCoqlGyUdA2dVCu9fNCw8AmJU0PDIjHR+dEJeABt8ADMpgFthinTczJJhUXEpBAKi9RLpMuaqmtMZHR1Cc3sdczln6UstXpA-AcDCGGxhMIAVX6URihHiSSIfwAsmAMoJttk9tdVIQNOVquV1IV5LVEKpXrZ7HIdEoya8bKpzB8vgEhn8AVBgRg6BMpjN5tgluhVjC4ZsETscqB9tJHGj1BjVFicXI8fUlNZ7G4lAStFpzEp9DT+nSUhRIBEGCwOFRAQAVeFZXa5UXmazOZRilzHUnyyWEbFe9QNX2yHQ6-yDfXkUY0ejRSjg8gJZJrcjhq2Im0ilESqUyuS4q4HDxE+zSLSUmy6czOQPfIbBUNhcOs9CTaZzRYreOJgXW4V5BBi1Ho5yY5zYrNynNye35uxyHGqVQGaTeHwgcj8CBwcS04Px6i0JNC5EISVKQgdbFKdRkmxqVQ6eVPVFFux6KnucwFCt6+OjDZ8QVI22ILo8oNNIhANC0HQ2OoNyyNSS6bj8DKjMy6B7v+qb1M4twDhqYpyEoWhOCU8oaAo7gQYWOhQQRH5btWhpdls+4AYebQnloZ4Xq0163mO7R3MqHHmFU+hKLRPzVmGu4dsmXb7Oqx56CoBKKic2LukU9z2GeA74XBfRBoEaEpt2+HylmnrerOpYqI6AaLkAA */ createMachine( { tsTypes: {} as import("./userXService.typegen").Typegen0, @@ -20,14 +20,14 @@ const userMachine = services: {} as { getMe: { data: API.UserResponse - }, + } signIn: { data: API.LoginResponse | undefined - }, + } signOut: { data: void } - } + }, }, id: "userState", initial: "signedOut", @@ -40,30 +40,41 @@ const userMachine = }, }, signingIn: { - tags: "loading", invoke: { src: "signIn", id: "signIn", - onDone: "#userState.gettingUser", - onError: { - actions: "assignError", - target: "#userState.signedOut", - }, + onDone: [ + { + target: "#userState.gettingUser", + }, + ], + onError: [ + { + actions: "assignError", + target: "#userState.signedOut", + }, + ], }, + tags: "loading", }, gettingUser: { - tags: "loading", invoke: { src: "getMe", id: "getMe", - onDone: { - target: "signedIn", - actions: "assignMe" - }, - onError: { - actions: "assignError" - } - } + onDone: [ + { + actions: "assignMe", + target: "#userState.signedIn", + }, + ], + onError: [ + { + actions: "assignError", + target: "#userState.signedOut", + }, + ], + }, + tags: "loading", }, signedIn: { on: { @@ -73,7 +84,6 @@ const userMachine = }, }, signingOut: { - tags: "loading", invoke: { src: "signOut", id: "signOut", @@ -85,27 +95,28 @@ const userMachine = ], onError: [ { - actions: ["assignError"], - target: "#userState.signedOut", + actions: "assignError", + target: "#userState.signedIn", }, ], }, + tags: "loading", }, }, }, { services: { signIn: async (context: UserContext, event: UserEvent) => { - if (event.type === 'SIGN_IN') { + if (event.type === "SIGN_IN") { return await API.login(event.email, event.password) } - }, + }, signOut: API.logout, - getMe: API.getUser + getMe: API.getUser, }, actions: { assignMe: assign({ - me: (_, event) => event.data + me: (_, event) => event.data, }), unassignMe: assign({ me: () => undefined, @@ -113,7 +124,7 @@ const userMachine = assignError: assign({ error: (_, event) => event.data, }), - } + }, }, ) From 1548fb8ea3a39094187cf48fc3f458dc85325395 Mon Sep 17 00:00:00 2001 From: Presley Date: Tue, 15 Mar 2022 03:00:00 +0000 Subject: [PATCH 04/42] Lint --- site/xServices/user/userXService.ts | 65 ++++++++++++----------------- 1 file changed, 26 insertions(+), 39 deletions(-) diff --git a/site/xServices/user/userXService.ts b/site/xServices/user/userXService.ts index ff384ed05ef51..70dbbd5db2862 100644 --- a/site/xServices/user/userXService.ts +++ b/site/xServices/user/userXService.ts @@ -43,17 +43,14 @@ const userMachine = invoke: { src: "signIn", id: "signIn", - onDone: [ - { - target: "#userState.gettingUser", - }, - ], - onError: [ - { - actions: "assignError", - target: "#userState.signedOut", - }, - ], + onDone: { + target: "#userState.gettingUser", + }, + + onError: { + actions: "assignError", + target: "#userState.signedOut", + }, }, tags: "loading", }, @@ -61,18 +58,14 @@ const userMachine = invoke: { src: "getMe", id: "getMe", - onDone: [ - { - actions: "assignMe", - target: "#userState.signedIn", - }, - ], - onError: [ - { - actions: "assignError", - target: "#userState.signedOut", - }, - ], + onDone: { + actions: "assignMe", + target: "#userState.signedIn", + }, + onError: { + actions: "assignError", + target: "#userState.signedOut", + }, }, tags: "loading", }, @@ -87,18 +80,14 @@ const userMachine = invoke: { src: "signOut", id: "signOut", - onDone: [ - { - actions: "unassignMe", - target: "#userState.signedOut", - }, - ], - onError: [ - { - actions: "assignError", - target: "#userState.signedIn", - }, - ], + onDone: { + actions: "unassignMe", + target: "#userState.signedOut", + }, + onError: { + actions: "assignError", + target: "#userState.signedIn", + }, }, tags: "loading", }, @@ -106,10 +95,8 @@ const userMachine = }, { services: { - signIn: async (context: UserContext, event: UserEvent) => { - if (event.type === "SIGN_IN") { - return await API.login(event.email, event.password) - } + signIn: async (_, event: UserEvent) => { + return await API.login(event.email, event.password) }, signOut: API.logout, getMe: API.getUser, From 80e10c8f69a7bfabfc9ede8731bc6875bd03ffd7 Mon Sep 17 00:00:00 2001 From: Presley Date: Tue, 15 Mar 2022 03:22:07 +0000 Subject: [PATCH 05/42] Change initial state to handle loss of state --- site/xServices/user/userXService.ts | 153 +++++++++++++++------------- 1 file changed, 82 insertions(+), 71 deletions(-) diff --git a/site/xServices/user/userXService.ts b/site/xServices/user/userXService.ts index 70dbbd5db2862..f93c4833ea9c9 100644 --- a/site/xServices/user/userXService.ts +++ b/site/xServices/user/userXService.ts @@ -10,89 +10,100 @@ export interface UserContext { export type UserEvent = { type: "SIGN_OUT" } | { type: "SIGN_IN"; email: string; password: string } const userMachine = - /** @xstate-layout N4IgpgJg5mDOIC5QFdZgE4GUAuBDbYAdLAJZQB2kA8stgMSYCSA4gHID6jrioADgPalsJfuR4gAHogDs0gIyF5ADmkBmAJyyALACY5W6QBoQAT0QBWdecIAGOzZ2qlANhtznlgL6fjqDDnwiUgoScihGcjoIUSJQgDd+AGsgsnII8QEhETEkSUQddQUC5xUHG2cK1TlVYzMELX1CCoqlGyUdA2dVCu9fNCw8AmJU0PDIjHR+dEJeABt8ADMpgFthinTczJJhUXEpBAKi9RLpMuaqmtMZHR1Cc3sdczln6UstXpA-AcDCGGxhMIAVX6URihHiSSIfwAsmAMoJttk9tdVIQNOVquV1IV5LVEKpXrZ7HIdEoya8bKpzB8vgEhn8AVBgRg6BMpjN5tgluhVjC4ZsETscqB9tJHGj1BjVFicXI8fUlNZ7G4lAStFpzEp9DT+nSUhRIBEGCwOFRAQAVeFZXa5UXmazOZRilzHUnyyWEbFe9QNX2yHQ6-yDfXkUY0ejRSjg8gJZJrcjhq2Im0ilESqUyuS4q4HDxE+zSLSUmy6czOQPfIbBUNhcOs9CTaZzRYreOJgXW4V5BBi1Ho5yY5zYrNynNye35uxyHGqVQGaTeHwgcj8CBwcS04Px6i0JNC5EISVKQgdbFKdRkmxqVQ6eVPVFFux6KnucwFCt6+OjDZ8QVI22ILo8oNNIhANC0HQ2OoNyyNSS6bj8DKjMy6B7v+qb1M4twDhqYpyEoWhOCU8oaAo7gQYWOhQQRH5btWhpdls+4AYebQnloZ4Xq0163mO7R3MqHHmFU+hKLRPzVmGu4dsmXb7Oqx56CoBKKic2LukU9z2GeA74XBfRBoEaEpt2+HylmnrerOpYqI6AaLkAA */ + /** @xstate-layout N4IgpgJg5mDOIC5QFdZgE4GUAuBDbYAdLAJZQB2kA8stgMSYCSA4gHID6jrioADgPalsJfuR4gAHogDM0gJyEATAHYAbNIAscxdunKArKo0AaEAE9EG+YX0BGABz3FD6atsbbc5QF9vp1Bg4+ESkFCTkUIzkdBCiROEAbvwA1iFk5FHiAkIiYkiSiHIADITSOvbSRRqq2m7StqYWCIr2+ja2tvVV+vqaRVW+-mhYeATE6eGR0Rjo-OiEvAA2+ABmcwC24xSZ+dkkwqLiUgiytoQahna2Bo5FDeaI+optyo5y9nIOrQY+fiABI2ChBg2GEEQAqsMYnFCIkUkQQQBZMBZQT7XJHGRGQjKYrKDQfIqKKyKPSNRC2ImlRRFfS4jqqKrFOSDf7DIJjEFgqCQjB0GZzBbLbBrdCbJEo3Zog55UDHaT2EqaZSueSqHQ1Krk5plQhGeyvO7XIqqJysgEctIUSBRBgsDhUcEAFVROUO+WOBvs5zUKrUtlUdPk2tsKkIlMqqkDcn0rUq9nN7NGVvIkxo9FilFh5CSqS25HTrvR7rliCMGlK1Tkcij9lNF302qeqhx7iKXmcGg8tkTgWT+bTtH56Fm8yWqw2+cLUrdsoKCEDbSj7bUd2UoYJ2sULXDi4Mn3VlQ0vj+5H4EDg4gt-dClAg0740oxHseocIck0gf1jIu9yaXbOPR8XXXETVxBM-mvIFb0mHZH1nTEEDsQgiSMbQLlkZRum1LtvWuEC3BjaRYwMXtAU5MBQUmXl0CLGVEI0ZRCEVJ4sPKAxPGIkMqUpQ0rHxDQTQVMjLXzG05z2eiXwXao9Q3IosLsA1FBDMNFRqUNlEUVQDB0llIKTaCJgiB8QEk59SwQewHBQjwnAMFcCVUHDOhQ+QsPbWkPmskTkzoiz51JZjaRUIl3g4j9GweZoFF4xUtEZZ461UE9vCAA */ createMachine( { - tsTypes: {} as import("./userXService.typegen").Typegen0, - schema: { - context: {} as UserContext, - events: {} as UserEvent, - services: {} as { - getMe: { - data: API.UserResponse - } - signIn: { - data: API.LoginResponse | undefined - } - signOut: { - data: void - } + tsTypes: {} as import("./userXService.typegen").Typegen0, + schema: { + context: {} as UserContext, + events: {} as UserEvent, + services: {} as { + getMe: { + data: API.UserResponse + } + signIn: { + data: API.LoginResponse | undefined + } + signOut: { + data: void + } + }, + }, + id: "userState", + initial: "gettingUser", + states: { + signedOut: { + on: { + SIGN_IN: { + target: "#userState.signingIn", }, }, - id: "userState", - initial: "signedOut", - states: { - signedOut: { - on: { - SIGN_IN: { - target: "#userState.signingIn", - }, + }, + signingIn: { + invoke: { + src: "signIn", + id: "signIn", + onDone: [ + { + target: "#userState.gettingUser", }, - }, - signingIn: { - invoke: { - src: "signIn", - id: "signIn", - onDone: { - target: "#userState.gettingUser", - }, - - onError: { - actions: "assignError", - target: "#userState.signedOut", - }, + ], + onError: [ + { + actions: "assignError", + target: "#userState.signedOut", }, - tags: "loading", - }, - gettingUser: { - invoke: { - src: "getMe", - id: "getMe", - onDone: { - actions: "assignMe", - target: "#userState.signedIn", - }, - onError: { - actions: "assignError", - target: "#userState.signedOut", - }, + ], + }, + tags: "loading", + }, + gettingUser: { + invoke: { + src: "getMe", + id: "getMe", + onDone: [ + { + actions: "assignMe", + target: "#userState.signedIn", }, - tags: "loading", - }, - signedIn: { - on: { - SIGN_OUT: { - target: "#userState.signingOut", - }, + ], + onError: [ + { + actions: "assignError", + target: "#userState.signedOut", }, + ], + }, + tags: "loading", + }, + signedIn: { + on: { + SIGN_OUT: { + target: "#userState.signingOut", }, - signingOut: { - invoke: { - src: "signOut", - id: "signOut", - onDone: { - actions: "unassignMe", - target: "#userState.signedOut", - }, - onError: { - actions: "assignError", - target: "#userState.signedIn", - }, + }, + }, + signingOut: { + invoke: { + src: "signOut", + id: "signOut", + onDone: [ + { + actions: "unassignMe", + target: "#userState.signedOut", }, - tags: "loading", - }, + ], + onError: [ + { + actions: "assignError", + target: "#userState.signedIn", + }, + ], }, + tags: "loading", }, + }, +}, { services: { signIn: async (_, event: UserEvent) => { From 3daf0a9c62b6d824ad99b769ab5a44b2ccf6ec86 Mon Sep 17 00:00:00 2001 From: Presley Date: Tue, 15 Mar 2022 03:39:06 +0000 Subject: [PATCH 06/42] Fix gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index dba8147adbae6..3eda69d0c83a4 100644 --- a/.gitignore +++ b/.gitignore @@ -23,7 +23,7 @@ site/storybook-static/ site/test-results/ site/yarn-error.log coverage/ -site/**/*.typegen +site/**/*.typegen.ts # Build dist/ From 9f653272f7e705aa19a4ce6e3e29345502166ba9 Mon Sep 17 00:00:00 2001 From: Presley Date: Tue, 15 Mar 2022 14:55:00 +0000 Subject: [PATCH 07/42] Fix types by hook or by crook --- site/xServices/user/userXService.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/site/xServices/user/userXService.ts b/site/xServices/user/userXService.ts index f93c4833ea9c9..d00c5d6a0d610 100644 --- a/site/xServices/user/userXService.ts +++ b/site/xServices/user/userXService.ts @@ -3,7 +3,7 @@ import { UserResponse } from "../../api" import * as API from "../../api" export interface UserContext { - error?: Error + error?: Error | unknown // unknown is a concession while I work out typing issues me?: UserResponse } @@ -107,7 +107,9 @@ const userMachine = { services: { signIn: async (_, event: UserEvent) => { - return await API.login(event.email, event.password) + if (event.type === 'SIGN_IN') { + return await API.login(event.email, event.password) + } }, signOut: API.logout, getMe: API.getUser, @@ -116,12 +118,13 @@ const userMachine = assignMe: assign({ me: (_, event) => event.data, }), - unassignMe: assign({ - me: () => undefined, - }), + unassignMe: assign((context: UserContext) => ({ + ...context, + me: undefined, + })), assignError: assign({ error: (_, event) => event.data, - }), + }) }, }, ) From d6bd5f649301e04a86685e3708592189213b3c71 Mon Sep 17 00:00:00 2001 From: Presley Date: Tue, 15 Mar 2022 20:37:39 +0000 Subject: [PATCH 08/42] Use xservice in all pages --- site/components/SignIn/SignInForm.stories.tsx | 9 ++++- site/components/SignIn/SignInForm.tsx | 38 +++++++----------- site/pages/cli-auth.tsx | 7 +++- site/pages/index.tsx | 6 ++- .../[organization]/[project]/create.tsx | 6 ++- .../[organization]/[project]/index.tsx | 8 ++-- site/pages/projects/index.tsx | 15 +++++-- site/pages/workspaces/[workspace].tsx | 8 ++-- site/xServices/user/userXService.ts | 40 ++++++++++++++----- 9 files changed, 84 insertions(+), 53 deletions(-) diff --git a/site/components/SignIn/SignInForm.stories.tsx b/site/components/SignIn/SignInForm.stories.tsx index 5556cf38b6b6d..d9e16692414b2 100644 --- a/site/components/SignIn/SignInForm.stories.tsx +++ b/site/components/SignIn/SignInForm.stories.tsx @@ -1,6 +1,7 @@ import { Story } from "@storybook/react" import React from "react" -import { SignInForm, SignInProps } from "./SignInForm" +import { BrowserRouter } from "react-router-dom" +import { SignInForm } from "./SignInForm" export default { title: "SignIn/SignInForm", @@ -10,7 +11,11 @@ export default { }, } -const Template: Story = (args) => +const Template: Story = (args) => ( + + + +) export const Example = Template.bind({}) Example.args = {} diff --git a/site/components/SignIn/SignInForm.tsx b/site/components/SignIn/SignInForm.tsx index 6f48f61058d5e..9853bdc807d11 100644 --- a/site/components/SignIn/SignInForm.tsx +++ b/site/components/SignIn/SignInForm.tsx @@ -1,15 +1,15 @@ import { makeStyles } from "@material-ui/core/styles" import { FormikContextType, useFormik } from "formik" import { Location } from "history" -import { useNavigate, useLocation } from "react-router-dom" +import { useLocation, Navigate } from "react-router-dom" import React from "react" -import { useSWRConfig } from "swr" import * as Yup from "yup" import { Welcome } from "./Welcome" import { FormTextField } from "../Form" -import * as API from "./../../api" import { LoadingButton } from "./../Button" +import { userXService } from "../../xServices/user/userXService" +import { useActor } from "@xstate/react" /** * BuiltInAuthFormValues describes a form using built-in (email/password) @@ -40,17 +40,11 @@ const useStyles = makeStyles((theme) => ({ }, })) -export interface SignInProps { - loginHandler?: (email: string, password: string) => Promise -} - -export const SignInForm: React.FC = ({ - loginHandler = (email: string, password: string) => API.login(email, password), -}) => { - const navigate = useNavigate() +export const SignInForm: React.FC = () => { const location = useLocation() const styles = useStyles() - const { mutate } = useSWRConfig() + const [userState, userSend] = useActor(userXService) + const { authError } = userState.context const form: FormikContextType = useFormik({ initialValues: { @@ -58,20 +52,15 @@ export const SignInForm: React.FC = ({ password: "", }, validationSchema, - onSubmit: async ({ email, password }, helpers) => { - try { - await loginHandler(email, password) - // Tell SWR to invalidate the cache for the user endpoint - await mutate("/api/v2/users/me") - - const redirect = getRedirectFromLocation(location) - await navigate(redirect) - } catch (err) { - helpers.setFieldError("password", "The username or password is incorrect.") - } + onSubmit: async ({ email, password }) => { + userSend({ type: 'SIGN_IN', email, password }) }, }) + if (userState.matches('signedIn')) { + return + } + return ( <> @@ -103,12 +92,13 @@ export const SignInForm: React.FC = ({ isPassword placeholder="Password" variant="outlined" + helperText={authError ? (authError as Error).message : ''} />
{ - const { me } = useUser(true) + const [userState] = useActor(userXService) + const { me } = userState.context + const styles = useStyles() const [apiKey, setApiKey] = useState(null) diff --git a/site/pages/index.tsx b/site/pages/index.tsx index e6ae6e51ff2d3..6d26587c17056 100644 --- a/site/pages/index.tsx +++ b/site/pages/index.tsx @@ -1,11 +1,13 @@ +import { useActor } from "@xstate/react" import React from "react" import { Navigate } from "react-router-dom" import { FullScreenLoader } from "../components/Loader/FullScreenLoader" -import { useUser } from "../contexts/UserContext" +import { userXService } from "../xServices/user/userXService" export const IndexPage: React.FC = () => { - const { me } = useUser(/* redirectOnError */ true) + const [userState] = useActor(userXService) + const { me } = userState.context if (me) { // Once the user is logged in, just redirect them to /projects as the landing page diff --git a/site/pages/projects/[organization]/[project]/create.tsx b/site/pages/projects/[organization]/[project]/create.tsx index 545c9913c663a..c1b2c8e18a413 100644 --- a/site/pages/projects/[organization]/[project]/create.tsx +++ b/site/pages/projects/[organization]/[project]/create.tsx @@ -4,17 +4,19 @@ import { useNavigate, useParams } from "react-router-dom" import useSWR from "swr" import * as API from "../../../../api" -import { useUser } from "../../../../contexts/UserContext" import { ErrorSummary } from "../../../../components/ErrorSummary" import { FullScreenLoader } from "../../../../components/Loader/FullScreenLoader" import { CreateWorkspaceForm } from "../../../../forms/CreateWorkspaceForm" import { unsafeSWRArgument } from "../../../../util" +import { useActor } from "@xstate/react" +import { userXService } from "../../../../xServices/user/userXService" export const CreateWorkspacePage: React.FC = () => { const { organization: organizationName, project: projectName } = useParams() const navigate = useNavigate() const styles = useStyles() - const { me } = useUser(/* redirectOnError */ true) + const [userState] = useActor(userXService) + const { me } = userState.context const { data: organizationInfo, error: organizationError } = useSWR( () => `/api/v2/users/me/organizations/${organizationName}`, diff --git a/site/pages/projects/[organization]/[project]/index.tsx b/site/pages/projects/[organization]/[project]/index.tsx index 858a6bb87a565..cea1127e035ed 100644 --- a/site/pages/projects/[organization]/[project]/index.tsx +++ b/site/pages/projects/[organization]/[project]/index.tsx @@ -10,15 +10,17 @@ import { FullScreenLoader } from "../../../../components/Loader/FullScreenLoader import { Navbar } from "../../../../components/Navbar" import { Footer } from "../../../../components/Page" import { Column, Table } from "../../../../components/Table" -import { useUser } from "../../../../contexts/UserContext" import { ErrorSummary } from "../../../../components/ErrorSummary" import { firstOrItem } from "../../../../util/array" import { EmptyState } from "../../../../components/EmptyState" import { unsafeSWRArgument } from "../../../../util" +import { useActor } from "@xstate/react" +import { userXService } from "../../../../xServices/user/userXService" export const ProjectPage: React.FC = () => { const styles = useStyles() - const { me, signOut } = useUser(true) + const [userState, userSend] = useActor(userXService) + const { me } = userState.context const navigate = useNavigate() const { project: projectName, organization: organizationName } = useParams() @@ -89,7 +91,7 @@ export const ProjectPage: React.FC = () => { return (
- + userSend('SIGN_OUT')} />
{ const styles = useStyles() - const { me, signOut } = useUser(true) + const location = useLocation() + const [userState, userSend] = useActor(userXService) + const { me } = userState.context const { data: orgs, error: orgsError } = useSWR("/api/v2/users/me/organizations") const { data: projects, error } = useSWR( orgs ? `/api/v2/organizations/${orgs[0].id}/projects` : null, ) + if (userState.matches('signedOut')) { + return + } + if (error) { return } @@ -74,7 +81,7 @@ export const ProjectsPage: React.FC = () => { return (
- + userSend("SIGN_OUT")} />
diff --git a/site/pages/workspaces/[workspace].tsx b/site/pages/workspaces/[workspace].tsx index 86e3e0ba4cdbe..1f5b590171e5c 100644 --- a/site/pages/workspaces/[workspace].tsx +++ b/site/pages/workspaces/[workspace].tsx @@ -4,18 +4,20 @@ import { makeStyles } from "@material-ui/core/styles" import { useParams } from "react-router-dom" import { Navbar } from "../../components/Navbar" import { Footer } from "../../components/Page" -import { useUser } from "../../contexts/UserContext" import { firstOrItem } from "../../util/array" import { ErrorSummary } from "../../components/ErrorSummary" import { FullScreenLoader } from "../../components/Loader/FullScreenLoader" import { Workspace } from "../../components/Workspace" import { unsafeSWRArgument } from "../../util" import * as API from "../../api" +import { useActor } from "@xstate/react" +import { userXService } from "../../xServices/user/userXService" export const WorkspacePage: React.FC = () => { const styles = useStyles() const { workspace: workspaceQueryParam } = useParams() - const { me, signOut } = useUser(true) + const [userState, userSend] = useActor(userXService) + const { me } = userState.context const { data: workspace, error: workspaceError } = useSWR(() => { const workspaceParam = firstOrItem(workspaceQueryParam, null) @@ -50,7 +52,7 @@ export const WorkspacePage: React.FC = () => { return (
- + userSend("SIGN_OUT")} />
diff --git a/site/xServices/user/userXService.ts b/site/xServices/user/userXService.ts index d00c5d6a0d610..173e4bf0b419c 100644 --- a/site/xServices/user/userXService.ts +++ b/site/xServices/user/userXService.ts @@ -3,14 +3,15 @@ import { UserResponse } from "../../api" import * as API from "../../api" export interface UserContext { - error?: Error | unknown // unknown is a concession while I work out typing issues + getUserError?: Error | unknown // unknown is a concession while I work out typing issues + authError?: Error | unknown me?: UserResponse } export type UserEvent = { type: "SIGN_OUT" } | { type: "SIGN_IN"; email: string; password: string } const userMachine = - /** @xstate-layout N4IgpgJg5mDOIC5QFdZgE4GUAuBDbYAdLAJZQB2kA8stgMSYCSA4gHID6jrioADgPalsJfuR4gAHogDM0gJyEATAHYAbNIAscxdunKArKo0AaEAE9EG+YX0BGABz3FD6atsbbc5QF9vp1Bg4+ESkFCTkUIzkdBCiROEAbvwA1iFk5FHiAkIiYkiSiHIADITSOvbSRRqq2m7StqYWCIr2+ja2tvVV+vqaRVW+-mhYeATE6eGR0Rjo-OiEvAA2+ABmcwC24xSZ+dkkwqLiUgiytoQahna2Bo5FDeaI+optyo5y9nIOrQY+fiABI2ChBg2GEEQAqsMYnFCIkUkQQQBZMBZQT7XJHGRGQjKYrKDQfIqKKyKPSNRC2ImlRRFfS4jqqKrFOSDf7DIJjEFgqCQjB0GZzBbLbBrdCbJEo3Zog55UDHaT2EqaZSueSqHQ1Krk5plQhGeyvO7XIqqJysgEctIUSBRBgsDhUcEAFVROUO+WOBvs5zUKrUtlUdPk2tsKkIlMqqkDcn0rUq9nN7NGVvIkxo9FilFh5CSqS25HTrvR7rliCMGlK1Tkcij9lNF302qeqhx7iKXmcGg8tkTgWT+bTtH56Fm8yWqw2+cLUrdsoKCEDbSj7bUd2UoYJ2sULXDi4Mn3VlQ0vj+5H4EDg4gt-dClAg0740oxHseocIck0gf1jIu9yaXbOPR8XXXETVxBM-mvIFb0mHZH1nTEEDsQgiSMbQLlkZRum1LtvWuEC3BjaRYwMXtAU5MBQUmXl0CLGVEI0ZRCEVJ4sPKAxPGIkMqUpQ0rHxDQTQVMjLXzG05z2eiXwXao9Q3IosLsA1FBDMNFRqUNlEUVQDB0llIKTaCJgiB8QEk59SwQewHBQjwnAMFcCVUHDOhQ+QsPbWkPmskTkzoiz51JZjaRUIl3g4j9GweZoFF4xUtEZZ461UE9vCAA */ + /** @xstate-layout N4IgpgJg5mDOIC5QFdZgE4GUAuBDbYAdLAJZQB2kA8stgMSYCSA4gHID6jrioADgPalsJfuR4gAHogDM0gJyEATAHYAbNIAscxdunKArKo0AaEAE9EG+YX0BGABz3FD6atsbbc5QF9vp1Bg4+ESkFCTkUIzkdBCiROEAbvwA1iFk5FHiAkIiYkiSiHIADITSOvbSRRqq2m7StqYWCIr2+ja2tvVV+vqaRVW+-mhYeATE6eGR0Rjo-OiEvAA2+ABmcwC24xSZ+dkkwqLiUghlyoRF+opayq4X+sqK+o2Il6qEcq5y+hrfjt+qgxAARGwUIMGwwgiAFVhjE4oREikiOCALJgLKCfa5I4yIyEZTFZQaezFK5lPTPBC2IqKUqKC4EjqqKrFOSA4FBMbgyFQGEYOgzOYLZbYNboTao9G7TEHPKgY7SewlTQ3dRyVQ6GpVSmKMqEIz2ZRKjrKIqqJzs4actIUSBRBgsDhUKEAFQxOUO+WOhvshA0ahualsqnu8kpthUhGplVUIa+rUq9ktgVGNvIkxo9FilAR5CSqS25Ez7qxnvliCMGlK1Tk6vN5p+T3ML0Ub2U7iKXmcGg8tmTILGoXTEUzAvQs3mS1WG0LxelHrlBQQIbasc7aiKtnbV3sOpaUZXBjkwd1A0B5H4EDg4g5qcL1FoJdlOIQdlpH2qhmJzJ+DWbCB7WxSmUIl2wJM0CSTPwgStO8h0mHY+BlbEvReICaSMbQflkU0fkpHtfS3MC3C+aR9ENfR+2tMEwAhSY+XQJ8UPLACziVS5TXKAxPDI8MaSjIojSqPQezNRUqLg9I7UXPZn1Q5dqn1CMNEEi4HAecNIyVGoIweVQDH0tloNvUF4JHR951LRdvQcc4PCcAx12-fDOnOeRTU7C4SXsPtjNg4ImLLJddUIdiVBpOQKJ4psmh0ASQOPRUiI8RRfF8IA */ createMachine( { tsTypes: {} as import("./userXService.typegen").Typegen0, @@ -29,6 +30,11 @@ const userMachine = } }, }, + context: { + me: undefined, + getUserError: undefined, + authError: undefined + }, id: "userState", initial: "gettingUser", states: { @@ -46,11 +52,12 @@ const userMachine = onDone: [ { target: "#userState.gettingUser", + actions: "clearAuthError" }, ], onError: [ { - actions: "assignError", + actions: "assignAuthError", target: "#userState.signedOut", }, ], @@ -63,13 +70,13 @@ const userMachine = id: "getMe", onDone: [ { - actions: "assignMe", + actions: ["assignMe", "clearGetUserError"], target: "#userState.signedIn", }, ], onError: [ { - actions: "assignError", + actions: "assignGetUserError", target: "#userState.signedOut", }, ], @@ -89,13 +96,13 @@ const userMachine = id: "signOut", onDone: [ { - actions: "unassignMe", + actions: ["unassignMe", "clearAuthError"], target: "#userState.signedOut", }, ], onError: [ { - actions: "assignError", + actions: "assignAuthError", target: "#userState.signedIn", }, ], @@ -122,11 +129,22 @@ const userMachine = ...context, me: undefined, })), - assignError: assign({ - error: (_, event) => event.data, - }) + assignGetUserError: assign({ + getUserError: (_, event) => event.data, + }), + clearGetUserError: assign((context: UserContext) => ({ + ...context, + getUserError: undefined + })), + assignAuthError: assign({ + authError: (_, event) => event.data, + }), + clearAuthError: assign((context: UserContext) => ({ + ...context, + authError: undefined + })) }, }, ) -export const userService = interpret(userMachine).start() +export const userXService = interpret(userMachine, { devTools: true }).start() From 1e07da742e26352590604b86368e1f26a2b931ad Mon Sep 17 00:00:00 2001 From: Presley Date: Wed, 16 Mar 2022 02:24:07 +0000 Subject: [PATCH 09/42] Glue/visual component separation --- site/components/SignIn/SignInForm.stories.tsx | 30 ++- site/components/SignIn/SignInForm.tsx | 45 +++-- site/pages/login.tsx | 31 ++- .../[organization]/[project]/index.tsx | 2 +- site/pages/projects/index.tsx | 2 +- site/xServices/user/userXService.ts | 182 +++++++++--------- 6 files changed, 168 insertions(+), 124 deletions(-) diff --git a/site/components/SignIn/SignInForm.stories.tsx b/site/components/SignIn/SignInForm.stories.tsx index d9e16692414b2..d0ad5609aa877 100644 --- a/site/components/SignIn/SignInForm.stories.tsx +++ b/site/components/SignIn/SignInForm.stories.tsx @@ -1,21 +1,37 @@ import { Story } from "@storybook/react" import React from "react" import { BrowserRouter } from "react-router-dom" -import { SignInForm } from "./SignInForm" +import { SignInForm, SignInFormProps } from "./SignInForm" export default { title: "SignIn/SignInForm", component: SignInForm, argTypes: { - loginHandler: { action: "Login" }, + isSignedIn: "boolean", + isLoading: "boolean", + redirectTo: "string", + authErrorMessage: "string", + onSubmit: { action: "Submit" }, }, } -const Template: Story = (args) => ( - +const Template: Story = (args: SignInFormProps) => ( - ) -export const Example = Template.bind({}) -Example.args = {} +export const SignedOut = Template.bind({}) +SignedOut.args = { + isSignedIn: false, + isLoading: false, + redirectTo: "/projects", + authErrorMessage: undefined, + onSubmit: ({ email, password }) => { + return Promise.resolve() + }, +} + +export const Loading = Template.bind({}) +Loading.args = { ...SignedOut.args, isLoading: true } + +export const WithError = Template.bind({}) +WithError.args = { ...SignedOut.args, authErrorMessage: "Email or password was invalid" } \ No newline at end of file diff --git a/site/components/SignIn/SignInForm.tsx b/site/components/SignIn/SignInForm.tsx index 9853bdc807d11..a73ba3357234f 100644 --- a/site/components/SignIn/SignInForm.tsx +++ b/site/components/SignIn/SignInForm.tsx @@ -1,15 +1,13 @@ import { makeStyles } from "@material-ui/core/styles" import { FormikContextType, useFormik } from "formik" -import { Location } from "history" -import { useLocation, Navigate } from "react-router-dom" +import { Navigate } from "react-router-dom" import React from "react" import * as Yup from "yup" import { Welcome } from "./Welcome" import { FormTextField } from "../Form" +import FormHelperText from '@material-ui/core/FormHelperText'; import { LoadingButton } from "./../Button" -import { userXService } from "../../xServices/user/userXService" -import { useActor } from "@xstate/react" /** * BuiltInAuthFormValues describes a form using built-in (email/password) @@ -40,11 +38,22 @@ const useStyles = makeStyles((theme) => ({ }, })) -export const SignInForm: React.FC = () => { - const location = useLocation() +export interface SignInFormProps { + isSignedIn: boolean + isLoading: boolean + authErrorMessage?: string + redirectTo: string + onSubmit: ({ email, password }: { email: string; password: string }) => Promise +} + +export const SignInForm: React.FC = ({ + isSignedIn, + isLoading, + authErrorMessage, + redirectTo, + onSubmit, +}) => { const styles = useStyles() - const [userState, userSend] = useActor(userXService) - const { authError } = userState.context const form: FormikContextType = useFormik({ initialValues: { @@ -52,13 +61,11 @@ export const SignInForm: React.FC = () => { password: "", }, validationSchema, - onSubmit: async ({ email, password }) => { - userSend({ type: 'SIGN_IN', email, password }) - }, + onSubmit, }) - if (userState.matches('signedIn')) { - return + if (isSignedIn) { + return } return ( @@ -92,13 +99,13 @@ export const SignInForm: React.FC = () => { isPassword placeholder="Password" variant="outlined" - helperText={authError ? (authError as Error).message : ''} /> + {authErrorMessage && {authErrorMessage}}
{ ) } - -const getRedirectFromLocation = (location: Location) => { - const defaultRedirect = "/" - - const searchParams = new URLSearchParams(location.search) - const redirect = searchParams.get("redirect") - return redirect ? redirect : defaultRedirect -} diff --git a/site/pages/login.tsx b/site/pages/login.tsx index eb404f04f5a01..9daf5aaddf99e 100644 --- a/site/pages/login.tsx +++ b/site/pages/login.tsx @@ -1,6 +1,10 @@ import { makeStyles } from "@material-ui/core/styles" +import { useActor } from "@xstate/react" import React from "react" +import { userXService } from "../xServices/user/userXService" import { SignInForm } from "./../components/SignIn" +import { useLocation } from "react-router-dom" +import { Location } from "history" export const useStyles = makeStyles((theme) => ({ root: { @@ -16,12 +20,37 @@ export const useStyles = makeStyles((theme) => ({ }, })) +const getRedirectFromLocation = (location: Location) => { + const defaultRedirect = "/" + + const searchParams = new URLSearchParams(location.search) + const redirect = searchParams.get("redirect") + return redirect ? redirect : defaultRedirect +} + export const SignInPage: React.FC = () => { const styles = useStyles() + const location = useLocation() + const [userState, userSend] = useActor(userXService) + const isSignedIn = userState.matches("signedIn") + const isLoading = userState.hasTag("loading") + const redirectTo = getRedirectFromLocation(location) + const authErrorMessage = userState.context.authError ? (userState.context.authError as Error).message : undefined + + const onSubmit = async ({ email, password }: { email: string; password: string }) => { + userSend({ type: "SIGN_IN", email, password }) + } + return (
- +
) diff --git a/site/pages/projects/[organization]/[project]/index.tsx b/site/pages/projects/[organization]/[project]/index.tsx index cea1127e035ed..ecce05f281f2b 100644 --- a/site/pages/projects/[organization]/[project]/index.tsx +++ b/site/pages/projects/[organization]/[project]/index.tsx @@ -91,7 +91,7 @@ export const ProjectPage: React.FC = () => { return (
- userSend('SIGN_OUT')} /> + userSend("SIGN_OUT")} />
{ orgs ? `/api/v2/organizations/${orgs[0].id}/projects` : null, ) - if (userState.matches('signedOut')) { + if (userState.matches("signedOut")) { return } diff --git a/site/xServices/user/userXService.ts b/site/xServices/user/userXService.ts index 173e4bf0b419c..f585f3875922a 100644 --- a/site/xServices/user/userXService.ts +++ b/site/xServices/user/userXService.ts @@ -14,107 +14,107 @@ const userMachine = /** @xstate-layout N4IgpgJg5mDOIC5QFdZgE4GUAuBDbYAdLAJZQB2kA8stgMSYCSA4gHID6jrioADgPalsJfuR4gAHogDM0gJyEATAHYAbNIAscxdunKArKo0AaEAE9EG+YX0BGABz3FD6atsbbc5QF9vp1Bg4+ESkFCTkUIzkdBCiROEAbvwA1iFk5FHiAkIiYkiSiHIADITSOvbSRRqq2m7StqYWCIr2+ja2tvVV+vqaRVW+-mhYeATE6eGR0Rjo-OiEvAA2+ABmcwC24xSZ+dkkwqLiUghlyoRF+opayq4X+sqK+o2Il6qEcq5y+hrfjt+qgxAARGwUIMGwwgiAFVhjE4oREikiOCALJgLKCfa5I4yIyEZTFZQaezFK5lPTPBC2IqKUqKC4EjqqKrFOSA4FBMbgyFQGEYOgzOYLZbYNboTao9G7TEHPKgY7SewlTQ3dRyVQ6GpVSmKMqEIz2ZRKjrKIqqJzs4actIUSBRBgsDhUKEAFQxOUO+WOhvshA0ahualsqnu8kpthUhGplVUIa+rUq9ktgVGNvIkxo9FilAR5CSqS25Ez7qxnvliCMGlK1Tk6vN5p+T3ML0Ub2U7iKXmcGg8tmTILGoXTEUzAvQs3mS1WG0LxelHrlBQQIbasc7aiKtnbV3sOpaUZXBjkwd1A0B5H4EDg4g5qcL1FoJdlOIQdlpH2qhmJzJ+DWbCB7WxSmUIl2wJM0CSTPwgStO8h0mHY+BlbEvReICaSMbQflkU0fkpHtfS3MC3C+aR9ENfR+2tMEwAhSY+XQJ8UPLACziVS5TXKAxPDI8MaSjIojSqPQezNRUqLg9I7UXPZn1Q5dqn1CMNEEi4HAecNIyVGoIweVQDH0tloNvUF4JHR951LRdvQcc4PCcAx12-fDOnOeRTU7C4SXsPtjNg4ImLLJddUIdiVBpOQKJ4psmh0ASQOPRUiI8RRfF8IA */ createMachine( { - tsTypes: {} as import("./userXService.typegen").Typegen0, - schema: { - context: {} as UserContext, - events: {} as UserEvent, - services: {} as { - getMe: { - data: API.UserResponse - } - signIn: { - data: API.LoginResponse | undefined - } - signOut: { - data: void - } - }, - }, - context: { - me: undefined, - getUserError: undefined, - authError: undefined - }, - id: "userState", - initial: "gettingUser", - states: { - signedOut: { - on: { - SIGN_IN: { - target: "#userState.signingIn", + tsTypes: {} as import("./userXService.typegen").Typegen0, + schema: { + context: {} as UserContext, + events: {} as UserEvent, + services: {} as { + getMe: { + data: API.UserResponse + } + signIn: { + data: API.LoginResponse | undefined + } + signOut: { + data: void + } }, }, - }, - signingIn: { - invoke: { - src: "signIn", - id: "signIn", - onDone: [ - { - target: "#userState.gettingUser", - actions: "clearAuthError" - }, - ], - onError: [ - { - actions: "assignAuthError", - target: "#userState.signedOut", - }, - ], + context: { + me: undefined, + getUserError: undefined, + authError: undefined, }, - tags: "loading", - }, - gettingUser: { - invoke: { - src: "getMe", - id: "getMe", - onDone: [ - { - actions: ["assignMe", "clearGetUserError"], - target: "#userState.signedIn", + id: "userState", + initial: "gettingUser", + states: { + signedOut: { + on: { + SIGN_IN: { + target: "#userState.signingIn", + }, + }, + }, + signingIn: { + invoke: { + src: "signIn", + id: "signIn", + onDone: [ + { + target: "#userState.gettingUser", + actions: "clearAuthError", + }, + ], + onError: [ + { + actions: "assignAuthError", + target: "#userState.signedOut", + }, + ], }, - ], - onError: [ - { - actions: "assignGetUserError", - target: "#userState.signedOut", + tags: "loading", + }, + gettingUser: { + invoke: { + src: "getMe", + id: "getMe", + onDone: [ + { + actions: ["assignMe", "clearGetUserError"], + target: "#userState.signedIn", + }, + ], + onError: [ + { + actions: "assignGetUserError", + target: "#userState.signedOut", + }, + ], }, - ], - }, - tags: "loading", - }, - signedIn: { - on: { - SIGN_OUT: { - target: "#userState.signingOut", + tags: "loading", }, - }, - }, - signingOut: { - invoke: { - src: "signOut", - id: "signOut", - onDone: [ - { - actions: ["unassignMe", "clearAuthError"], - target: "#userState.signedOut", + signedIn: { + on: { + SIGN_OUT: { + target: "#userState.signingOut", + }, }, - ], - onError: [ - { - actions: "assignAuthError", - target: "#userState.signedIn", + }, + signingOut: { + invoke: { + src: "signOut", + id: "signOut", + onDone: [ + { + actions: ["unassignMe", "clearAuthError"], + target: "#userState.signedOut", + }, + ], + onError: [ + { + actions: "assignAuthError", + target: "#userState.signedIn", + }, + ], }, - ], + tags: "loading", + }, }, - tags: "loading", }, - }, -}, { services: { signIn: async (_, event: UserEvent) => { - if (event.type === 'SIGN_IN') { + if (event.type === "SIGN_IN") { return await API.login(event.email, event.password) } }, @@ -134,15 +134,15 @@ const userMachine = }), clearGetUserError: assign((context: UserContext) => ({ ...context, - getUserError: undefined + getUserError: undefined, })), assignAuthError: assign({ authError: (_, event) => event.data, }), clearAuthError: assign((context: UserContext) => ({ ...context, - authError: undefined - })) + authError: undefined, + })), }, }, ) From 16692da6201baf686244192ec23f6c7e82b53c7b Mon Sep 17 00:00:00 2001 From: Presley Date: Wed, 16 Mar 2022 14:50:14 +0000 Subject: [PATCH 10/42] Fix dependency merge --- site/package.json | 12 +++++------- yarn.lock | 4 ++++ 2 files changed, 9 insertions(+), 7 deletions(-) create mode 100644 yarn.lock diff --git a/site/package.json b/site/package.json index 412eda4621a13..8f83ff3211ff7 100644 --- a/site/package.json +++ b/site/package.json @@ -30,7 +30,10 @@ "react-dom": "17.0.2", "react-router-dom": "6.2.2", "swr": "1.2.2", - "yup": "0.32.11" + "yup": "0.32.11", + "@xstate/cli": "^0.1.4", + "@xstate/react": "^2.0.1", + "xstate": "^4.30.6" }, "devDependencies": { "@playwright/test": "1.20.0", @@ -82,10 +85,5 @@ "firefox 63", "edge 79", "safari 13.1" - ], - "dependencies": { - "@xstate/cli": "^0.1.4", - "@xstate/react": "^2.0.1", - "xstate": "^4.30.6" - } + ] } diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000000000..fb57ccd13afbd --- /dev/null +++ b/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + From d1017f019a1d414dd7e4d5eb618d952d15417bf1 Mon Sep 17 00:00:00 2001 From: Presley Date: Wed, 16 Mar 2022 15:06:09 +0000 Subject: [PATCH 11/42] Lint --- site/components/SignIn/SignInForm.stories.tsx | 6 ++---- site/components/SignIn/SignInForm.tsx | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/site/components/SignIn/SignInForm.stories.tsx b/site/components/SignIn/SignInForm.stories.tsx index d0ad5609aa877..f1404e3aa8199 100644 --- a/site/components/SignIn/SignInForm.stories.tsx +++ b/site/components/SignIn/SignInForm.stories.tsx @@ -15,9 +15,7 @@ export default { }, } -const Template: Story = (args: SignInFormProps) => ( - -) +const Template: Story = (args: SignInFormProps) => export const SignedOut = Template.bind({}) SignedOut.args = { @@ -34,4 +32,4 @@ export const Loading = Template.bind({}) Loading.args = { ...SignedOut.args, isLoading: true } export const WithError = Template.bind({}) -WithError.args = { ...SignedOut.args, authErrorMessage: "Email or password was invalid" } \ No newline at end of file +WithError.args = { ...SignedOut.args, authErrorMessage: "Email or password was invalid" } diff --git a/site/components/SignIn/SignInForm.tsx b/site/components/SignIn/SignInForm.tsx index a73ba3357234f..a8835552e6938 100644 --- a/site/components/SignIn/SignInForm.tsx +++ b/site/components/SignIn/SignInForm.tsx @@ -6,7 +6,7 @@ import * as Yup from "yup" import { Welcome } from "./Welcome" import { FormTextField } from "../Form" -import FormHelperText from '@material-ui/core/FormHelperText'; +import FormHelperText from "@material-ui/core/FormHelperText" import { LoadingButton } from "./../Button" /** From 85779b9063a58871c5fed0d7759e2ec88d90de8a Mon Sep 17 00:00:00 2001 From: Presley Date: Wed, 16 Mar 2022 15:08:24 +0000 Subject: [PATCH 12/42] Remove UserContext --- site/app.tsx | 3 - site/components/Navbar/UserDropdown.tsx | 4 +- site/components/Navbar/index.tsx | 4 +- site/components/User/UserAvatar.tsx | 4 +- site/components/User/UserProfileCard.tsx | 4 +- site/contexts/UserContext.test.tsx | 94 ------------------------ site/contexts/UserContext.tsx | 73 ------------------ site/test_helpers/mocks.ts | 5 +- 8 files changed, 10 insertions(+), 181 deletions(-) delete mode 100644 site/contexts/UserContext.test.tsx delete mode 100644 site/contexts/UserContext.tsx diff --git a/site/app.tsx b/site/app.tsx index 21e9c70cf7b33..5d9266660f1ad 100644 --- a/site/app.tsx +++ b/site/app.tsx @@ -2,7 +2,6 @@ import React from "react" import CssBaseline from "@material-ui/core/CssBaseline" import ThemeProvider from "@material-ui/styles/ThemeProvider" import { SWRConfig } from "swr" -import { UserProvider } from "./contexts/UserContext" import { light } from "./theme" import { BrowserRouter as Router, Route, Routes } from "react-router-dom" @@ -39,7 +38,6 @@ export const App: React.FC = () => { }, }} > - @@ -69,7 +67,6 @@ export const App: React.FC = () => { - ) diff --git a/site/components/Navbar/UserDropdown.tsx b/site/components/Navbar/UserDropdown.tsx index 164d5822651df..012f7a9724968 100644 --- a/site/components/Navbar/UserDropdown.tsx +++ b/site/components/Navbar/UserDropdown.tsx @@ -11,11 +11,11 @@ import { LogoutIcon } from "../Icons" import { BorderedMenu } from "./BorderedMenu" import { UserProfileCard } from "../User/UserProfileCard" -import { User } from "../../contexts/UserContext" import { UserAvatar } from "../User" +import { UserResponse } from "../../api" export interface UserDropdownProps { - user: User + user: UserResponse onSignOut: () => void } diff --git a/site/components/Navbar/index.tsx b/site/components/Navbar/index.tsx index cc02405a9b3cd..da43d112c993c 100644 --- a/site/components/Navbar/index.tsx +++ b/site/components/Navbar/index.tsx @@ -3,12 +3,12 @@ import Button from "@material-ui/core/Button" import { makeStyles } from "@material-ui/core/styles" import { Link } from "react-router-dom" -import { User } from "../../contexts/UserContext" import { Logo } from "../Icons" import { UserDropdown } from "./UserDropdown" +import { UserResponse } from "../../api" export interface NavbarProps { - user?: User + user?: UserResponse onSignOut: () => void } diff --git a/site/components/User/UserAvatar.tsx b/site/components/User/UserAvatar.tsx index 020e6eb063ad5..b90db25f0bb01 100644 --- a/site/components/User/UserAvatar.tsx +++ b/site/components/User/UserAvatar.tsx @@ -1,9 +1,9 @@ import Avatar from "@material-ui/core/Avatar" import React from "react" -import { User } from "../../contexts/UserContext" +import { UserResponse } from "../../api" export interface UserAvatarProps { - user: User + user: UserResponse className?: string } diff --git a/site/components/User/UserProfileCard.tsx b/site/components/User/UserProfileCard.tsx index b3cc1ec3deeb9..1a013161bf6bb 100644 --- a/site/components/User/UserProfileCard.tsx +++ b/site/components/User/UserProfileCard.tsx @@ -1,12 +1,12 @@ import { makeStyles } from "@material-ui/core/styles" import Typography from "@material-ui/core/Typography" import React from "react" +import { UserResponse } from "../../api" -import { User } from "../../contexts/UserContext" import { UserAvatar } from "./UserAvatar" interface UserProfileCardProps { - user: User + user: UserResponse } export const UserProfileCard: React.FC = ({ user }) => { diff --git a/site/contexts/UserContext.test.tsx b/site/contexts/UserContext.test.tsx deleted file mode 100644 index 58af022aa0fd1..0000000000000 --- a/site/contexts/UserContext.test.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import React from "react" -import { SWRConfig } from "swr" -import { screen, waitFor } from "@testing-library/react" -import { User, UserProvider, useUser } from "./UserContext" -import { history, MockUser, render } from "../test_helpers" - -namespace Helpers { - // Helper component that renders out the state of the `useUser` hook. - // It just renders simple text in the 'error', 'me', and 'loading' states, - // so that the test can get a peak at the state of the hook. - const TestComponent: React.FC<{ redirectOnFailure: boolean }> = ({ redirectOnFailure }) => { - const { me, error } = useUser(redirectOnFailure) - - if (error) { - return
{`Error: ${error.toString()}`}
- } - if (me) { - return
{`Me: ${me.toString()}`}
- } - - return
Loading
- } - - // Helper to render a userContext, and all the scaffolding needed - // (an SWRConfig as well as a UserPRovider) - export const renderUserContext = ( - simulatedRequest: () => Promise, - redirectOnFailure: boolean, - ): React.ReactElement => { - return ( - // Set up an SWRConfig that works for testing - we'll simulate a request, - // and set up the cache to reset every test. - new Map(), - }} - > - - - - - ) - } -} - -describe("UserContext", () => { - const failingRequest = () => Promise.reject("Failed to load user") - const successfulRequest = () => Promise.resolve(MockUser) - - // Reset the router to '/' before every test - beforeEach(() => { - history.replace("/") - }) - - it("shouldn't redirect if user fails to load and redirectOnFailure is false", async () => { - // When - render(Helpers.renderUserContext(failingRequest, false)) - - // Then - // Verify we get an error message - await waitFor(() => { - expect(screen.queryByText("Error:", { exact: false })).toBeDefined() - }) - // ...and the route should be unchanged - expect(history.location.pathname).toEqual("/") - expect(history.location.search).toEqual("") - }) - - it("should redirect if user fails to load and redirectOnFailure is true", async () => { - // When - render(Helpers.renderUserContext(failingRequest, true)) - - // Then - // Verify we route to the login page - await waitFor(() => expect(history.location.pathname).toEqual("/login")) - await waitFor(() => expect(history.location.search).toEqual("?redirect=%2F")) - }) - - it("should not redirect if user loads and redirectOnFailure is true", async () => { - // When - render(Helpers.renderUserContext(successfulRequest, true)) - - // Then - // Verify the user is rendered - await waitFor(() => { - expect(screen.queryByText("Me:", { exact: false })).toBeDefined() - }) - // ...and the route should be unchanged - expect(history.location.pathname).toEqual("/") - expect(history.location.search).toEqual("") - }) -}) diff --git a/site/contexts/UserContext.tsx b/site/contexts/UserContext.tsx deleted file mode 100644 index 459af26595b56..0000000000000 --- a/site/contexts/UserContext.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { useLocation, useNavigate } from "react-router-dom" -import React, { useContext, useEffect } from "react" -import useSWR from "swr" - -import * as API from "../api" - -export interface User { - readonly id: string - readonly username: string - readonly email: string - readonly created_at: string -} - -export interface UserContext { - readonly error?: Error - readonly me?: User - readonly signOut: () => Promise -} - -const UserContext = React.createContext({ - signOut: () => { - return Promise.reject("Sign out API not available") - }, -}) - -export const useUser = (redirectOnError = false): UserContext => { - const ctx = useContext(UserContext) - const navigate = useNavigate() - const { pathname } = useLocation() - - const requestError = ctx.error - useEffect(() => { - if (redirectOnError && requestError) { - navigate({ - pathname: "/login", - search: "?redirect=" + encodeURIComponent(pathname), - }) - } - // Disabling exhaustive deps here because it can cause an - // infinite useEffect loop. Should (hopefully) go away - // when we switch to an alternate routing strategy. - }, [redirectOnError, requestError]) // eslint-disable-line react-hooks/exhaustive-deps - - return ctx -} - -export const UserProvider: React.FC = (props) => { - const navigate = useNavigate() - const location = useLocation() - const { data, error, mutate } = useSWR("/api/v2/users/me") - - const signOut = async () => { - await API.logout() - // Tell SWR to invalidate the cache for the user endpoint - await mutate("/api/v2/users/me") - navigate({ - pathname: "/login", - search: "?redirect=" + encodeURIComponent(location.pathname), - }) - } - - return ( - - {props.children} - - ) -} diff --git a/site/test_helpers/mocks.ts b/site/test_helpers/mocks.ts index 081292b2d663e..787258a962c96 100644 --- a/site/test_helpers/mocks.ts +++ b/site/test_helpers/mocks.ts @@ -1,7 +1,6 @@ -import { User } from "../contexts/UserContext" -import { Provisioner, Organization, Project, Workspace } from "../api" +import { Provisioner, Organization, Project, Workspace, UserResponse } from "../api" -export const MockUser: User = { +export const MockUser: UserResponse = { id: "test-user-id", username: "TestUser", email: "test@coder.com", From f82b0991e5894d83d833f610622081f700a3f09b Mon Sep 17 00:00:00 2001 From: Presley Date: Wed, 16 Mar 2022 16:29:21 +0000 Subject: [PATCH 13/42] Remove inspector --- site/app.tsx | 3 --- site/package.json | 8 ++++---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/site/app.tsx b/site/app.tsx index 5d9266660f1ad..6e84d76a73b51 100644 --- a/site/app.tsx +++ b/site/app.tsx @@ -13,9 +13,6 @@ import { ProjectsPage } from "./pages/projects" import { ProjectPage } from "./pages/projects/[organization]/[project]" import { CreateWorkspacePage } from "./pages/projects/[organization]/[project]/create" import { WorkspacePage } from "./pages/workspaces/[workspace]" -import { Interpreter } from "xstate/lib/interpreter" - -Interpreter.defaultOptions.devTools = process.env.NODE_ENV === "development" export const App: React.FC = () => { return ( diff --git a/site/package.json b/site/package.json index 8f83ff3211ff7..bd6d179d3a6db 100644 --- a/site/package.json +++ b/site/package.json @@ -24,16 +24,16 @@ "@material-ui/core": "4.9.4", "@material-ui/icons": "4.5.1", "@material-ui/lab": "4.0.0-alpha.42", + "@xstate/cli": "^0.1.4", + "@xstate/react": "^2.0.1", "formik": "2.2.9", "history": "5.3.0", "react": "17.0.2", "react-dom": "17.0.2", "react-router-dom": "6.2.2", "swr": "1.2.2", - "yup": "0.32.11", - "@xstate/cli": "^0.1.4", - "@xstate/react": "^2.0.1", - "xstate": "^4.30.6" + "xstate": "^4.30.6", + "yup": "0.32.11" }, "devDependencies": { "@playwright/test": "1.20.0", From d4d79ea7c701f58061b73c9331bf63a232512c3f Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Wed, 16 Mar 2022 16:31:33 +0000 Subject: [PATCH 14/42] Add typegen command to site/out --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 5f7d5fb9b21d1..c0e7551ad60c3 100644 --- a/Makefile +++ b/Makefile @@ -84,6 +84,7 @@ provisionersdk/proto: provisionersdk/proto/provisioner.proto site/out: ./scripts/yarn_install.sh + cd site && yarn typegen cd site && yarn build # Restores GITKEEP files! git checkout HEAD site/out From 17db4a9b5fa229c534b24fad54cb8a548dca5da5 Mon Sep 17 00:00:00 2001 From: Presley Date: Wed, 16 Mar 2022 16:56:32 +0000 Subject: [PATCH 15/42] Fix index page redirects --- site/pages/index.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/site/pages/index.tsx b/site/pages/index.tsx index 6d26587c17056..b11cd8ae06c80 100644 --- a/site/pages/index.tsx +++ b/site/pages/index.tsx @@ -7,12 +7,13 @@ import { userXService } from "../xServices/user/userXService" export const IndexPage: React.FC = () => { const [userState] = useActor(userXService) - const { me } = userState.context - if (me) { - // Once the user is logged in, just redirect them to /projects as the landing page + if (userState.matches('signedIn')) { return + } else if (userState.matches('signedOut')) { + return + } else { + return } - return } From 8d3f0519987ed244ed7619bb3e97ddf123f72526 Mon Sep 17 00:00:00 2001 From: Presley Date: Wed, 16 Mar 2022 18:49:45 +0000 Subject: [PATCH 16/42] DRY up nav and redirects --- site/app.tsx | 78 ++++++++++++++----- site/components/Navbar/index.tsx | 12 ++- site/components/RequireAuth/RequireAuth.tsx | 32 ++++++++ site/components/SignIn/SignInForm.stories.tsx | 7 +- site/components/SignIn/SignInForm.tsx | 15 +--- site/pages/index.tsx | 14 +--- site/pages/login.tsx | 25 +++--- .../[organization]/[project]/create.tsx | 6 +- .../[organization]/[project]/index.tsx | 8 +- site/pages/projects/index.tsx | 15 +--- site/pages/workspaces/[workspace].tsx | 9 +-- site/xServices/user/userXService.ts | 3 - 12 files changed, 119 insertions(+), 105 deletions(-) create mode 100644 site/components/RequireAuth/RequireAuth.tsx diff --git a/site/app.tsx b/site/app.tsx index 6e84d76a73b51..5df79e0cf3163 100644 --- a/site/app.tsx +++ b/site/app.tsx @@ -13,6 +13,7 @@ import { ProjectsPage } from "./pages/projects" import { ProjectPage } from "./pages/projects/[organization]/[project]" import { CreateWorkspacePage } from "./pages/projects/[organization]/[project]/create" import { WorkspacePage } from "./pages/workspaces/[workspace]" +import { AuthAndNav, RequireAuth } from "./components/RequireAuth/RequireAuth" export const App: React.FC = () => { return ( @@ -35,35 +36,70 @@ export const App: React.FC = () => { }, }} > - - + + - - - } /> + + + + + + } + /> - } /> - } /> + } /> + } /> - - } /> - - } /> - } /> - + + + + + } + /> + + + + + } + /> + + + + } + /> + - - } /> - + + + + + } + /> + - {/* Using path="*"" means "match anything", so this route + {/* Using path="*"" means "match anything", so this route acts like a catch-all for URLs that we don't have explicit routes for. */} - } /> - - - + } /> + + + ) diff --git a/site/components/Navbar/index.tsx b/site/components/Navbar/index.tsx index da43d112c993c..110e9ae77e09f 100644 --- a/site/components/Navbar/index.tsx +++ b/site/components/Navbar/index.tsx @@ -6,13 +6,23 @@ import { Link } from "react-router-dom" import { Logo } from "../Icons" import { UserDropdown } from "./UserDropdown" import { UserResponse } from "../../api" +import { useActor } from "@xstate/react" +import { userXService } from "../../xServices/user/userXService" export interface NavbarProps { user?: UserResponse onSignOut: () => void } -export const Navbar: React.FC = ({ user, onSignOut }) => { +export const Navbar: React.FC = () => { + const [userState, userSend] = useActor(userXService) + const { me } = userState.context + const onSignOut = () => userSend("SIGN_OUT") + + return +} + +export const NavbarView: React.FC = ({ user, onSignOut }) => { const styles = useStyles() return (
diff --git a/site/components/RequireAuth/RequireAuth.tsx b/site/components/RequireAuth/RequireAuth.tsx new file mode 100644 index 0000000000000..5ad1c5ec61c24 --- /dev/null +++ b/site/components/RequireAuth/RequireAuth.tsx @@ -0,0 +1,32 @@ +import { useActor } from "@xstate/react" +import React from "react" +import { Navigate, useLocation } from "react-router" +import { userXService } from "../../xServices/user/userXService" +import { FullScreenLoader } from "../Loader/FullScreenLoader" +import { Navbar } from "../Navbar" + +interface RequireAuthProps { + children: JSX.Element +} + +export const RequireAuth: React.FC = ({ children }) => { + const [userState] = useActor(userXService) + const location = useLocation() + + if (userState.matches("signedOut") || !userState.context.me) { + return + } else if (userState.hasTag("loading")) { + return + } else { + return children + } +} + +export const AuthAndNav: React.FC = ({ children }) => ( + + <> + + {children} + + +) diff --git a/site/components/SignIn/SignInForm.stories.tsx b/site/components/SignIn/SignInForm.stories.tsx index f1404e3aa8199..caac9bba5a230 100644 --- a/site/components/SignIn/SignInForm.stories.tsx +++ b/site/components/SignIn/SignInForm.stories.tsx @@ -1,15 +1,12 @@ import { Story } from "@storybook/react" import React from "react" -import { BrowserRouter } from "react-router-dom" import { SignInForm, SignInFormProps } from "./SignInForm" export default { title: "SignIn/SignInForm", component: SignInForm, argTypes: { - isSignedIn: "boolean", isLoading: "boolean", - redirectTo: "string", authErrorMessage: "string", onSubmit: { action: "Submit" }, }, @@ -19,11 +16,9 @@ const Template: Story = (args: SignInFormProps) => { + onSubmit: () => { return Promise.resolve() }, } diff --git a/site/components/SignIn/SignInForm.tsx b/site/components/SignIn/SignInForm.tsx index a8835552e6938..9b3db2ddff8c8 100644 --- a/site/components/SignIn/SignInForm.tsx +++ b/site/components/SignIn/SignInForm.tsx @@ -1,6 +1,5 @@ import { makeStyles } from "@material-ui/core/styles" import { FormikContextType, useFormik } from "formik" -import { Navigate } from "react-router-dom" import React from "react" import * as Yup from "yup" @@ -39,20 +38,12 @@ const useStyles = makeStyles((theme) => ({ })) export interface SignInFormProps { - isSignedIn: boolean isLoading: boolean authErrorMessage?: string - redirectTo: string onSubmit: ({ email, password }: { email: string; password: string }) => Promise } -export const SignInForm: React.FC = ({ - isSignedIn, - isLoading, - authErrorMessage, - redirectTo, - onSubmit, -}) => { +export const SignInForm: React.FC = ({ isLoading, authErrorMessage, onSubmit }) => { const styles = useStyles() const form: FormikContextType = useFormik({ @@ -64,10 +55,6 @@ export const SignInForm: React.FC = ({ onSubmit, }) - if (isSignedIn) { - return - } - return ( <> diff --git a/site/pages/index.tsx b/site/pages/index.tsx index b11cd8ae06c80..970d010d60676 100644 --- a/site/pages/index.tsx +++ b/site/pages/index.tsx @@ -1,19 +1,7 @@ -import { useActor } from "@xstate/react" import React from "react" import { Navigate } from "react-router-dom" -import { FullScreenLoader } from "../components/Loader/FullScreenLoader" -import { userXService } from "../xServices/user/userXService" export const IndexPage: React.FC = () => { - const [userState] = useActor(userXService) - - if (userState.matches('signedIn')) { - return - } else if (userState.matches('signedOut')) { - return - } else { - return - } - + return } diff --git a/site/pages/login.tsx b/site/pages/login.tsx index 9daf5aaddf99e..61a8e601c12a8 100644 --- a/site/pages/login.tsx +++ b/site/pages/login.tsx @@ -3,7 +3,7 @@ import { useActor } from "@xstate/react" import React from "react" import { userXService } from "../xServices/user/userXService" import { SignInForm } from "./../components/SignIn" -import { useLocation } from "react-router-dom" +import { Navigate, useLocation } from "react-router-dom" import { Location } from "history" export const useStyles = makeStyles((theme) => ({ @@ -32,7 +32,6 @@ export const SignInPage: React.FC = () => { const styles = useStyles() const location = useLocation() const [userState, userSend] = useActor(userXService) - const isSignedIn = userState.matches("signedIn") const isLoading = userState.hasTag("loading") const redirectTo = getRedirectFromLocation(location) const authErrorMessage = userState.context.authError ? (userState.context.authError as Error).message : undefined @@ -41,17 +40,15 @@ export const SignInPage: React.FC = () => { userSend({ type: "SIGN_IN", email, password }) } - return ( -
-
- + if (userState.matches("signedIn")) { + return + } else { + return ( +
+
+ +
-
- ) + ) + } } diff --git a/site/pages/projects/[organization]/[project]/create.tsx b/site/pages/projects/[organization]/[project]/create.tsx index c1b2c8e18a413..84b8b1a6dd03d 100644 --- a/site/pages/projects/[organization]/[project]/create.tsx +++ b/site/pages/projects/[organization]/[project]/create.tsx @@ -8,15 +8,11 @@ import { ErrorSummary } from "../../../../components/ErrorSummary" import { FullScreenLoader } from "../../../../components/Loader/FullScreenLoader" import { CreateWorkspaceForm } from "../../../../forms/CreateWorkspaceForm" import { unsafeSWRArgument } from "../../../../util" -import { useActor } from "@xstate/react" -import { userXService } from "../../../../xServices/user/userXService" export const CreateWorkspacePage: React.FC = () => { const { organization: organizationName, project: projectName } = useParams() const navigate = useNavigate() const styles = useStyles() - const [userState] = useActor(userXService) - const { me } = userState.context const { data: organizationInfo, error: organizationError } = useSWR( () => `/api/v2/users/me/organizations/${organizationName}`, @@ -44,7 +40,7 @@ export const CreateWorkspacePage: React.FC = () => { return } - if (!me || !project) { + if (!project) { return } diff --git a/site/pages/projects/[organization]/[project]/index.tsx b/site/pages/projects/[organization]/[project]/index.tsx index ecce05f281f2b..c2e4b38480b6d 100644 --- a/site/pages/projects/[organization]/[project]/index.tsx +++ b/site/pages/projects/[organization]/[project]/index.tsx @@ -7,20 +7,15 @@ import useSWR from "swr" import { Organization, Project, Workspace } from "../../../../api" import { Header } from "../../../../components/Header" import { FullScreenLoader } from "../../../../components/Loader/FullScreenLoader" -import { Navbar } from "../../../../components/Navbar" import { Footer } from "../../../../components/Page" import { Column, Table } from "../../../../components/Table" import { ErrorSummary } from "../../../../components/ErrorSummary" import { firstOrItem } from "../../../../util/array" import { EmptyState } from "../../../../components/EmptyState" import { unsafeSWRArgument } from "../../../../util" -import { useActor } from "@xstate/react" -import { userXService } from "../../../../xServices/user/userXService" export const ProjectPage: React.FC = () => { const styles = useStyles() - const [userState, userSend] = useActor(userXService) - const { me } = userState.context const navigate = useNavigate() const { project: projectName, organization: organizationName } = useParams() @@ -49,7 +44,7 @@ export const ProjectPage: React.FC = () => { return } - if (!me || !projectInfo || !workspaces) { + if (!projectInfo || !workspaces) { return } @@ -91,7 +86,6 @@ export const ProjectPage: React.FC = () => { return (
- userSend("SIGN_OUT")} />
{ const styles = useStyles() - const location = useLocation() - const [userState, userSend] = useActor(userXService) - const { me } = userState.context const { data: orgs, error: orgsError } = useSWR("/api/v2/users/me/organizations") const { data: projects, error } = useSWR( orgs ? `/api/v2/organizations/${orgs[0].id}/projects` : null, ) - if (userState.matches("signedOut")) { - return - } - if (error) { return } @@ -38,7 +28,7 @@ export const ProjectsPage: React.FC = () => { return } - if (!me || !projects || !orgs) { + if (!projects || !orgs) { return } @@ -81,7 +71,6 @@ export const ProjectsPage: React.FC = () => { return (
- userSend("SIGN_OUT")} />
diff --git a/site/pages/workspaces/[workspace].tsx b/site/pages/workspaces/[workspace].tsx index 1f5b590171e5c..5753c9d040499 100644 --- a/site/pages/workspaces/[workspace].tsx +++ b/site/pages/workspaces/[workspace].tsx @@ -2,7 +2,6 @@ import React from "react" import useSWR from "swr" import { makeStyles } from "@material-ui/core/styles" import { useParams } from "react-router-dom" -import { Navbar } from "../../components/Navbar" import { Footer } from "../../components/Page" import { firstOrItem } from "../../util/array" import { ErrorSummary } from "../../components/ErrorSummary" @@ -10,14 +9,10 @@ import { FullScreenLoader } from "../../components/Loader/FullScreenLoader" import { Workspace } from "../../components/Workspace" import { unsafeSWRArgument } from "../../util" import * as API from "../../api" -import { useActor } from "@xstate/react" -import { userXService } from "../../xServices/user/userXService" export const WorkspacePage: React.FC = () => { const styles = useStyles() const { workspace: workspaceQueryParam } = useParams() - const [userState, userSend] = useActor(userXService) - const { me } = userState.context const { data: workspace, error: workspaceError } = useSWR(() => { const workspaceParam = firstOrItem(workspaceQueryParam, null) @@ -46,14 +41,12 @@ export const WorkspacePage: React.FC = () => { return } - if (!me || !workspace || !project || !organization) { + if (!workspace || !project || !organization) { return } return (
- userSend("SIGN_OUT")} /> -
diff --git a/site/xServices/user/userXService.ts b/site/xServices/user/userXService.ts index f585f3875922a..3e1d672129ab0 100644 --- a/site/xServices/user/userXService.ts +++ b/site/xServices/user/userXService.ts @@ -25,9 +25,6 @@ const userMachine = signIn: { data: API.LoginResponse | undefined } - signOut: { - data: void - } }, }, context: { From e5fd63514d5c378c5570cbc1c21f2c70baa6feb7 Mon Sep 17 00:00:00 2001 From: Presley Date: Wed, 16 Mar 2022 21:54:28 +0000 Subject: [PATCH 17/42] Moves based on merge --- site/src/app.tsx | 2 +- site/src/components/Navbar/index.tsx | 2 +- site/{ => src}/components/RequireAuth/RequireAuth.tsx | 2 +- site/src/pages/login.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename site/{ => src}/components/RequireAuth/RequireAuth.tsx (92%) diff --git a/site/src/app.tsx b/site/src/app.tsx index ebf38c85843e7..3c496ed9afbed 100644 --- a/site/src/app.tsx +++ b/site/src/app.tsx @@ -14,7 +14,7 @@ import { ProjectPage } from "./pages/projects/[organization]/[project]" import { CreateWorkspacePage } from "./pages/projects/[organization]/[project]/create" import { WorkspacePage } from "./pages/workspaces/[workspace]" import { HealthzPage } from "./pages/healthz" -import { AuthAndNav, RequireAuth } from "../components/RequireAuth/RequireAuth" +import { AuthAndNav, RequireAuth } from "./components/RequireAuth/RequireAuth" export const App: React.FC = () => { return ( diff --git a/site/src/components/Navbar/index.tsx b/site/src/components/Navbar/index.tsx index 110e9ae77e09f..0d359a58e43a3 100644 --- a/site/src/components/Navbar/index.tsx +++ b/site/src/components/Navbar/index.tsx @@ -7,7 +7,7 @@ import { Logo } from "../Icons" import { UserDropdown } from "./UserDropdown" import { UserResponse } from "../../api" import { useActor } from "@xstate/react" -import { userXService } from "../../xServices/user/userXService" +import { userXService } from "../../../xServices/user/userXService" export interface NavbarProps { user?: UserResponse diff --git a/site/components/RequireAuth/RequireAuth.tsx b/site/src/components/RequireAuth/RequireAuth.tsx similarity index 92% rename from site/components/RequireAuth/RequireAuth.tsx rename to site/src/components/RequireAuth/RequireAuth.tsx index 5ad1c5ec61c24..a477a22815edb 100644 --- a/site/components/RequireAuth/RequireAuth.tsx +++ b/site/src/components/RequireAuth/RequireAuth.tsx @@ -1,7 +1,7 @@ import { useActor } from "@xstate/react" import React from "react" import { Navigate, useLocation } from "react-router" -import { userXService } from "../../xServices/user/userXService" +import { userXService } from "../../../xServices/user/userXService" import { FullScreenLoader } from "../Loader/FullScreenLoader" import { Navbar } from "../Navbar" diff --git a/site/src/pages/login.tsx b/site/src/pages/login.tsx index 61a8e601c12a8..a13cd737b31e4 100644 --- a/site/src/pages/login.tsx +++ b/site/src/pages/login.tsx @@ -1,10 +1,10 @@ import { makeStyles } from "@material-ui/core/styles" import { useActor } from "@xstate/react" import React from "react" -import { userXService } from "../xServices/user/userXService" import { SignInForm } from "./../components/SignIn" import { Navigate, useLocation } from "react-router-dom" import { Location } from "history" +import { userXService } from "../../xServices/user/userXService" export const useStyles = makeStyles((theme) => ({ root: { From 9ef9dca443791c557094a5b1aefa50bd35de52a6 Mon Sep 17 00:00:00 2001 From: Presley Date: Wed, 16 Mar 2022 22:04:17 +0000 Subject: [PATCH 18/42] Moving Page helpers into Page dir --- site/src/app.tsx | 2 +- site/src/components/Page/AuthAndNav.tsx | 12 ++++++++++++ .../components/{RequireAuth => Page}/RequireAuth.tsx | 11 +---------- site/src/components/Page/index.tsx | 2 ++ 4 files changed, 16 insertions(+), 11 deletions(-) create mode 100644 site/src/components/Page/AuthAndNav.tsx rename site/src/components/{RequireAuth => Page}/RequireAuth.tsx (75%) diff --git a/site/src/app.tsx b/site/src/app.tsx index 3c496ed9afbed..c42d384728084 100644 --- a/site/src/app.tsx +++ b/site/src/app.tsx @@ -14,7 +14,7 @@ import { ProjectPage } from "./pages/projects/[organization]/[project]" import { CreateWorkspacePage } from "./pages/projects/[organization]/[project]/create" import { WorkspacePage } from "./pages/workspaces/[workspace]" import { HealthzPage } from "./pages/healthz" -import { AuthAndNav, RequireAuth } from "./components/RequireAuth/RequireAuth" +import { AuthAndNav, RequireAuth } from "./components/Page" export const App: React.FC = () => { return ( diff --git a/site/src/components/Page/AuthAndNav.tsx b/site/src/components/Page/AuthAndNav.tsx new file mode 100644 index 0000000000000..65e80e88e1d26 --- /dev/null +++ b/site/src/components/Page/AuthAndNav.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import { Navbar } from "../Navbar"; +import { RequireAuth, RequireAuthProps } from "./RequireAuth"; + +export const AuthAndNav: React.FC = ({ children }) => ( + + <> + + {children} + + +) \ No newline at end of file diff --git a/site/src/components/RequireAuth/RequireAuth.tsx b/site/src/components/Page/RequireAuth.tsx similarity index 75% rename from site/src/components/RequireAuth/RequireAuth.tsx rename to site/src/components/Page/RequireAuth.tsx index a477a22815edb..c4b569d5b6dab 100644 --- a/site/src/components/RequireAuth/RequireAuth.tsx +++ b/site/src/components/Page/RequireAuth.tsx @@ -3,9 +3,8 @@ import React from "react" import { Navigate, useLocation } from "react-router" import { userXService } from "../../../xServices/user/userXService" import { FullScreenLoader } from "../Loader/FullScreenLoader" -import { Navbar } from "../Navbar" -interface RequireAuthProps { +export interface RequireAuthProps { children: JSX.Element } @@ -22,11 +21,3 @@ export const RequireAuth: React.FC = ({ children }) => { } } -export const AuthAndNav: React.FC = ({ children }) => ( - - <> - - {children} - - -) diff --git a/site/src/components/Page/index.tsx b/site/src/components/Page/index.tsx index a29a2e5d3927b..8232f4db244eb 100644 --- a/site/src/components/Page/index.tsx +++ b/site/src/components/Page/index.tsx @@ -1 +1,3 @@ export * from "./Footer" +export * from "./RequireAuth" +export * from "./AuthAndNav" From 97d94de54d942b0ebb59659861e9754ba63f1b7b Mon Sep 17 00:00:00 2001 From: Presley Date: Wed, 16 Mar 2022 22:20:45 +0000 Subject: [PATCH 19/42] Move xservice into src, update script --- site/package.json | 2 +- site/src/components/Navbar/index.tsx | 2 +- site/src/components/Page/RequireAuth.tsx | 2 +- site/src/pages/login.tsx | 2 +- site/{ => src}/xServices/user/userXService.ts | 5 ++--- 5 files changed, 6 insertions(+), 7 deletions(-) rename site/{ => src}/xServices/user/userXService.ts (98%) diff --git a/site/package.json b/site/package.json index 2139033cf9930..2e8758b0ea695 100644 --- a/site/package.json +++ b/site/package.json @@ -18,7 +18,7 @@ "storybook:build": "build-storybook", "test": "jest --selectProjects test", "test:coverage": "jest --selectProjects test --collectCoverage", - "typegen": "xstate typegen '**/*.ts'" + "typegen": "xstate typegen 'src/**/*.ts'" }, "dependencies": { "@material-ui/core": "4.9.4", diff --git a/site/src/components/Navbar/index.tsx b/site/src/components/Navbar/index.tsx index 0d359a58e43a3..110e9ae77e09f 100644 --- a/site/src/components/Navbar/index.tsx +++ b/site/src/components/Navbar/index.tsx @@ -7,7 +7,7 @@ import { Logo } from "../Icons" import { UserDropdown } from "./UserDropdown" import { UserResponse } from "../../api" import { useActor } from "@xstate/react" -import { userXService } from "../../../xServices/user/userXService" +import { userXService } from "../../xServices/user/userXService" export interface NavbarProps { user?: UserResponse diff --git a/site/src/components/Page/RequireAuth.tsx b/site/src/components/Page/RequireAuth.tsx index c4b569d5b6dab..88d132dd26adf 100644 --- a/site/src/components/Page/RequireAuth.tsx +++ b/site/src/components/Page/RequireAuth.tsx @@ -1,7 +1,7 @@ import { useActor } from "@xstate/react" import React from "react" import { Navigate, useLocation } from "react-router" -import { userXService } from "../../../xServices/user/userXService" +import { userXService } from "../../xServices/user/userXService" import { FullScreenLoader } from "../Loader/FullScreenLoader" export interface RequireAuthProps { diff --git a/site/src/pages/login.tsx b/site/src/pages/login.tsx index a13cd737b31e4..5745d50cbd2c7 100644 --- a/site/src/pages/login.tsx +++ b/site/src/pages/login.tsx @@ -4,7 +4,7 @@ import React from "react" import { SignInForm } from "./../components/SignIn" import { Navigate, useLocation } from "react-router-dom" import { Location } from "history" -import { userXService } from "../../xServices/user/userXService" +import { userXService } from "../xServices/user/userXService" export const useStyles = makeStyles((theme) => ({ root: { diff --git a/site/xServices/user/userXService.ts b/site/src/xServices/user/userXService.ts similarity index 98% rename from site/xServices/user/userXService.ts rename to site/src/xServices/user/userXService.ts index 3e1d672129ab0..98e208b97aadf 100644 --- a/site/xServices/user/userXService.ts +++ b/site/src/xServices/user/userXService.ts @@ -1,11 +1,10 @@ import { createMachine, interpret, assign } from "xstate" -import { UserResponse } from "../../api" -import * as API from "../../api" +import * as API from '../../api' export interface UserContext { getUserError?: Error | unknown // unknown is a concession while I work out typing issues authError?: Error | unknown - me?: UserResponse + me?: API.UserResponse } export type UserEvent = { type: "SIGN_OUT" } | { type: "SIGN_IN"; email: string; password: string } From 9c21d4f6ae928e0bf7872b345f65a553eb91eaf3 Mon Sep 17 00:00:00 2001 From: Presley Date: Wed, 16 Mar 2022 22:44:01 +0000 Subject: [PATCH 20/42] Move and storybook navbarview --- .../components/Navbar/NavbarView.stories.tsx | 22 ++++++ site/src/components/Navbar/NavbarView.tsx | 69 ++++++++++++++++++ site/src/components/Navbar/index.tsx | 70 +------------------ 3 files changed, 92 insertions(+), 69 deletions(-) create mode 100644 site/src/components/Navbar/NavbarView.stories.tsx create mode 100644 site/src/components/Navbar/NavbarView.tsx diff --git a/site/src/components/Navbar/NavbarView.stories.tsx b/site/src/components/Navbar/NavbarView.stories.tsx new file mode 100644 index 0000000000000..afa202eded697 --- /dev/null +++ b/site/src/components/Navbar/NavbarView.stories.tsx @@ -0,0 +1,22 @@ + +import { Story } from "@storybook/react" +import React from "react" +import { NavbarView, NavbarViewProps } from "./NavbarView" + +export default { + title: "Page/NavbarView", + component: NavbarView, + argTypes: { + onSignOut: { action: "Sign Out" }, + }, +} + +const Template: Story = (args: NavbarViewProps) => + +export const Primary = Template.bind({}) +Primary.args = { + user: { id: '1', username: 'CathyCoder', email: 'cathy@coder.com', created_at: 'dawn'}, + onSignOut: () => { + return Promise.resolve() + }, +} \ No newline at end of file diff --git a/site/src/components/Navbar/NavbarView.tsx b/site/src/components/Navbar/NavbarView.tsx new file mode 100644 index 0000000000000..3009095577f52 --- /dev/null +++ b/site/src/components/Navbar/NavbarView.tsx @@ -0,0 +1,69 @@ +import React from 'react' +import Button from "@material-ui/core/Button" +import { makeStyles } from "@material-ui/core/styles" +import { Link } from "react-router-dom" +import { Logo } from "../Icons" +import { UserDropdown } from "./UserDropdown" +import { UserResponse } from "../../api" + +export interface NavbarViewProps { + user?: UserResponse + onSignOut: () => void +} + +export const NavbarView: React.FC = ({ user, onSignOut }) => { + const styles = useStyles() + return ( +
+
+ + + +
+
+
{user && }
+
+ ) +} + +const useStyles = makeStyles((theme) => ({ + root: { + position: "relative", + display: "flex", + flex: "0", + flexDirection: "row", + justifyContent: "center", + alignItems: "center", + height: "56px", + background: theme.palette.navbar.main, + marginTop: 0, + transition: "margin 150ms ease", + "@media (display-mode: standalone)": { + borderTop: `1px solid ${theme.palette.divider}`, + }, + borderBottom: `1px solid #383838`, + }, + fixed: { + flex: "0", + }, + fullWidth: { + flex: "1", + }, + logo: { + flex: "0", + height: "56px", + paddingLeft: theme.spacing(4), + paddingRight: theme.spacing(2), + borderRadius: 0, + "& svg": { + display: "block", + width: 125, + }, + }, + title: { + flex: "1", + textAlign: "center", + }, +})) diff --git a/site/src/components/Navbar/index.tsx b/site/src/components/Navbar/index.tsx index 110e9ae77e09f..87d3e88058820 100644 --- a/site/src/components/Navbar/index.tsx +++ b/site/src/components/Navbar/index.tsx @@ -1,18 +1,7 @@ import React from "react" -import Button from "@material-ui/core/Button" -import { makeStyles } from "@material-ui/core/styles" -import { Link } from "react-router-dom" - -import { Logo } from "../Icons" -import { UserDropdown } from "./UserDropdown" -import { UserResponse } from "../../api" import { useActor } from "@xstate/react" import { userXService } from "../../xServices/user/userXService" - -export interface NavbarProps { - user?: UserResponse - onSignOut: () => void -} +import { NavbarView } from "./NavbarView" export const Navbar: React.FC = () => { const [userState, userSend] = useActor(userXService) @@ -21,60 +10,3 @@ export const Navbar: React.FC = () => { return } - -export const NavbarView: React.FC = ({ user, onSignOut }) => { - const styles = useStyles() - return ( -
-
- - - -
-
-
{user && }
-
- ) -} - -const useStyles = makeStyles((theme) => ({ - root: { - position: "relative", - display: "flex", - flex: "0", - flexDirection: "row", - justifyContent: "center", - alignItems: "center", - height: "56px", - background: theme.palette.navbar.main, - marginTop: 0, - transition: "margin 150ms ease", - "@media (display-mode: standalone)": { - borderTop: `1px solid ${theme.palette.divider}`, - }, - borderBottom: `1px solid #383838`, - }, - fixed: { - flex: "0", - }, - fullWidth: { - flex: "1", - }, - logo: { - flex: "0", - height: "56px", - paddingLeft: theme.spacing(4), - paddingRight: theme.spacing(2), - borderRadius: 0, - "& svg": { - display: "block", - width: 125, - }, - }, - title: { - flex: "1", - textAlign: "center", - }, -})) From d1b1bab3679088c5c8d42ddf326d0e5967630268 Mon Sep 17 00:00:00 2001 From: Presley Date: Wed, 16 Mar 2022 22:55:41 +0000 Subject: [PATCH 21/42] Update docs --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c7e802701fbd1..c07cb1d3d2fce 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,6 @@ To manually run the server and go through first-time set up, run the following c You'll now be able to login and access the server. -To create a project, run: - `dist/coder_linux_amd64/coder projects create -d /path/to/project` ### Development @@ -63,6 +62,10 @@ The `develop.sh` script does three things: This is the recommend flow for working on the front-end, as hot-reload is set up as part of the webpack config. +Note that `./develop.sh` creates a user and allows you to log into the UI, but does not log you into the CLI, which is required for creating a project. Use the `login` command above before the `projects create` command. + +While we're working on automating XState typegen, you may need to run `yarn typegen` from `site`. + ## Front-End Plan For the front-end team, we're planning on 2 phases to the 'v2' work: From e6a7895fe145f00868053d8daf90f7be1bf354ec Mon Sep 17 00:00:00 2001 From: Presley Date: Wed, 16 Mar 2022 23:43:19 +0000 Subject: [PATCH 22/42] Install MSW --- site/package.json | 1 + site/yarn.lock | 341 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 331 insertions(+), 11 deletions(-) diff --git a/site/package.json b/site/package.json index 2e8758b0ea695..005578e0b8bf8 100644 --- a/site/package.json +++ b/site/package.json @@ -69,6 +69,7 @@ "jest": "27.5.1", "jest-junit": "13.0.0", "jest-runner-eslint": "1.0.0", + "msw": "^0.39.2", "prettier": "2.6.0", "react-hot-loader": "4.13.0", "sql-formatter": "4.0.2", diff --git a/site/yarn.lock b/site/yarn.lock index d985e4d9f126a..a5945b1a5a133 100644 --- a/site/yarn.lock +++ b/site/yarn.lock @@ -1658,6 +1658,26 @@ call-me-maybe "^1.0.1" glob-to-regexp "^0.3.0" +"@mswjs/cookies@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@mswjs/cookies/-/cookies-0.2.0.tgz#7ef2b5d7e444498bb27cf57720e61f76a4ce9f23" + integrity sha512-GTKYnIfXVP8GL8HRWrse+ujqDXCLKvu7+JoL6pvZFzS/d2i9pziByoWD69cOe35JNoSrx2DPNqrhUF+vgV3qUA== + dependencies: + "@types/set-cookie-parser" "^2.4.0" + set-cookie-parser "^2.4.6" + +"@mswjs/interceptors@^0.15.1": + version "0.15.1" + resolved "https://registry.yarnpkg.com/@mswjs/interceptors/-/interceptors-0.15.1.tgz#4a0009f56e51bc2cd3176f1507065c7d2f6c0d5e" + integrity sha512-D5B+ZJNlfvBm6ZctAfRBdNJdCHYAe2Ix4My5qfbHV5WH+3lkt3mmsjiWJzEh5ZwGDauzY487TldI275If7DJVw== + dependencies: + "@open-draft/until" "^1.0.3" + "@xmldom/xmldom" "^0.7.5" + debug "^4.3.3" + headers-polyfill "^3.0.4" + outvariant "^1.2.1" + strict-event-emitter "^0.2.0" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -1700,6 +1720,11 @@ mkdirp "^1.0.4" rimraf "^3.0.2" +"@open-draft/until@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@open-draft/until/-/until-1.0.3.tgz#db9cc719191a62e7d9200f6e7bab21c5b848adca" + integrity sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q== + "@playwright/test@1.20.0": version "1.20.0" resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.20.0.tgz#df5b1b45976d11c365e6cb60f8ec1ca7cccb84cf" @@ -2812,6 +2837,11 @@ dependencies: "@types/node" "*" +"@types/cookie@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" + integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== + "@types/cookiejar@*": version "2.1.2" resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.2.tgz#66ad9331f63fe8a3d3d9d8c6e3906dd10f6446e8" @@ -2928,6 +2958,11 @@ jest-matcher-utils "^27.0.0" pretty-format "^27.0.0" +"@types/js-levenshtein@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/js-levenshtein/-/js-levenshtein-1.1.1.tgz#ba05426a43f9e4e30b631941e0aa17bf0c890ed5" + integrity sha512-qC4bCqYGy1y/NP7dDVr7KJarn+PbX1nSpwA7JXdu0HxT3QYjO8MJ+cntENtHFVy2dRAyBV23OZ6MxsW1AM1L8g== + "@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.9" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" @@ -3083,6 +3118,13 @@ "@types/mime" "^1" "@types/node" "*" +"@types/set-cookie-parser@^2.4.0": + version "2.4.2" + resolved "https://registry.yarnpkg.com/@types/set-cookie-parser/-/set-cookie-parser-2.4.2.tgz#b6a955219b54151bfebd4521170723df5e13caad" + integrity sha512-fBZgytwhYAUkj/jC/FAV4RQ5EerRup1YQsXQCh8rZfiHkc4UahC192oH0smGwsXol3cL3A5oETuAHeQHmhXM4w== + dependencies: + "@types/node" "*" + "@types/sockjs@^0.3.33": version "0.3.33" resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.33.tgz#570d3a0b99ac995360e3136fd6045113b1bd236f" @@ -3604,6 +3646,11 @@ resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.6.1.tgz#0de2875ac31b46b6c5bb1ae0a7d7f0ba5678dffe" integrity sha512-gNGTiTrjEVQ0OcVnzsRSqTxaBSr+dmTfm+qJsCDluky8uhdLWep7Gcr62QsAKHTMxjCS/8nEITsmFAhfIx+QSw== +"@xmldom/xmldom@^0.7.5": + version "0.7.5" + resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.7.5.tgz#09fa51e356d07d0be200642b0e4f91d8e6dd408d" + integrity sha512-V3BIhmY36fXZ1OtVcI9W+FxQqxVLsPKcNjWigIaa81dLC9IolJl5Mt4Cvhmr0flUnjSpTdrbMTSbXqYqV5dT6A== + "@xstate/cli@^0.1.4": version "0.1.4" resolved "https://registry.yarnpkg.com/@xstate/cli/-/cli-0.1.4.tgz#5d909b980a5e62744f90b2790be3aa2717cfb4f8" @@ -4327,7 +4374,7 @@ base16@^1.0.0: resolved "https://registry.yarnpkg.com/base16/-/base16-1.0.0.tgz#e297f60d7ec1014a7a971a39ebc8a98c0b681e70" integrity sha1-4pf2DX7BAUp6lxo568ipjAtoHnA= -base64-js@^1.0.2: +base64-js@^1.0.2, base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -4384,6 +4431,15 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" +bl@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + bluebird@^3.3.5, bluebird@^3.5.5: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" @@ -4602,6 +4658,14 @@ buffer@^4.3.0: ieee754 "^1.1.4" isarray "^1.0.0" +buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + builtin-status-codes@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" @@ -4763,6 +4827,14 @@ ccount@^1.0.0: resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.1.0.tgz#246687debb6014735131be8abab2d93898f8d043" integrity sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg== +chalk@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad" + integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + chalk@^2.0.0, chalk@^2.4.1: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -4780,7 +4852,7 @@ chalk@^3.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^4.0.0, chalk@^4.1.0: +chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -4808,6 +4880,11 @@ character-reference-invalid@^1.0.0: resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560" integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg== +chardet@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== + chokidar@^2.1.8: version "2.1.8" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" @@ -4914,6 +4991,18 @@ cli-boxes@^2.2.1: resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + +cli-spinners@^2.5.0: + version "2.6.1" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.1.tgz#adc954ebe281c37a6319bfa401e6dd2488ffb70d" + integrity sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g== + cli-table3@^0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.1.tgz#36ce9b7af4847f288d3cdd081fbd09bf7bd237b8" @@ -4923,6 +5012,11 @@ cli-table3@^0.6.1: optionalDependencies: colors "1.4.0" +cli-width@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" + integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== + cliui@^7.0.2: version "7.0.4" resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" @@ -4941,6 +5035,11 @@ clone-deep@^4.0.1: kind-of "^6.0.2" shallow-clone "^3.0.0" +clone@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" + integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= + clsx@^1.0.2, clsx@^1.0.4, clsx@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" @@ -5192,7 +5291,7 @@ cookie-signature@1.0.6: resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= -cookie@0.4.2: +cookie@0.4.2, cookie@^0.4.2: version "0.4.2" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== @@ -5498,7 +5597,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9: dependencies: ms "2.0.0" -debug@4, debug@4.3.3, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2: +debug@4, debug@4.3.3, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3: version "4.3.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== @@ -5587,6 +5686,13 @@ default-gateway@^6.0.3: dependencies: execa "^5.0.0" +defaults@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d" + integrity sha1-xlYFHpgX2f8I7YgUd/P+QBnz730= + dependencies: + clone "^1.0.2" + define-lazy-prop@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" @@ -6381,7 +6487,7 @@ eventemitter3@^4.0.0: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== -events@^3.0.0, events@^3.2.0: +events@^3.0.0, events@^3.2.0, events@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== @@ -6523,6 +6629,15 @@ extend@^3.0.0: resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== +external-editor@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" + integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== + dependencies: + chardet "^0.7.0" + iconv-lite "^0.4.24" + tmp "^0.0.33" + extglob@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" @@ -6661,6 +6776,13 @@ figgy-pudding@^3.5.1: resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e" integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw== +figures@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== + dependencies: + escape-string-regexp "^1.0.5" + file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -7207,6 +7329,11 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6 resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== +graphql@^16.3.0: + version "16.3.0" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.3.0.tgz#a91e24d10babf9e60c706919bb182b53ccdffc05" + integrity sha512-xm+ANmA16BzCT5pLjuXySbQVFwH3oJctUVdy81w1sV0vBU0KgDdBGtxQOUd5zqOBk/JayAFeG8Dlmeq74rjm/A== + gzip-size@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462" @@ -7398,6 +7525,11 @@ he@^1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== +headers-polyfill@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/headers-polyfill/-/headers-polyfill-3.0.4.tgz#cd70c815a441dd882372fcd6eda212ce997c9b18" + integrity sha512-I1DOM1EdWYntdrnCvqQtcKwSSuiTzoqOExy4v1mdcFixFZABlWP4IPHdmoLtPda0abMHqDOY4H9svhQ10DFR4w== + hex-color-regex@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" @@ -7635,7 +7767,7 @@ hyphenate-style-name@^1.0.3: resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d" integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ== -iconv-lite@0.4.24: +iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -7649,7 +7781,7 @@ icss-utils@^4.0.0, icss-utils@^4.1.1: dependencies: postcss "^7.0.14" -ieee754@^1.1.4: +ieee754@^1.1.13, ieee754@^1.1.4: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -7728,6 +7860,26 @@ inline-style-parser@0.1.1: resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1" integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q== +inquirer@^8.2.0: + version "8.2.1" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.1.tgz#e00022e3e8930a92662f760f020686530a84671d" + integrity sha512-pxhBaw9cyTFMjwKtkjePWDhvwzvrNGAw7En4hottzlPvz80GZaMZthdDU35aA6/f5FRZf3uhE057q8w1DE3V2g== + dependencies: + ansi-escapes "^4.2.1" + chalk "^4.1.1" + cli-cursor "^3.1.0" + cli-width "^3.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.21" + mute-stream "0.0.8" + ora "^5.4.1" + run-async "^2.4.0" + rxjs "^7.5.5" + string-width "^4.1.0" + strip-ansi "^6.0.0" + through "^2.3.6" + internal-slot@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" @@ -7997,6 +8149,11 @@ is-in-browser@^1.0.2, is-in-browser@^1.1.3: resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835" integrity sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU= +is-interactive@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" + integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== + is-map@^2.0.1, is-map@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127" @@ -8007,6 +8164,11 @@ is-negative-zero@^2.0.1: resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== +is-node-process@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-node-process/-/is-node-process-1.0.1.tgz#4fc7ac3a91e8aac58175fe0578abbc56f2831b23" + integrity sha512-5IcdXuf++TTNt3oGl9EBdkvndXA8gmc4bz/Y+mdEpWh3Mcn/+kOw6hI7LD5CocqJWMzeb0I0ClndRVNdEPuJXQ== + is-number-object@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.6.tgz#6a7aaf838c7f0686a50b4553f7e54a96494e89f0" @@ -8131,6 +8293,11 @@ is-typedarray@^1.0.0: resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + is-weakmap@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2" @@ -8785,6 +8952,11 @@ jpeg-js@0.4.3: resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.3.tgz#6158e09f1983ad773813704be80680550eff977b" integrity sha512-ru1HWKek8octvUHFHvE5ZzQ1yAsJmIvRdGWvSoKV52XKyuyYA437QWDttXT8eZXDSbuMpHlLzPDZUPd6idIz+Q== +js-levenshtein@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d" + integrity sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g== + js-string-escape@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/js-string-escape/-/js-string-escape-1.0.1.tgz#e2625badbc0d67c7533e9edc1068c587ae4137ef" @@ -9188,6 +9360,14 @@ lodash@^4.0.1, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.2 resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -9612,6 +9792,31 @@ ms@2.1.3, ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +msw@^0.39.2: + version "0.39.2" + resolved "https://registry.yarnpkg.com/msw/-/msw-0.39.2.tgz#832e9274db62c43cb79854d5a69dce031c700de8" + integrity sha512-ju/HpqQpE4/qCxZ23t5Gaau0KREn4QuFzdG28nP1EpidMrymMJuIvNd32+2uGTGG031PMwrC41YW7vCxHOwyHA== + dependencies: + "@mswjs/cookies" "^0.2.0" + "@mswjs/interceptors" "^0.15.1" + "@open-draft/until" "^1.0.3" + "@types/cookie" "^0.4.1" + "@types/js-levenshtein" "^1.1.1" + chalk "4.1.1" + chokidar "^3.4.2" + cookie "^0.4.2" + graphql "^16.3.0" + headers-polyfill "^3.0.4" + inquirer "^8.2.0" + is-node-process "^1.0.1" + js-levenshtein "^1.1.6" + node-fetch "^2.6.7" + path-to-regexp "^6.2.0" + statuses "^2.0.0" + strict-event-emitter "^0.2.0" + type-fest "^1.2.2" + yargs "^17.3.1" + multicast-dns-service-types@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" @@ -9625,6 +9830,11 @@ multicast-dns@^6.0.1: dns-packet "^1.3.1" thunky "^1.0.2" +mute-stream@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" + integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== + nan@^2.12.1: version "2.15.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" @@ -9702,7 +9912,7 @@ node-dir@^0.1.10: dependencies: minimatch "^3.0.2" -node-fetch@2.6.7, node-fetch@^2.6.1: +node-fetch@2.6.7, node-fetch@^2.6.1, node-fetch@^2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== @@ -9950,7 +10160,7 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" -onetime@^5.1.2: +onetime@^5.1.0, onetime@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== @@ -10003,11 +10213,36 @@ optionator@^0.9.1: type-check "^0.4.0" word-wrap "^1.2.3" +ora@^5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18" + integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== + dependencies: + bl "^4.1.0" + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-spinners "^2.5.0" + is-interactive "^1.0.0" + is-unicode-supported "^0.1.0" + log-symbols "^4.1.0" + strip-ansi "^6.0.0" + wcwidth "^1.0.1" + os-browserify@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= +os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= + +outvariant@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/outvariant/-/outvariant-1.2.1.tgz#e630f6cdc1dbf398ed857e36f219de4a005ccd35" + integrity sha512-bcILvFkvpMXh66+Ubax/inxbKRyWTUiiFIW2DWkiS79wakrLGn3Ydy+GvukadiyfZjaL6C7YhIem4EZSM282wA== + overlayscrollbars@^1.13.1: version "1.13.1" resolved "https://registry.yarnpkg.com/overlayscrollbars/-/overlayscrollbars-1.13.1.tgz#0b840a88737f43a946b9d87875a2f9e421d0338a" @@ -10262,6 +10497,11 @@ path-to-regexp@0.1.7: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= +path-to-regexp@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.0.tgz#f7b3803336104c346889adece614669230645f38" + integrity sha512-f66KywYG6+43afgE/8j/GoiNyygk/bnoCbps++3ErRKsIYkGGupyv07R2Ok5m9i67Iqc+T2g1eAUGUPzWhYTyg== + path-type@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" @@ -11058,7 +11298,7 @@ read-pkg@^5.2.0: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.0.6, readable-stream@^3.6.0: +readable-stream@^3.0.6, readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -11337,6 +11577,14 @@ resolve@^2.0.0-next.3: is-core-module "^2.2.0" path-parse "^1.0.6" +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + ret@~0.1.10: version "0.1.15" resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" @@ -11399,6 +11647,11 @@ rsvp@^4.8.4: resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== +run-async@^2.4.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" + integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -11413,6 +11666,13 @@ run-queue@^1.0.0, run-queue@^1.0.3: dependencies: aproba "^1.1.1" +rxjs@^7.5.5: + version "7.5.5" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.5.tgz#2ebad89af0f560f460ad5cc4213219e1f7dd4e9f" + integrity sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw== + dependencies: + tslib "^2.1.0" + safe-buffer@5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" @@ -11629,6 +11889,11 @@ set-blocking@^2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= +set-cookie-parser@^2.4.6: + version "2.4.8" + resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.4.8.tgz#d0da0ed388bc8f24e706a391f9c9e252a13c58b2" + integrity sha512-edRH8mBKEWNVIVMKejNnuJxleqYE/ZSdcT8/Nem9/mmosx12pctd80s2Oy00KNZzrogMZS5mauK2/ymL1bvlvg== + set-value@^2.0.0, set-value@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" @@ -11982,6 +12247,11 @@ static-extend@^0.1.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= +statuses@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + store2@^2.12.0: version "2.13.1" resolved "https://registry.yarnpkg.com/store2/-/store2-2.13.1.tgz#fae7b5bb9d35fc53dc61cd262df3abb2f6e59022" @@ -12019,6 +12289,13 @@ stream-shift@^1.0.0: resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== +strict-event-emitter@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/strict-event-emitter/-/strict-event-emitter-0.2.0.tgz#78e2f75dc6ea502e5d8a877661065a1e2deedecd" + integrity sha512-zv7K2egoKwkQkZGEaH8m+i2D0XiKzx5jNsiSul6ja2IYFvil10A59Z9Y7PPAAe5OW53dQUf9CfsHKzjZzKkm1w== + dependencies: + events "^3.3.0" + string-length@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" @@ -12375,6 +12652,11 @@ through2@^2.0.0: readable-stream "~2.3.6" xtend "~4.0.1" +through@^2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + thunky@^1.0.2: version "1.1.0" resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" @@ -12397,6 +12679,13 @@ tinycolor2@^1.4.1: resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803" integrity sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA== +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + tmpl@1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" @@ -12568,7 +12857,7 @@ tslib@^1.10.0, tslib@^1.8.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.3.0: +tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0: version "2.3.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== @@ -12624,6 +12913,11 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-fest@^1.2.2: + version "1.4.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" + integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== + type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" @@ -13058,6 +13352,13 @@ wbuf@^1.1.0, wbuf@^1.7.3: dependencies: minimalistic-assert "^1.0.0" +wcwidth@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" + integrity sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g= + dependencies: + defaults "^1.0.3" + web-namespaces@^1.0.0: version "1.1.4" resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-1.1.4.tgz#bc98a3de60dadd7faefc403d1076d529f5e030ec" @@ -13511,6 +13812,11 @@ yargs-parser@20.x, yargs-parser@^20.2.2, yargs-parser@^20.2.7: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== +yargs-parser@^21.0.0: + version "21.0.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.0.1.tgz#0267f286c877a4f0f728fceb6f8a3e4cb95c6e35" + integrity sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg== + yargs@^16.2.0: version "16.2.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" @@ -13524,6 +13830,19 @@ yargs@^16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" +yargs@^17.3.1: + version "17.3.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.3.1.tgz#da56b28f32e2fd45aefb402ed9c26f42be4c07b9" + integrity sha512-WUANQeVgjLbNsEmGk20f+nlHgOqzRFpiGWVaBrYGYIGANIIu3lWjoyi0fNlFmJkvfhCZ6BXINe7/W2O2bV4iaA== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.0.0" + yauzl@2.10.0, yauzl@^2.10.0: version "2.10.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" From 6ac0530d8684492046ad866d7ec6d85b041c0fdd Mon Sep 17 00:00:00 2001 From: Presley Date: Wed, 16 Mar 2022 23:44:20 +0000 Subject: [PATCH 23/42] Reorganization, with apologies --- site/src/api.ts | 154 ------------------ site/src/{api.test.ts => api/index.test.ts} | 3 +- site/src/api/index.ts | 90 ++++++++++ site/src/api/types.ts | 65 ++++++++ site/src/components/Navbar/NavbarView.tsx | 2 +- site/src/components/Workspace/Workspace.tsx | 8 +- site/src/forms/CreateProjectForm.tsx | 2 +- site/src/forms/CreateWorkspaceForm.tsx | 2 +- .../mocks.ts => mocks/entities.ts} | 6 +- .../[organization]/[project]/create.tsx | 7 +- .../[organization]/[project]/index.tsx | 2 +- site/src/pages/projects/index.tsx | 2 +- site/src/pages/workspaces/[workspace].tsx | 8 +- site/src/test_helpers/index.tsx | 2 +- site/src/xServices/user/userXService.ts | 7 +- 15 files changed, 184 insertions(+), 176 deletions(-) delete mode 100644 site/src/api.ts rename site/src/{api.test.ts => api/index.test.ts} (97%) create mode 100644 site/src/api/index.ts create mode 100644 site/src/api/types.ts rename site/src/{test_helpers/mocks.ts => mocks/entities.ts} (84%) diff --git a/site/src/api.ts b/site/src/api.ts deleted file mode 100644 index 977766c431d26..0000000000000 --- a/site/src/api.ts +++ /dev/null @@ -1,154 +0,0 @@ -import axios, { AxiosRequestHeaders } from "axios" -import { mutate } from "swr" - -const CONTENT_TYPE_JSON: AxiosRequestHeaders = { - "Content-Type": "application/json", -} -export interface LoginResponse { - session_token: string -} - -export interface UserResponse { - readonly id: string - readonly username: string - readonly email: string - readonly created_at: string -} - -/** - * `Organization` must be kept in sync with the go struct in organizations.go - */ -export interface Organization { - id: string - name: string - created_at: string - updated_at: string -} - -export interface Provisioner { - id: string - name: string -} - -export const provisioners: Provisioner[] = [ - { - id: "terraform", - name: "Terraform", - }, - { - id: "cdr-basic", - name: "Basic", - }, -] - -// This must be kept in sync with the `Project` struct in the back-end -export interface Project { - id: string - created_at: string - updated_at: string - organization_id: string - name: string - provisioner: string - active_version_id: string -} - -export interface CreateProjectRequest { - name: string - organizationId: string - provisioner: string -} - -export namespace Project { - export const create = async (request: CreateProjectRequest): Promise => { - const response = await fetch(`/api/v2/projects/${request.organizationId}/`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(request), - }) - - const body = await response.json() - await mutate("/api/v2/projects") - if (!response.ok) { - throw new Error(body.message) - } - - return body - } -} - -export interface CreateWorkspaceRequest { - name: string - project_id: string -} - -// Must be kept in sync with backend Workspace struct -export interface Workspace { - id: string - created_at: string - updated_at: string - owner_id: string - project_id: string - name: string -} - -export namespace Workspace { - export const create = async (request: CreateWorkspaceRequest): Promise => { - const response = await fetch(`/api/v2/users/me/workspaces`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(request), - }) - - const body = await response.json() - if (!response.ok) { - throw new Error(body.message) - } - - // Let SWR know that both the /api/v2/workspaces/* and /api/v2/projects/* - // endpoints will need to fetch new data. - const mutateWorkspacesPromise = mutate("/api/v2/workspaces") - const mutateProjectsPromise = mutate("/api/v2/projects") - await Promise.all([mutateWorkspacesPromise, mutateProjectsPromise]) - - return body - } -} - -export interface LoginResponse { - session_token: string -} - -export const login = async (email: string, password: string): Promise => { - const payload = JSON.stringify({ - email, - password, - }) - - const response = await axios.post("/api/v2/users/login", payload, { - headers: { ...CONTENT_TYPE_JSON }, - }) - - return response.data -} - -export const logout = async (): Promise => { - await axios.post("/api/v2/users/logout") -} - -export const getUser = async (): Promise => { - const response = await axios.get("/api/v2/users/me") - return response.data -} - -export interface APIKeyResponse { - key: string -} - -export const getApiKey = async (): Promise => { - const response = await axios.post("/api/v2/users/me/keys") - return response.data -} diff --git a/site/src/api.test.ts b/site/src/api/index.test.ts similarity index 97% rename from site/src/api.test.ts rename to site/src/api/index.test.ts index 68da3fa56de5d..ec707499316f0 100644 --- a/site/src/api.test.ts +++ b/site/src/api/index.test.ts @@ -1,5 +1,6 @@ import axios from "axios" -import { APIKeyResponse, getApiKey, login, LoginResponse, logout } from "./api" +import { getApiKey, login, logout } from "." +import { LoginResponse, APIKeyResponse } from "./types" // Mock the axios module so that no real network requests are made, but rather // we swap in a resolved or rejected value diff --git a/site/src/api/index.ts b/site/src/api/index.ts new file mode 100644 index 0000000000000..30b63da9cec81 --- /dev/null +++ b/site/src/api/index.ts @@ -0,0 +1,90 @@ +import axios, { AxiosRequestHeaders } from "axios" +import { mutate } from "swr" +import * as Types from "./types" + +const CONTENT_TYPE_JSON: AxiosRequestHeaders = { + "Content-Type": "application/json", +} + +export const provisioners: Types.Provisioner[] = [ + { + id: "terraform", + name: "Terraform", + }, + { + id: "cdr-basic", + name: "Basic", + }, +] + +export namespace Project { + export const create = async (request: Types.CreateProjectRequest): Promise => { + const response = await fetch(`/api/v2/projects/${request.organizationId}/`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(request), + }) + + const body = await response.json() + await mutate("/api/v2/projects") + if (!response.ok) { + throw new Error(body.message) + } + + return body + } +} + +export namespace Workspace { + export const create = async (request: Types.CreateWorkspaceRequest): Promise => { + const response = await fetch(`/api/v2/users/me/workspaces`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(request), + }) + + const body = await response.json() + if (!response.ok) { + throw new Error(body.message) + } + + // Let SWR know that both the /api/v2/workspaces/* and /api/v2/projects/* + // endpoints will need to fetch new data. + const mutateWorkspacesPromise = mutate("/api/v2/workspaces") + const mutateProjectsPromise = mutate("/api/v2/projects") + await Promise.all([mutateWorkspacesPromise, mutateProjectsPromise]) + + return body + } +} + +export const login = async (email: string, password: string): Promise => { + const payload = JSON.stringify({ + email, + password, + }) + + const response = await axios.post("/api/v2/users/login", payload, { + headers: { ...CONTENT_TYPE_JSON }, + }) + + return response.data +} + +export const logout = async (): Promise => { + await axios.post("/api/v2/users/logout") +} + +export const getUser = async (): Promise => { + const response = await axios.get("/api/v2/users/me") + return response.data +} + +export const getApiKey = async (): Promise => { + const response = await axios.post("/api/v2/users/me/keys") + return response.data +} diff --git a/site/src/api/types.ts b/site/src/api/types.ts new file mode 100644 index 0000000000000..0c9775f15aee0 --- /dev/null +++ b/site/src/api/types.ts @@ -0,0 +1,65 @@ +export interface LoginResponse { + session_token: string +} + +export interface UserResponse { + readonly id: string + readonly username: string + readonly email: string + readonly created_at: string +} + +/** + * `Organization` must be kept in sync with the go struct in organizations.go + */ +export interface Organization { + id: string + name: string + created_at: string + updated_at: string +} + +export interface Provisioner { + id: string + name: string +} + +// This must be kept in sync with the `Project` struct in the back-end +export interface Project { + id: string + created_at: string + updated_at: string + organization_id: string + name: string + provisioner: string + active_version_id: string +} + +export interface CreateProjectRequest { + name: string + organizationId: string + provisioner: string +} + +export interface CreateWorkspaceRequest { + name: string + project_id: string +} + +// Must be kept in sync with backend Workspace struct +export interface Workspace { + id: string + created_at: string + updated_at: string + owner_id: string + project_id: string + name: string +} + +export interface LoginResponse { + session_token: string +} + +export interface APIKeyResponse { + key: string +} diff --git a/site/src/components/Navbar/NavbarView.tsx b/site/src/components/Navbar/NavbarView.tsx index 3009095577f52..2e0d448a1c512 100644 --- a/site/src/components/Navbar/NavbarView.tsx +++ b/site/src/components/Navbar/NavbarView.tsx @@ -4,7 +4,7 @@ import { makeStyles } from "@material-ui/core/styles" import { Link } from "react-router-dom" import { Logo } from "../Icons" import { UserDropdown } from "./UserDropdown" -import { UserResponse } from "../../api" +import { UserResponse } from "../../api/types" export interface NavbarViewProps { user?: UserResponse diff --git a/site/src/components/Workspace/Workspace.tsx b/site/src/components/Workspace/Workspace.tsx index 8eb1887881795..466b7a6cb0a5c 100644 --- a/site/src/components/Workspace/Workspace.tsx +++ b/site/src/components/Workspace/Workspace.tsx @@ -6,13 +6,13 @@ import CloudCircleIcon from "@material-ui/icons/CloudCircle" import { Link } from "react-router-dom" import React from "react" import * as Constants from "./constants" -import * as API from "../../api" +import * as Types from "../../api/types" import { WorkspaceSection } from "./WorkspaceSection" export interface WorkspaceProps { - organization: API.Organization - workspace: API.Workspace - project: API.Project + organization: Types.Organization + workspace: Types.Workspace + project: Types.Project } /** diff --git a/site/src/forms/CreateProjectForm.tsx b/site/src/forms/CreateProjectForm.tsx index a930cf487e054..77aefacef7c55 100644 --- a/site/src/forms/CreateProjectForm.tsx +++ b/site/src/forms/CreateProjectForm.tsx @@ -13,7 +13,7 @@ import { FormCloseButton, } from "../components/Form" import { LoadingButton } from "../components/Button" -import { Organization, Project, Provisioner, CreateProjectRequest } from "./../api" +import { Organization, Project, Provisioner, CreateProjectRequest } from "../api/types" export interface CreateProjectFormProps { provisioners: Provisioner[] diff --git a/site/src/forms/CreateWorkspaceForm.tsx b/site/src/forms/CreateWorkspaceForm.tsx index 06ff82b144c6f..5d95e14498732 100644 --- a/site/src/forms/CreateWorkspaceForm.tsx +++ b/site/src/forms/CreateWorkspaceForm.tsx @@ -6,7 +6,7 @@ import * as Yup from "yup" import { FormCloseButton, FormTextField, FormTitle, FormSection } from "../components/Form" import { LoadingButton } from "../components/Button" -import { Project, Workspace, CreateWorkspaceRequest } from "../api" +import { Project, Workspace, CreateWorkspaceRequest } from "../api/types" export interface CreateWorkspaceForm { project: Project diff --git a/site/src/test_helpers/mocks.ts b/site/src/mocks/entities.ts similarity index 84% rename from site/src/test_helpers/mocks.ts rename to site/src/mocks/entities.ts index 787258a962c96..d2d9656cc96bd 100644 --- a/site/src/test_helpers/mocks.ts +++ b/site/src/mocks/entities.ts @@ -1,4 +1,8 @@ -import { Provisioner, Organization, Project, Workspace, UserResponse } from "../api" +import { Provisioner, Organization, Project, Workspace, UserResponse } from "../api/types" + +export const MockSessionToken = { session_token: "my-session-token" } + +export const MockAPIKey = { key: "my-api-key" } export const MockUser: UserResponse = { id: "test-user-id", diff --git a/site/src/pages/projects/[organization]/[project]/create.tsx b/site/src/pages/projects/[organization]/[project]/create.tsx index 84b8b1a6dd03d..87e93d36bb47a 100644 --- a/site/src/pages/projects/[organization]/[project]/create.tsx +++ b/site/src/pages/projects/[organization]/[project]/create.tsx @@ -3,6 +3,7 @@ import { makeStyles } from "@material-ui/core/styles" import { useNavigate, useParams } from "react-router-dom" import useSWR from "swr" +import * as Types from "../../../../api/types" import * as API from "../../../../api" import { ErrorSummary } from "../../../../components/ErrorSummary" import { FullScreenLoader } from "../../../../components/Loader/FullScreenLoader" @@ -14,11 +15,11 @@ export const CreateWorkspacePage: React.FC = () => { const navigate = useNavigate() const styles = useStyles() - const { data: organizationInfo, error: organizationError } = useSWR( + const { data: organizationInfo, error: organizationError } = useSWR( () => `/api/v2/users/me/organizations/${organizationName}`, ) - const { data: project, error: projectError } = useSWR(() => { + const { data: project, error: projectError } = useSWR(() => { return `/api/v2/organizations/${unsafeSWRArgument(organizationInfo).id}/projects/${projectName}` }) @@ -26,7 +27,7 @@ export const CreateWorkspacePage: React.FC = () => { navigate(`/projects/${organizationName}/${projectName}`) }, [navigate, organizationName, projectName]) - const onSubmit = async (req: API.CreateWorkspaceRequest) => { + const onSubmit = async (req: Types.CreateWorkspaceRequest) => { const workspace = await API.Workspace.create(req) navigate(`/workspaces/${workspace.id}`) return workspace diff --git a/site/src/pages/projects/[organization]/[project]/index.tsx b/site/src/pages/projects/[organization]/[project]/index.tsx index c2e4b38480b6d..b762335bce754 100644 --- a/site/src/pages/projects/[organization]/[project]/index.tsx +++ b/site/src/pages/projects/[organization]/[project]/index.tsx @@ -4,7 +4,7 @@ import Paper from "@material-ui/core/Paper" import { Link, useNavigate, useParams } from "react-router-dom" import useSWR from "swr" -import { Organization, Project, Workspace } from "../../../../api" +import { Organization, Project, Workspace } from "../../../../api/types" import { Header } from "../../../../components/Header" import { FullScreenLoader } from "../../../../components/Loader/FullScreenLoader" import { Footer } from "../../../../components/Page" diff --git a/site/src/pages/projects/index.tsx b/site/src/pages/projects/index.tsx index d36c369f5e753..84d4ce411c943 100644 --- a/site/src/pages/projects/index.tsx +++ b/site/src/pages/projects/index.tsx @@ -9,7 +9,7 @@ import { Footer } from "../../components/Page" import { Column, Table } from "../../components/Table" import { FullScreenLoader } from "../../components/Loader/FullScreenLoader" -import { Organization, Project } from "./../../api" +import { Organization, Project } from "../../api/types" import useSWR from "swr" import { CodeExample } from "../../components/CodeExample/CodeExample" diff --git a/site/src/pages/workspaces/[workspace].tsx b/site/src/pages/workspaces/[workspace].tsx index 5753c9d040499..8e153eebb787c 100644 --- a/site/src/pages/workspaces/[workspace].tsx +++ b/site/src/pages/workspaces/[workspace].tsx @@ -8,24 +8,24 @@ import { ErrorSummary } from "../../components/ErrorSummary" import { FullScreenLoader } from "../../components/Loader/FullScreenLoader" import { Workspace } from "../../components/Workspace" import { unsafeSWRArgument } from "../../util" -import * as API from "../../api" +import * as Types from "../../api/types" export const WorkspacePage: React.FC = () => { const styles = useStyles() const { workspace: workspaceQueryParam } = useParams() - const { data: workspace, error: workspaceError } = useSWR(() => { + const { data: workspace, error: workspaceError } = useSWR(() => { const workspaceParam = firstOrItem(workspaceQueryParam, null) return `/api/v2/workspaces/${workspaceParam}` }) // Fetch parent project - const { data: project, error: projectError } = useSWR(() => { + const { data: project, error: projectError } = useSWR(() => { return `/api/v2/projects/${unsafeSWRArgument(workspace).project_id}` }) - const { data: organization, error: organizationError } = useSWR(() => { + const { data: organization, error: organizationError } = useSWR(() => { return `/api/v2/organizations/${unsafeSWRArgument(project).organization_id}` }) diff --git a/site/src/test_helpers/index.tsx b/site/src/test_helpers/index.tsx index 08626d442a0c4..a0a21239f4300 100644 --- a/site/src/test_helpers/index.tsx +++ b/site/src/test_helpers/index.tsx @@ -20,4 +20,4 @@ export const render = (component: React.ReactElement): RenderResult => { return wrappedRender({component}) } -export * from "./mocks" +export * from "../mocks/entities" diff --git a/site/src/xServices/user/userXService.ts b/site/src/xServices/user/userXService.ts index 98e208b97aadf..0df040433842c 100644 --- a/site/src/xServices/user/userXService.ts +++ b/site/src/xServices/user/userXService.ts @@ -1,10 +1,11 @@ import { createMachine, interpret, assign } from "xstate" +import * as Types from '../../api/types' import * as API from '../../api' export interface UserContext { getUserError?: Error | unknown // unknown is a concession while I work out typing issues authError?: Error | unknown - me?: API.UserResponse + me?: Types.UserResponse } export type UserEvent = { type: "SIGN_OUT" } | { type: "SIGN_IN"; email: string; password: string } @@ -19,10 +20,10 @@ const userMachine = events: {} as UserEvent, services: {} as { getMe: { - data: API.UserResponse + data: Types.UserResponse } signIn: { - data: API.LoginResponse | undefined + data: Types.LoginResponse | undefined } }, }, From 1c54f80100083fbab7b5b1b427c84490db08de40 Mon Sep 17 00:00:00 2001 From: Presley Date: Wed, 16 Mar 2022 23:46:19 +0000 Subject: [PATCH 24/42] Missed spots --- site/src/components/Navbar/UserDropdown.tsx | 2 +- site/src/components/User/UserAvatar.tsx | 2 +- site/src/components/User/UserProfileCard.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/site/src/components/Navbar/UserDropdown.tsx b/site/src/components/Navbar/UserDropdown.tsx index 012f7a9724968..cfd100cd36965 100644 --- a/site/src/components/Navbar/UserDropdown.tsx +++ b/site/src/components/Navbar/UserDropdown.tsx @@ -12,7 +12,7 @@ import { BorderedMenu } from "./BorderedMenu" import { UserProfileCard } from "../User/UserProfileCard" import { UserAvatar } from "../User" -import { UserResponse } from "../../api" +import { UserResponse } from "../../api/types" export interface UserDropdownProps { user: UserResponse diff --git a/site/src/components/User/UserAvatar.tsx b/site/src/components/User/UserAvatar.tsx index b90db25f0bb01..12070717908c9 100644 --- a/site/src/components/User/UserAvatar.tsx +++ b/site/src/components/User/UserAvatar.tsx @@ -1,6 +1,6 @@ import Avatar from "@material-ui/core/Avatar" import React from "react" -import { UserResponse } from "../../api" +import { UserResponse } from "../../api/types" export interface UserAvatarProps { user: UserResponse diff --git a/site/src/components/User/UserProfileCard.tsx b/site/src/components/User/UserProfileCard.tsx index 1a013161bf6bb..882bea250cf43 100644 --- a/site/src/components/User/UserProfileCard.tsx +++ b/site/src/components/User/UserProfileCard.tsx @@ -1,7 +1,7 @@ import { makeStyles } from "@material-ui/core/styles" import Typography from "@material-ui/core/Typography" import React from "react" -import { UserResponse } from "../../api" +import { UserResponse } from "../../api/types" import { UserAvatar } from "./UserAvatar" From ee8ff32e32b86960edcafcd6c22f74c7febc90c8 Mon Sep 17 00:00:00 2001 From: Presley Date: Wed, 16 Mar 2022 23:55:37 +0000 Subject: [PATCH 25/42] Add mock handlers --- site/src/mocks/handlers.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 site/src/mocks/handlers.ts diff --git a/site/src/mocks/handlers.ts b/site/src/mocks/handlers.ts new file mode 100644 index 0000000000000..25afa56079601 --- /dev/null +++ b/site/src/mocks/handlers.ts @@ -0,0 +1,20 @@ +import { rest } from 'msw' +import * as M from './entities' + +export const handlers = [ + rest.post("/api/v2/users/me/workspaces", (req, res, ctx) => { + return res(ctx.status(200), ctx.json(M.MockWorkspace)) + }), + rest.post("api/v2/users/login", (req, res, ctx) => { + return res(ctx.status(200), ctx.json(M.MockSessionToken)) + }), + rest.post("/api/v2/users/logout", (req, res, ctx) => { + return res(ctx.status(200)) + }), + rest.get("api/v2/users/me", (req, res, ctx) => { + return res(ctx.status(200), ctx.json(M.MockUser)) + }), + rest.get("api/v2/users/me/keys", (req, res, ctx) => { + return res(ctx.status(200), ctx.json(M.MockAPIKey)) + }) +] \ No newline at end of file From a88e2609640eb35908e558d945d3fe24f15b27f3 Mon Sep 17 00:00:00 2001 From: Presley Date: Thu, 17 Mar 2022 13:17:16 +0000 Subject: [PATCH 26/42] Configure jest for msw --- site/jest.config.js | 1 + site/src/mocks/server.ts | 5 +++++ site/src/setupTests.ts | 11 +++++++++++ 3 files changed, 17 insertions(+) create mode 100644 site/src/mocks/server.ts create mode 100644 site/src/setupTests.ts diff --git a/site/jest.config.js b/site/jest.config.js index 17e6e73fd154d..dc8b38f3ee9c4 100644 --- a/site/jest.config.js +++ b/site/jest.config.js @@ -1,4 +1,5 @@ module.exports = { + setupFilesAfterEnv: ['src/setupTests.js'], projects: [ { globals: { diff --git a/site/src/mocks/server.ts b/site/src/mocks/server.ts new file mode 100644 index 0000000000000..fadac07f29ad2 --- /dev/null +++ b/site/src/mocks/server.ts @@ -0,0 +1,5 @@ +import { setupServer } from 'msw/node' +import { handlers } from './handlers' + +// This configures a request mocking server with the given request handlers. +export const server = setupServer(...handlers) \ No newline at end of file diff --git a/site/src/setupTests.ts b/site/src/setupTests.ts new file mode 100644 index 0000000000000..405a4be90d3e2 --- /dev/null +++ b/site/src/setupTests.ts @@ -0,0 +1,11 @@ +// src/setupTests.js +import { server } from './mocks/server.js' +// Establish API mocking before all tests. +beforeAll(() => server.listen()) + +// Reset any request handlers that we may add during the tests, +// so they don't affect other tests. +afterEach(() => server.resetHandlers()) + +// Clean up after the tests are finished. +afterAll(() => server.close()) \ No newline at end of file From e0e36a0cafe21aabc6c1b723c226aebfbac8ea90 Mon Sep 17 00:00:00 2001 From: Presley Date: Thu, 17 Mar 2022 13:24:30 +0000 Subject: [PATCH 27/42] Fix typos --- site/jest.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/jest.config.js b/site/jest.config.js index dc8b38f3ee9c4..499d1f4ebd856 100644 --- a/site/jest.config.js +++ b/site/jest.config.js @@ -1,5 +1,5 @@ module.exports = { - setupFilesAfterEnv: ['src/setupTests.js'], + setupFilesAfterEnv: ["./src/setupTests.ts"], projects: [ { globals: { From cf7f9346c93c8eb973b59bc32932090c8d746f7e Mon Sep 17 00:00:00 2001 From: Presley Date: Thu, 17 Mar 2022 14:02:40 +0000 Subject: [PATCH 28/42] Shift unit test to NavbarView --- site/src/components/Navbar/index.test.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/site/src/components/Navbar/index.test.tsx b/site/src/components/Navbar/index.test.tsx index a4d794cf7847d..58a53f658a7f9 100644 --- a/site/src/components/Navbar/index.test.tsx +++ b/site/src/components/Navbar/index.test.tsx @@ -2,15 +2,15 @@ import React from "react" import { screen } from "@testing-library/react" import { render, MockUser } from "../../test_helpers" -import { Navbar } from "./index" +import { NavbarView } from "./NavbarView" -describe("Navbar", () => { +describe("NavbarView", () => { const noop = () => { return } it("renders content", async () => { // When - render() + render() // Then await screen.findAllByText("Coder", { exact: false }) @@ -24,7 +24,7 @@ describe("Navbar", () => { } // When - render() + render() // Then // There should be a 'B' avatar! From ca3e7fc0f27b699257394839e033d5ad6f52b9c8 Mon Sep 17 00:00:00 2001 From: Presley Date: Thu, 17 Mar 2022 14:28:26 +0000 Subject: [PATCH 29/42] Fix test types --- site/package.json | 2 +- site/tsconfig.json | 2 +- site/tsconfig.test.json | 1 + site/yarn.lock | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/site/package.json b/site/package.json index 005578e0b8bf8..6318ea094dcfe 100644 --- a/site/package.json +++ b/site/package.json @@ -46,7 +46,7 @@ "@storybook/react": "6.4.19", "@testing-library/react": "12.1.4", "@types/express": "4.17.13", - "@types/jest": "27.4.1", + "@types/jest": "^27.4.1", "@types/node": "14.18.12", "@types/react": "17.0.40", "@types/react-dom": "17.0.13", diff --git a/site/tsconfig.json b/site/tsconfig.json index 1989ec0b9efc5..09f1b3f27fda0 100644 --- a/site/tsconfig.json +++ b/site/tsconfig.json @@ -16,5 +16,5 @@ "target": "es5" }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "_jest", "**/*.test.tsx"] + "exclude": ["node_modules", "_jest", "**/*.test.tsx?"] } diff --git a/site/tsconfig.test.json b/site/tsconfig.test.json index 2cd6b5bbd2bfd..011db38931aa8 100644 --- a/site/tsconfig.test.json +++ b/site/tsconfig.test.json @@ -1,4 +1,5 @@ { "extends": "./tsconfig.json", + "include": ["**/*.test.tsx?"], "exclude": ["node_modules", "_jest"] } diff --git a/site/yarn.lock b/site/yarn.lock index a5945b1a5a133..b6fe1d874f493 100644 --- a/site/yarn.lock +++ b/site/yarn.lock @@ -2950,7 +2950,7 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@27.4.1": +"@types/jest@^27.4.1": version "27.4.1" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.4.1.tgz#185cbe2926eaaf9662d340cc02e548ce9e11ab6d" integrity sha512-23iPJADSmicDVrWk+HT58LMJtzLAnB2AgIzplQuq/bSrGaxCrlvRFjGbXmamnnk/mAmCdLStiGqggu28ocUyiw== From 6700453cf199ec0d8f4a3021961e4c5642ffed56 Mon Sep 17 00:00:00 2001 From: Presley Date: Thu, 17 Mar 2022 14:28:38 +0000 Subject: [PATCH 30/42] Rename NavbarView test --- .../src/components/Navbar/{index.test.tsx => NavbarView.test.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename site/src/components/Navbar/{index.test.tsx => NavbarView.test.tsx} (100%) diff --git a/site/src/components/Navbar/index.test.tsx b/site/src/components/Navbar/NavbarView.test.tsx similarity index 100% rename from site/src/components/Navbar/index.test.tsx rename to site/src/components/Navbar/NavbarView.test.tsx From 48ac5b73ff7d4ea4db8f5bcd96461d8d6297842c Mon Sep 17 00:00:00 2001 From: Presley Date: Thu, 17 Mar 2022 15:49:35 +0000 Subject: [PATCH 31/42] Attempt at test, wip --- .../src/components/SignIn/SignInForm.test.tsx | 82 ------------------- site/src/components/SignIn/SignInForm.tsx | 2 +- site/src/mocks/handlers.ts | 10 +-- site/src/pages/login.test.tsx | 81 ++++++++++++++++++ site/src/setupTests.ts | 5 +- 5 files changed, 91 insertions(+), 89 deletions(-) delete mode 100644 site/src/components/SignIn/SignInForm.test.tsx create mode 100644 site/src/pages/login.test.tsx diff --git a/site/src/components/SignIn/SignInForm.test.tsx b/site/src/components/SignIn/SignInForm.test.tsx deleted file mode 100644 index 83694b50fced6..0000000000000 --- a/site/src/components/SignIn/SignInForm.test.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import React from "react" -import { act, fireEvent, screen, waitFor } from "@testing-library/react" -import { history, render } from "../../test_helpers" - -import { SignInForm } from "./SignInForm" - -describe("SignInForm", () => { - beforeEach(() => { - history.replace("/") - }) - - it("renders content", async () => { - // When - render() - - // Then - await screen.findByText("Sign In", { exact: false }) - }) - - it("shows an error message if SignIn fails", async () => { - // Given - const loginHandler = (_email: string, _password: string) => Promise.reject("Unacceptable credentials") - - // When - // Render the component - const { container } = render() - const inputs = container.querySelectorAll("input") - // Set username / password - fireEvent.change(inputs[0], { target: { value: "test@coder.com" } }) - fireEvent.change(inputs[1], { target: { value: "password" } }) - // Click sign-in - const elem = await screen.findByText("Sign In") - act(() => elem.click()) - - // Then - // Should see an error message - const errorMessage = await screen.findByText("The username or password is incorrect.") - expect(errorMessage).toBeDefined() - }) - - it("redirects when login is complete", async () => { - // Given - const loginHandler = (_email: string, _password: string) => Promise.resolve() - - // When - // Render the component - const { container } = render() - // Set user / password - const inputs = container.querySelectorAll("input") - fireEvent.change(inputs[0], { target: { value: "test@coder.com" } }) - fireEvent.change(inputs[1], { target: { value: "password" } }) - // Click sign-in - const elem = await screen.findByText("Sign In") - act(() => elem.click()) - - // Then - // Should redirect because login was successful - await waitFor(() => expect(history.location.pathname).toEqual("/")) - }) - - it("respects ?redirect query parameter when complete", async () => { - // Given - const loginHandler = (_email: string, _password: string) => Promise.resolve() - // Set a path to redirect to after login is successful - history.replace("/login?redirect=%2Fsome%2Fother%2Fpath") - - // When - // Render the component - const { container } = render() - // Set user / password - const inputs = container.querySelectorAll("input") - fireEvent.change(inputs[0], { target: { value: "test@coder.com" } }) - fireEvent.change(inputs[1], { target: { value: "password" } }) - // Click sign-in - const elem = await screen.findByText("Sign In") - act(() => elem.click()) - - // Then - // Should redirect to /some/other/path because ?redirect was specified and login was successful - await waitFor(() => expect(history.location.pathname).toEqual("/some/other/path")) - }) -}) diff --git a/site/src/components/SignIn/SignInForm.tsx b/site/src/components/SignIn/SignInForm.tsx index 9b3db2ddff8c8..e001e0ed8865b 100644 --- a/site/src/components/SignIn/SignInForm.tsx +++ b/site/src/components/SignIn/SignInForm.tsx @@ -87,7 +87,7 @@ export const SignInForm: React.FC = ({ isLoading, authErrorMess placeholder="Password" variant="outlined" /> - {authErrorMessage && {authErrorMessage}} + {authErrorMessage && {authErrorMessage}}
{ + rest.post("/api/v2/users/me/workspaces", async (req, res, ctx) => { return res(ctx.status(200), ctx.json(M.MockWorkspace)) }), - rest.post("api/v2/users/login", (req, res, ctx) => { + rest.post("/api/v2/users/login", async (req, res, ctx) => { return res(ctx.status(200), ctx.json(M.MockSessionToken)) }), - rest.post("/api/v2/users/logout", (req, res, ctx) => { + rest.post("/api/v2/users/logout", async (req, res, ctx) => { return res(ctx.status(200)) }), - rest.get("api/v2/users/me", (req, res, ctx) => { + rest.get("/api/v2/users/me", async (req, res, ctx) => { return res(ctx.status(200), ctx.json(M.MockUser)) }), - rest.get("api/v2/users/me/keys", (req, res, ctx) => { + rest.get("/api/v2/users/me/keys", async (req, res, ctx) => { return res(ctx.status(200), ctx.json(M.MockAPIKey)) }) ] \ No newline at end of file diff --git a/site/src/pages/login.test.tsx b/site/src/pages/login.test.tsx new file mode 100644 index 0000000000000..58054acfd5eb1 --- /dev/null +++ b/site/src/pages/login.test.tsx @@ -0,0 +1,81 @@ +import React from "react" +import { act, fireEvent, screen, waitFor } from "@testing-library/react" +import { history, render } from "../test_helpers" +import { SignInPage } from "./login" +import { server } from "../mocks/server" +import { rest } from "msw" + +describe("SignInPage", () => { + beforeEach(() => { + history.replace("/login") + }) + + it("renders the sign-in form", async () => { + // When + render() + + // Then + await screen.findByText("Sign In", { exact: false }) + }) + + it("shows an error message if SignIn fails", async () => { + // Given + const { container } = render() + // Make login fail + server.use( + rest.post('/api/v2/users/login', async (req, res, ctx) => { + return res( + ctx.status(500), + ctx.json({message: 'nope'})) + }), + ) + + // When + // Set username / password + const [username, password] = container.querySelectorAll("input") + fireEvent.change(username, { target: { value: "test@coder.com" } }) + fireEvent.change(password, { target: { value: "password" } }) + // Click sign-in + const signInButton = await screen.findByText("Sign In") + act(() => signInButton.click()) + + // Then + // Finding error by test id because it comes from the backend + const errorMessage = await screen.findByTestId("sign-in-error") + expect(errorMessage).toBeDefined() + expect(history.location.pathname).toEqual("/login") + }) + + it("redirects when login is complete", async () => { + // Given + const { container } = render() + + // When user signs in + const [username, password] = container.querySelectorAll("input") + fireEvent.change(username, { target: { value: "test@coder.com" } }) + fireEvent.change(password, { target: { value: "password" } }) + const signInButton = await screen.findByText("Sign In") + act(() => signInButton.click()) + + // Then + await waitFor(() => expect(history.location.pathname).toEqual("/projects")) + }) + + it("respects ?redirect query parameter when complete", async () => { + // Given + const { container } = render() + // Set a path to redirect to after login is successful + act(() => history.replace("/login?redirect=%2Fsome%2Fother%2Fpath")) + console.log(history.location.pathname) + + // When user signs in + const [username, password] = container.querySelectorAll("input") + fireEvent.change(username, { target: { value: "test@coder.com" } }) + fireEvent.change(password, { target: { value: "password" } }) + const signInButton = await screen.findByText("Sign In") + act(() => signInButton.click()) + + // Then + await waitFor(() => expect(history.location.pathname).toEqual("/some/other/path")) + }) +}) diff --git a/site/src/setupTests.ts b/site/src/setupTests.ts index 405a4be90d3e2..2ff730e0d9dd9 100644 --- a/site/src/setupTests.ts +++ b/site/src/setupTests.ts @@ -1,7 +1,10 @@ // src/setupTests.js import { server } from './mocks/server.js' + // Establish API mocking before all tests. -beforeAll(() => server.listen()) +beforeAll(() => server.listen({ + onUnhandledRequest: "error" +})) // Reset any request handlers that we may add during the tests, // so they don't affect other tests. From 5c142db29c23a908419639ddddfe894f2f51d77d Mon Sep 17 00:00:00 2001 From: Presley Date: Thu, 17 Mar 2022 19:36:26 +0000 Subject: [PATCH 32/42] Fix config --- site/jest.config.js | 2 +- site/src/setupTests.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/site/jest.config.js b/site/jest.config.js index 499d1f4ebd856..e68474e5d4ec4 100644 --- a/site/jest.config.js +++ b/site/jest.config.js @@ -1,5 +1,4 @@ module.exports = { - setupFilesAfterEnv: ["./src/setupTests.ts"], projects: [ { globals: { @@ -19,6 +18,7 @@ module.exports = { testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$", testPathIgnorePatterns: ["/node_modules/", "/__tests__/fakes", "/e2e/"], moduleDirectories: ["node_modules", ""], + setupFilesAfterEnv: ["./src/setupTests.ts"], }, { displayName: "lint", diff --git a/site/src/setupTests.ts b/site/src/setupTests.ts index 2ff730e0d9dd9..d9924d08f8fbc 100644 --- a/site/src/setupTests.ts +++ b/site/src/setupTests.ts @@ -1,5 +1,4 @@ -// src/setupTests.js -import { server } from './mocks/server.js' +import { server } from './mocks/server' // Establish API mocking before all tests. beforeAll(() => server.listen({ From 595859ac30dedc60a126e6d55aca5dc64db63891 Mon Sep 17 00:00:00 2001 From: Presley Date: Thu, 17 Mar 2022 21:46:13 +0000 Subject: [PATCH 33/42] Be logged out, only warn --- site/src/pages/login.test.tsx | 22 +++++++++++++++------- site/src/setupTests.ts | 2 +- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/site/src/pages/login.test.tsx b/site/src/pages/login.test.tsx index 58054acfd5eb1..623ae10ce673b 100644 --- a/site/src/pages/login.test.tsx +++ b/site/src/pages/login.test.tsx @@ -8,6 +8,14 @@ import { rest } from "msw" describe("SignInPage", () => { beforeEach(() => { history.replace("/login") + // appear logged out + server.use( + rest.get('/api/v2/users/me', (req, res, ctx) => { + return res( + ctx.status(401), + ctx.json({message: 'no user here'})) + }) + ) }) it("renders the sign-in form", async () => { @@ -22,13 +30,13 @@ describe("SignInPage", () => { // Given const { container } = render() // Make login fail - server.use( - rest.post('/api/v2/users/login', async (req, res, ctx) => { - return res( - ctx.status(500), - ctx.json({message: 'nope'})) - }), - ) + // server.use( + // rest.post('/api/v2/users/login', async (req, res, ctx) => { + // return res( + // ctx.status(500), + // ctx.json({message: 'nope'})) + // }), + // ) // When // Set username / password diff --git a/site/src/setupTests.ts b/site/src/setupTests.ts index d9924d08f8fbc..2244ba9aa2481 100644 --- a/site/src/setupTests.ts +++ b/site/src/setupTests.ts @@ -2,7 +2,7 @@ import { server } from './mocks/server' // Establish API mocking before all tests. beforeAll(() => server.listen({ - onUnhandledRequest: "error" + onUnhandledRequest: "warn" })) // Reset any request handlers that we may add during the tests, From 18248776adef015bd1494b73449d02a53791a022 Mon Sep 17 00:00:00 2001 From: Presley Date: Thu, 17 Mar 2022 23:24:10 +0000 Subject: [PATCH 34/42] Conditionally show text to help test --- site/src/components/SignIn/SignInForm.tsx | 2 +- site/src/pages/login.test.tsx | 47 ++++------------------- 2 files changed, 8 insertions(+), 41 deletions(-) diff --git a/site/src/components/SignIn/SignInForm.tsx b/site/src/components/SignIn/SignInForm.tsx index e001e0ed8865b..9899f234e7c3f 100644 --- a/site/src/components/SignIn/SignInForm.tsx +++ b/site/src/components/SignIn/SignInForm.tsx @@ -98,7 +98,7 @@ export const SignInForm: React.FC = ({ isLoading, authErrorMess type="submit" variant="contained" > - Sign In + { isLoading ? '' : "Sign In" }
diff --git a/site/src/pages/login.test.tsx b/site/src/pages/login.test.tsx index 623ae10ce673b..63a1f8123aae5 100644 --- a/site/src/pages/login.test.tsx +++ b/site/src/pages/login.test.tsx @@ -30,13 +30,13 @@ describe("SignInPage", () => { // Given const { container } = render() // Make login fail - // server.use( - // rest.post('/api/v2/users/login', async (req, res, ctx) => { - // return res( - // ctx.status(500), - // ctx.json({message: 'nope'})) - // }), - // ) + server.use( + rest.post('/api/v2/users/login', async (req, res, ctx) => { + return res( + ctx.status(500), + ctx.json({message: 'nope'})) + }), + ) // When // Set username / password @@ -53,37 +53,4 @@ describe("SignInPage", () => { expect(errorMessage).toBeDefined() expect(history.location.pathname).toEqual("/login") }) - - it("redirects when login is complete", async () => { - // Given - const { container } = render() - - // When user signs in - const [username, password] = container.querySelectorAll("input") - fireEvent.change(username, { target: { value: "test@coder.com" } }) - fireEvent.change(password, { target: { value: "password" } }) - const signInButton = await screen.findByText("Sign In") - act(() => signInButton.click()) - - // Then - await waitFor(() => expect(history.location.pathname).toEqual("/projects")) - }) - - it("respects ?redirect query parameter when complete", async () => { - // Given - const { container } = render() - // Set a path to redirect to after login is successful - act(() => history.replace("/login?redirect=%2Fsome%2Fother%2Fpath")) - console.log(history.location.pathname) - - // When user signs in - const [username, password] = container.querySelectorAll("input") - fireEvent.change(username, { target: { value: "test@coder.com" } }) - fireEvent.change(password, { target: { value: "password" } }) - const signInButton = await screen.findByText("Sign In") - act(() => signInButton.click()) - - // Then - await waitFor(() => expect(history.location.pathname).toEqual("/some/other/path")) - }) }) From f3902a1d73ec11a1f7750233072280271f0c5bcf Mon Sep 17 00:00:00 2001 From: Presley Date: Fri, 18 Mar 2022 00:05:21 +0000 Subject: [PATCH 35/42] Use a Context for MSW's sake --- site/src/app.tsx | 3 +++ site/src/components/Navbar/index.tsx | 7 +++--- site/src/components/Page/RequireAuth.tsx | 7 +++--- site/src/pages/cli-auth.tsx | 7 +++--- site/src/pages/login.tsx | 7 +++--- site/src/test_helpers/index.tsx | 3 +++ site/src/xServices/StateContext.tsx | 30 ++++++++++++++++++++++++ site/src/xServices/user/userXService.ts | 6 ++--- 8 files changed, 54 insertions(+), 16 deletions(-) create mode 100644 site/src/xServices/StateContext.tsx diff --git a/site/src/app.tsx b/site/src/app.tsx index c42d384728084..8e9f9c2a2610b 100644 --- a/site/src/app.tsx +++ b/site/src/app.tsx @@ -15,6 +15,7 @@ import { CreateWorkspacePage } from "./pages/projects/[organization]/[project]/c import { WorkspacePage } from "./pages/workspaces/[workspace]" import { HealthzPage } from "./pages/healthz" import { AuthAndNav, RequireAuth } from "./components/Page" +import { XServiceProvider } from "./xServices/StateContext" export const App: React.FC = () => { return ( @@ -37,6 +38,7 @@ export const App: React.FC = () => { }, }} > + @@ -102,6 +104,7 @@ export const App: React.FC = () => { + ) diff --git a/site/src/components/Navbar/index.tsx b/site/src/components/Navbar/index.tsx index 87d3e88058820..cddf9e5bd72f6 100644 --- a/site/src/components/Navbar/index.tsx +++ b/site/src/components/Navbar/index.tsx @@ -1,10 +1,11 @@ -import React from "react" +import React, { useContext } from "react" import { useActor } from "@xstate/react" -import { userXService } from "../../xServices/user/userXService" import { NavbarView } from "./NavbarView" +import { XServiceContext } from "../../xServices/StateContext" export const Navbar: React.FC = () => { - const [userState, userSend] = useActor(userXService) + const xServices = useContext(XServiceContext) + const [userState, userSend] = useActor(xServices.userXService); const { me } = userState.context const onSignOut = () => userSend("SIGN_OUT") diff --git a/site/src/components/Page/RequireAuth.tsx b/site/src/components/Page/RequireAuth.tsx index 88d132dd26adf..83c2ec4d8c4d3 100644 --- a/site/src/components/Page/RequireAuth.tsx +++ b/site/src/components/Page/RequireAuth.tsx @@ -1,7 +1,7 @@ import { useActor } from "@xstate/react" -import React from "react" +import React, { useContext } from "react" import { Navigate, useLocation } from "react-router" -import { userXService } from "../../xServices/user/userXService" +import { XServiceContext } from "../../xServices/StateContext" import { FullScreenLoader } from "../Loader/FullScreenLoader" export interface RequireAuthProps { @@ -9,7 +9,8 @@ export interface RequireAuthProps { } export const RequireAuth: React.FC = ({ children }) => { - const [userState] = useActor(userXService) + const xServices = useContext(XServiceContext) + const [userState] = useActor(xServices.userXService); const location = useLocation() if (userState.matches("signedOut") || !userState.context.me) { diff --git a/site/src/pages/cli-auth.tsx b/site/src/pages/cli-auth.tsx index 37d78324f561b..917354ebbd963 100644 --- a/site/src/pages/cli-auth.tsx +++ b/site/src/pages/cli-auth.tsx @@ -1,14 +1,15 @@ import { makeStyles } from "@material-ui/core/styles" -import React, { useEffect, useState } from "react" +import React, { useContext, useEffect, useState } from "react" import { getApiKey } from "../api" import { CliAuthToken } from "../components/SignIn" import { FullScreenLoader } from "../components/Loader/FullScreenLoader" import { useActor } from "@xstate/react" -import { userXService } from "../xServices/user/userXService" +import { XServiceContext } from "../xServices/StateContext" export const CliAuthenticationPage: React.FC = () => { - const [userState] = useActor(userXService) + const xServices = useContext(XServiceContext) + const [userState] = useActor(xServices.userXService); const { me } = userState.context const styles = useStyles() diff --git a/site/src/pages/login.tsx b/site/src/pages/login.tsx index 5745d50cbd2c7..36f30fec3ebb7 100644 --- a/site/src/pages/login.tsx +++ b/site/src/pages/login.tsx @@ -1,10 +1,10 @@ import { makeStyles } from "@material-ui/core/styles" import { useActor } from "@xstate/react" -import React from "react" +import React, { useContext } from "react" import { SignInForm } from "./../components/SignIn" import { Navigate, useLocation } from "react-router-dom" import { Location } from "history" -import { userXService } from "../xServices/user/userXService" +import { XServiceContext } from "../xServices/StateContext" export const useStyles = makeStyles((theme) => ({ root: { @@ -31,7 +31,8 @@ const getRedirectFromLocation = (location: Location) => { export const SignInPage: React.FC = () => { const styles = useStyles() const location = useLocation() - const [userState, userSend] = useActor(userXService) + const xServices = useContext(XServiceContext) + const [userState, userSend] = useActor(xServices.userXService); const isLoading = userState.hasTag("loading") const redirectTo = getRedirectFromLocation(location) const authErrorMessage = userState.context.authError ? (userState.context.authError as Error).message : undefined diff --git a/site/src/test_helpers/index.tsx b/site/src/test_helpers/index.tsx index a0a21239f4300..fae0de4ce46c6 100644 --- a/site/src/test_helpers/index.tsx +++ b/site/src/test_helpers/index.tsx @@ -5,13 +5,16 @@ import ThemeProvider from "@material-ui/styles/ThemeProvider" import { dark } from "../theme" import { createMemoryHistory } from "history" import { unstable_HistoryRouter as HistoryRouter } from "react-router-dom" +import { XServiceProvider } from "../xServices/StateContext" export const history = createMemoryHistory() export const WrapperComponent: React.FC = ({ children }) => { return ( + {children} + ) } diff --git a/site/src/xServices/StateContext.tsx b/site/src/xServices/StateContext.tsx new file mode 100644 index 0000000000000..3b16dd6fb0e9e --- /dev/null +++ b/site/src/xServices/StateContext.tsx @@ -0,0 +1,30 @@ +import React, { createContext } from 'react'; +import { useInterpret } from '@xstate/react'; +import { ActorRefFrom } from 'xstate'; +import { userMachine } from './user/userXService'; + +interface XServiceContextType { + userXService: ActorRefFrom; +} + +/** + * Consuming this Context will not automatically cause rerenders because + * the xServices in it are static references. + * + * To use one of the xServices, `useActor` will access all its state + * (causing re-renders for any changes to that one xService) and + * `useSelector` will access just one piece of state. + */ +export const XServiceContext = createContext( + {} as XServiceContextType, +); + +export const XServiceProvider: React.FC = ({ children }) => { + const userXService = useInterpret(userMachine); + + return ( + + {children} + + ); +}; \ No newline at end of file diff --git a/site/src/xServices/user/userXService.ts b/site/src/xServices/user/userXService.ts index 0df040433842c..cc67ad290f7a5 100644 --- a/site/src/xServices/user/userXService.ts +++ b/site/src/xServices/user/userXService.ts @@ -1,4 +1,4 @@ -import { createMachine, interpret, assign } from "xstate" +import { createMachine, assign } from "xstate" import * as Types from '../../api/types' import * as API from '../../api' @@ -10,7 +10,7 @@ export interface UserContext { export type UserEvent = { type: "SIGN_OUT" } | { type: "SIGN_IN"; email: string; password: string } -const userMachine = +export const userMachine = /** @xstate-layout N4IgpgJg5mDOIC5QFdZgE4GUAuBDbYAdLAJZQB2kA8stgMSYCSA4gHID6jrioADgPalsJfuR4gAHogDM0gJyEATAHYAbNIAscxdunKArKo0AaEAE9EG+YX0BGABz3FD6atsbbc5QF9vp1Bg4+ESkFCTkUIzkdBCiROEAbvwA1iFk5FHiAkIiYkiSiHIADITSOvbSRRqq2m7StqYWCIr2+ja2tvVV+vqaRVW+-mhYeATE6eGR0Rjo-OiEvAA2+ABmcwC24xSZ+dkkwqLiUghlyoRF+opayq4X+sqK+o2Il6qEcq5y+hrfjt+qgxAARGwUIMGwwgiAFVhjE4oREikiOCALJgLKCfa5I4yIyEZTFZQaezFK5lPTPBC2IqKUqKC4EjqqKrFOSA4FBMbgyFQGEYOgzOYLZbYNboTao9G7TEHPKgY7SewlTQ3dRyVQ6GpVSmKMqEIz2ZRKjrKIqqJzs4actIUSBRBgsDhUKEAFQxOUO+WOhvshA0ahualsqnu8kpthUhGplVUIa+rUq9ktgVGNvIkxo9FilAR5CSqS25Ez7qxnvliCMGlK1Tk6vN5p+T3ML0Ub2U7iKXmcGg8tmTILGoXTEUzAvQs3mS1WG0LxelHrlBQQIbasc7aiKtnbV3sOpaUZXBjkwd1A0B5H4EDg4g5qcL1FoJdlOIQdlpH2qhmJzJ+DWbCB7WxSmUIl2wJM0CSTPwgStO8h0mHY+BlbEvReICaSMbQflkU0fkpHtfS3MC3C+aR9ENfR+2tMEwAhSY+XQJ8UPLACziVS5TXKAxPDI8MaSjIojSqPQezNRUqLg9I7UXPZn1Q5dqn1CMNEEi4HAecNIyVGoIweVQDH0tloNvUF4JHR951LRdvQcc4PCcAx12-fDOnOeRTU7C4SXsPtjNg4ImLLJddUIdiVBpOQKJ4psmh0ASQOPRUiI8RRfF8IA */ createMachine( { @@ -143,5 +143,3 @@ const userMachine = }, }, ) - -export const userXService = interpret(userMachine, { devTools: true }).start() From 219bc4405e59fd6b2445c18522ffde10c36ed761 Mon Sep 17 00:00:00 2001 From: Presley Date: Fri, 18 Mar 2022 00:11:51 +0000 Subject: [PATCH 36/42] mocks -> test_helpers --- site/src/pages/login.test.tsx | 2 +- site/src/setupTests.ts | 2 +- site/src/{mocks => test_helpers}/entities.ts | 0 site/src/{mocks => test_helpers}/handlers.ts | 0 site/src/test_helpers/index.tsx | 2 +- site/src/{mocks => test_helpers}/server.ts | 0 6 files changed, 3 insertions(+), 3 deletions(-) rename site/src/{mocks => test_helpers}/entities.ts (100%) rename site/src/{mocks => test_helpers}/handlers.ts (100%) rename site/src/{mocks => test_helpers}/server.ts (100%) diff --git a/site/src/pages/login.test.tsx b/site/src/pages/login.test.tsx index 63a1f8123aae5..a8edec43be7de 100644 --- a/site/src/pages/login.test.tsx +++ b/site/src/pages/login.test.tsx @@ -2,7 +2,7 @@ import React from "react" import { act, fireEvent, screen, waitFor } from "@testing-library/react" import { history, render } from "../test_helpers" import { SignInPage } from "./login" -import { server } from "../mocks/server" +import { server } from "../test_helpers/server" import { rest } from "msw" describe("SignInPage", () => { diff --git a/site/src/setupTests.ts b/site/src/setupTests.ts index 2244ba9aa2481..9720883ba0844 100644 --- a/site/src/setupTests.ts +++ b/site/src/setupTests.ts @@ -1,4 +1,4 @@ -import { server } from './mocks/server' +import { server } from './test_helpers/server' // Establish API mocking before all tests. beforeAll(() => server.listen({ diff --git a/site/src/mocks/entities.ts b/site/src/test_helpers/entities.ts similarity index 100% rename from site/src/mocks/entities.ts rename to site/src/test_helpers/entities.ts diff --git a/site/src/mocks/handlers.ts b/site/src/test_helpers/handlers.ts similarity index 100% rename from site/src/mocks/handlers.ts rename to site/src/test_helpers/handlers.ts diff --git a/site/src/test_helpers/index.tsx b/site/src/test_helpers/index.tsx index fae0de4ce46c6..d4e6e15e2e804 100644 --- a/site/src/test_helpers/index.tsx +++ b/site/src/test_helpers/index.tsx @@ -23,4 +23,4 @@ export const render = (component: React.ReactElement): RenderResult => { return wrappedRender({component}) } -export * from "../mocks/entities" +export * from "./entities" diff --git a/site/src/mocks/server.ts b/site/src/test_helpers/server.ts similarity index 100% rename from site/src/mocks/server.ts rename to site/src/test_helpers/server.ts From 9d638ee71c4a55058e772cb5dc92df7a3375e03e Mon Sep 17 00:00:00 2001 From: Presley Date: Fri, 18 Mar 2022 00:27:20 +0000 Subject: [PATCH 37/42] Enable dev tools --- site/src/xServices/StateContext.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/xServices/StateContext.tsx b/site/src/xServices/StateContext.tsx index 3b16dd6fb0e9e..65dea6bccfd2f 100644 --- a/site/src/xServices/StateContext.tsx +++ b/site/src/xServices/StateContext.tsx @@ -20,7 +20,7 @@ export const XServiceContext = createContext( ); export const XServiceProvider: React.FC = ({ children }) => { - const userXService = useInterpret(userMachine); + const userXService = useInterpret(userMachine, { devTools: true }); return ( From b4bb44eec29dab103d26b2405f069da18c5c1f0a Mon Sep 17 00:00:00 2001 From: Presley Date: Fri, 18 Mar 2022 00:37:28 +0000 Subject: [PATCH 38/42] Format --- site/src/app.tsx | 88 +++++++++---------- .../components/Navbar/NavbarView.stories.tsx | 5 +- site/src/components/Navbar/NavbarView.tsx | 2 +- site/src/components/Navbar/index.tsx | 2 +- site/src/components/Page/AuthAndNav.tsx | 8 +- site/src/components/Page/RequireAuth.tsx | 3 +- site/src/components/SignIn/SignInForm.tsx | 8 +- site/src/pages/cli-auth.tsx | 2 +- site/src/pages/login.test.tsx | 14 ++- site/src/pages/login.tsx | 2 +- site/src/setupTests.ts | 12 +-- site/src/test_helpers/entities.ts | 2 +- site/src/test_helpers/handlers.ts | 8 +- site/src/test_helpers/index.tsx | 2 +- site/src/test_helpers/server.ts | 6 +- site/src/xServices/StateContext.tsx | 30 +++---- site/src/xServices/user/userXService.ts | 4 +- 17 files changed, 96 insertions(+), 102 deletions(-) diff --git a/site/src/app.tsx b/site/src/app.tsx index 8e9f9c2a2610b..e375f60d5810f 100644 --- a/site/src/app.tsx +++ b/site/src/app.tsx @@ -39,71 +39,71 @@ export const App: React.FC = () => { }} > - - + + - - - - - - } - /> - - } /> - } /> - } /> - - + + - - + + + } /> - + + } /> + } /> + } /> + + - + } /> + + + + + } + /> + + + + } + /> + + + + - - + + + } /> - - - - - - } - /> - - - {/* Using path="*"" means "match anything", so this route + {/* Using path="*"" means "match anything", so this route acts like a catch-all for URLs that we don't have explicit routes for. */} - } /> - - - + } /> + + + diff --git a/site/src/components/Navbar/NavbarView.stories.tsx b/site/src/components/Navbar/NavbarView.stories.tsx index afa202eded697..d6e9e5d30493b 100644 --- a/site/src/components/Navbar/NavbarView.stories.tsx +++ b/site/src/components/Navbar/NavbarView.stories.tsx @@ -1,4 +1,3 @@ - import { Story } from "@storybook/react" import React from "react" import { NavbarView, NavbarViewProps } from "./NavbarView" @@ -15,8 +14,8 @@ const Template: Story = (args: NavbarViewProps) => { return Promise.resolve() }, -} \ No newline at end of file +} diff --git a/site/src/components/Navbar/NavbarView.tsx b/site/src/components/Navbar/NavbarView.tsx index 2e0d448a1c512..b6e05c2b10c1e 100644 --- a/site/src/components/Navbar/NavbarView.tsx +++ b/site/src/components/Navbar/NavbarView.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React from "react" import Button from "@material-ui/core/Button" import { makeStyles } from "@material-ui/core/styles" import { Link } from "react-router-dom" diff --git a/site/src/components/Navbar/index.tsx b/site/src/components/Navbar/index.tsx index cddf9e5bd72f6..b1f751b4fa1f6 100644 --- a/site/src/components/Navbar/index.tsx +++ b/site/src/components/Navbar/index.tsx @@ -5,7 +5,7 @@ import { XServiceContext } from "../../xServices/StateContext" export const Navbar: React.FC = () => { const xServices = useContext(XServiceContext) - const [userState, userSend] = useActor(xServices.userXService); + const [userState, userSend] = useActor(xServices.userXService) const { me } = userState.context const onSignOut = () => userSend("SIGN_OUT") diff --git a/site/src/components/Page/AuthAndNav.tsx b/site/src/components/Page/AuthAndNav.tsx index 65e80e88e1d26..9d400828d78c1 100644 --- a/site/src/components/Page/AuthAndNav.tsx +++ b/site/src/components/Page/AuthAndNav.tsx @@ -1,6 +1,6 @@ -import React from "react"; -import { Navbar } from "../Navbar"; -import { RequireAuth, RequireAuthProps } from "./RequireAuth"; +import React from "react" +import { Navbar } from "../Navbar" +import { RequireAuth, RequireAuthProps } from "./RequireAuth" export const AuthAndNav: React.FC = ({ children }) => ( @@ -9,4 +9,4 @@ export const AuthAndNav: React.FC = ({ children }) => ( {children} -) \ No newline at end of file +) diff --git a/site/src/components/Page/RequireAuth.tsx b/site/src/components/Page/RequireAuth.tsx index 83c2ec4d8c4d3..099713cc1d286 100644 --- a/site/src/components/Page/RequireAuth.tsx +++ b/site/src/components/Page/RequireAuth.tsx @@ -10,7 +10,7 @@ export interface RequireAuthProps { export const RequireAuth: React.FC = ({ children }) => { const xServices = useContext(XServiceContext) - const [userState] = useActor(xServices.userXService); + const [userState] = useActor(xServices.userXService) const location = useLocation() if (userState.matches("signedOut") || !userState.context.me) { @@ -21,4 +21,3 @@ export const RequireAuth: React.FC = ({ children }) => { return children } } - diff --git a/site/src/components/SignIn/SignInForm.tsx b/site/src/components/SignIn/SignInForm.tsx index 9899f234e7c3f..0bd9bbc3cea1f 100644 --- a/site/src/components/SignIn/SignInForm.tsx +++ b/site/src/components/SignIn/SignInForm.tsx @@ -87,7 +87,11 @@ export const SignInForm: React.FC = ({ isLoading, authErrorMess placeholder="Password" variant="outlined" /> - {authErrorMessage && {authErrorMessage}} + {authErrorMessage && ( + + {authErrorMessage} + + )}
= ({ isLoading, authErrorMess type="submit" variant="contained" > - { isLoading ? '' : "Sign In" } + {isLoading ? "" : "Sign In"}
diff --git a/site/src/pages/cli-auth.tsx b/site/src/pages/cli-auth.tsx index 917354ebbd963..26620a9d0bac3 100644 --- a/site/src/pages/cli-auth.tsx +++ b/site/src/pages/cli-auth.tsx @@ -9,7 +9,7 @@ import { XServiceContext } from "../xServices/StateContext" export const CliAuthenticationPage: React.FC = () => { const xServices = useContext(XServiceContext) - const [userState] = useActor(xServices.userXService); + const [userState] = useActor(xServices.userXService) const { me } = userState.context const styles = useStyles() diff --git a/site/src/pages/login.test.tsx b/site/src/pages/login.test.tsx index a8edec43be7de..a82a461ef4c73 100644 --- a/site/src/pages/login.test.tsx +++ b/site/src/pages/login.test.tsx @@ -10,11 +10,9 @@ describe("SignInPage", () => { history.replace("/login") // appear logged out server.use( - rest.get('/api/v2/users/me', (req, res, ctx) => { - return res( - ctx.status(401), - ctx.json({message: 'no user here'})) - }) + rest.get("/api/v2/users/me", (req, res, ctx) => { + return res(ctx.status(401), ctx.json({ message: "no user here" })) + }), ) }) @@ -31,10 +29,8 @@ describe("SignInPage", () => { const { container } = render() // Make login fail server.use( - rest.post('/api/v2/users/login', async (req, res, ctx) => { - return res( - ctx.status(500), - ctx.json({message: 'nope'})) + rest.post("/api/v2/users/login", async (req, res, ctx) => { + return res(ctx.status(500), ctx.json({ message: "nope" })) }), ) diff --git a/site/src/pages/login.tsx b/site/src/pages/login.tsx index 36f30fec3ebb7..f6f2c08f67917 100644 --- a/site/src/pages/login.tsx +++ b/site/src/pages/login.tsx @@ -32,7 +32,7 @@ export const SignInPage: React.FC = () => { const styles = useStyles() const location = useLocation() const xServices = useContext(XServiceContext) - const [userState, userSend] = useActor(xServices.userXService); + const [userState, userSend] = useActor(xServices.userXService) const isLoading = userState.hasTag("loading") const redirectTo = getRedirectFromLocation(location) const authErrorMessage = userState.context.authError ? (userState.context.authError as Error).message : undefined diff --git a/site/src/setupTests.ts b/site/src/setupTests.ts index 9720883ba0844..8c82b55de2949 100644 --- a/site/src/setupTests.ts +++ b/site/src/setupTests.ts @@ -1,13 +1,15 @@ -import { server } from './test_helpers/server' +import { server } from "./test_helpers/server" // Establish API mocking before all tests. -beforeAll(() => server.listen({ - onUnhandledRequest: "warn" -})) +beforeAll(() => + server.listen({ + onUnhandledRequest: "warn", + }), +) // Reset any request handlers that we may add during the tests, // so they don't affect other tests. afterEach(() => server.resetHandlers()) // Clean up after the tests are finished. -afterAll(() => server.close()) \ No newline at end of file +afterAll(() => server.close()) diff --git a/site/src/test_helpers/entities.ts b/site/src/test_helpers/entities.ts index d2d9656cc96bd..7633161b4a23a 100644 --- a/site/src/test_helpers/entities.ts +++ b/site/src/test_helpers/entities.ts @@ -2,7 +2,7 @@ import { Provisioner, Organization, Project, Workspace, UserResponse } from "../ export const MockSessionToken = { session_token: "my-session-token" } -export const MockAPIKey = { key: "my-api-key" } +export const MockAPIKey = { key: "my-api-key" } export const MockUser: UserResponse = { id: "test-user-id", diff --git a/site/src/test_helpers/handlers.ts b/site/src/test_helpers/handlers.ts index f296f54a680de..c7ae6df9481d9 100644 --- a/site/src/test_helpers/handlers.ts +++ b/site/src/test_helpers/handlers.ts @@ -1,5 +1,5 @@ -import { rest } from 'msw' -import * as M from './entities' +import { rest } from "msw" +import * as M from "./entities" export const handlers = [ rest.post("/api/v2/users/me/workspaces", async (req, res, ctx) => { @@ -16,5 +16,5 @@ export const handlers = [ }), rest.get("/api/v2/users/me/keys", async (req, res, ctx) => { return res(ctx.status(200), ctx.json(M.MockAPIKey)) - }) -] \ No newline at end of file + }), +] diff --git a/site/src/test_helpers/index.tsx b/site/src/test_helpers/index.tsx index d4e6e15e2e804..ce192658cc757 100644 --- a/site/src/test_helpers/index.tsx +++ b/site/src/test_helpers/index.tsx @@ -13,7 +13,7 @@ export const WrapperComponent: React.FC = ({ children }) => { return ( - {children} + {children} ) diff --git a/site/src/test_helpers/server.ts b/site/src/test_helpers/server.ts index fadac07f29ad2..104074528453c 100644 --- a/site/src/test_helpers/server.ts +++ b/site/src/test_helpers/server.ts @@ -1,5 +1,5 @@ -import { setupServer } from 'msw/node' -import { handlers } from './handlers' +import { setupServer } from "msw/node" +import { handlers } from "./handlers" // This configures a request mocking server with the given request handlers. -export const server = setupServer(...handlers) \ No newline at end of file +export const server = setupServer(...handlers) diff --git a/site/src/xServices/StateContext.tsx b/site/src/xServices/StateContext.tsx index 65dea6bccfd2f..6086a112e0815 100644 --- a/site/src/xServices/StateContext.tsx +++ b/site/src/xServices/StateContext.tsx @@ -1,30 +1,24 @@ -import React, { createContext } from 'react'; -import { useInterpret } from '@xstate/react'; -import { ActorRefFrom } from 'xstate'; -import { userMachine } from './user/userXService'; +import React, { createContext } from "react" +import { useInterpret } from "@xstate/react" +import { ActorRefFrom } from "xstate" +import { userMachine } from "./user/userXService" interface XServiceContextType { - userXService: ActorRefFrom; + userXService: ActorRefFrom } /** * Consuming this Context will not automatically cause rerenders because * the xServices in it are static references. - * - * To use one of the xServices, `useActor` will access all its state - * (causing re-renders for any changes to that one xService) and + * + * To use one of the xServices, `useActor` will access all its state + * (causing re-renders for any changes to that one xService) and * `useSelector` will access just one piece of state. */ -export const XServiceContext = createContext( - {} as XServiceContextType, -); +export const XServiceContext = createContext({} as XServiceContextType) export const XServiceProvider: React.FC = ({ children }) => { - const userXService = useInterpret(userMachine, { devTools: true }); + const userXService = useInterpret(userMachine, { devTools: true }) - return ( - - {children} - - ); -}; \ No newline at end of file + return {children} +} diff --git a/site/src/xServices/user/userXService.ts b/site/src/xServices/user/userXService.ts index cc67ad290f7a5..bcbbe12868418 100644 --- a/site/src/xServices/user/userXService.ts +++ b/site/src/xServices/user/userXService.ts @@ -1,6 +1,6 @@ import { createMachine, assign } from "xstate" -import * as Types from '../../api/types' -import * as API from '../../api' +import * as Types from "../../api/types" +import * as API from "../../api" export interface UserContext { getUserError?: Error | unknown // unknown is a concession while I work out typing issues From 9c40fc594f42af6dd7269a8025e5755cdf239976 Mon Sep 17 00:00:00 2001 From: Presley Date: Fri, 18 Mar 2022 00:41:30 +0000 Subject: [PATCH 39/42] Fix import --- site/src/components/Navbar/NavbarView.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/site/src/components/Navbar/NavbarView.test.tsx b/site/src/components/Navbar/NavbarView.test.tsx index 58a53f658a7f9..cc1089cb1dd78 100644 --- a/site/src/components/Navbar/NavbarView.test.tsx +++ b/site/src/components/Navbar/NavbarView.test.tsx @@ -1,7 +1,8 @@ import React from "react" import { screen } from "@testing-library/react" -import { render, MockUser } from "../../test_helpers" +import { render } from "../../test_helpers" +import { MockUser } from "../../test_helpers/entities" import { NavbarView } from "./NavbarView" describe("NavbarView", () => { From e0f4d7abfd416b4822cd78294f1d5d7571c2d92e Mon Sep 17 00:00:00 2001 From: Presley Date: Fri, 18 Mar 2022 14:25:13 +0000 Subject: [PATCH 40/42] Fixes --- site/.eslintignore | 1 + site/package.json | 3 ++- site/src/api/types.ts | 4 ---- yarn.lock | 4 ---- 4 files changed, 3 insertions(+), 9 deletions(-) delete mode 100644 yarn.lock diff --git a/site/.eslintignore b/site/.eslintignore index 0f3e9e0e60fcb..c416c4be3c484 100644 --- a/site/.eslintignore +++ b/site/.eslintignore @@ -8,3 +8,4 @@ coverage .next storybook-static test-results +site/**/*.typegen.ts diff --git a/site/package.json b/site/package.json index 6318ea094dcfe..e581498f9b40d 100644 --- a/site/package.json +++ b/site/package.json @@ -18,13 +18,13 @@ "storybook:build": "build-storybook", "test": "jest --selectProjects test", "test:coverage": "jest --selectProjects test --collectCoverage", + "test:watch": "jest --selectProjects test --watch", "typegen": "xstate typegen 'src/**/*.ts'" }, "dependencies": { "@material-ui/core": "4.9.4", "@material-ui/icons": "4.5.1", "@material-ui/lab": "4.0.0-alpha.42", - "@xstate/cli": "^0.1.4", "@xstate/react": "^2.0.1", "axios": "0.26.1", "formik": "2.2.9", @@ -53,6 +53,7 @@ "@types/superagent": "4.1.15", "@typescript-eslint/eslint-plugin": "5.15.0", "@typescript-eslint/parser": "5.15.0", + "@xstate/cli": "^0.1.4", "copy-webpack-plugin": "10.2.4", "eslint": "8.11.0", "eslint-config-prettier": "8.5.0", diff --git a/site/src/api/types.ts b/site/src/api/types.ts index 0c9775f15aee0..376ffbc7761ef 100644 --- a/site/src/api/types.ts +++ b/site/src/api/types.ts @@ -56,10 +56,6 @@ export interface Workspace { name: string } -export interface LoginResponse { - session_token: string -} - export interface APIKeyResponse { key: string } diff --git a/yarn.lock b/yarn.lock deleted file mode 100644 index fb57ccd13afbd..0000000000000 --- a/yarn.lock +++ /dev/null @@ -1,4 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - From 42f05546ecbd7d0072ca6b9d5a735a1bdc55dfd4 Mon Sep 17 00:00:00 2001 From: Presley Date: Fri, 18 Mar 2022 14:32:05 +0000 Subject: [PATCH 41/42] Lint --- site/.eslintignore | 2 +- site/src/components/SignIn/SignInForm.tsx | 2 +- site/src/pages/login.test.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/site/.eslintignore b/site/.eslintignore index c416c4be3c484..d582fd9cc67ed 100644 --- a/site/.eslintignore +++ b/site/.eslintignore @@ -8,4 +8,4 @@ coverage .next storybook-static test-results -site/**/*.typegen.ts +**/*.typegen.ts diff --git a/site/src/components/SignIn/SignInForm.tsx b/site/src/components/SignIn/SignInForm.tsx index 0bd9bbc3cea1f..3c52f3d936fd8 100644 --- a/site/src/components/SignIn/SignInForm.tsx +++ b/site/src/components/SignIn/SignInForm.tsx @@ -88,7 +88,7 @@ export const SignInForm: React.FC = ({ isLoading, authErrorMess variant="outlined" /> {authErrorMessage && ( - + {authErrorMessage} )} diff --git a/site/src/pages/login.test.tsx b/site/src/pages/login.test.tsx index a82a461ef4c73..7b2abb966d4c9 100644 --- a/site/src/pages/login.test.tsx +++ b/site/src/pages/login.test.tsx @@ -1,5 +1,5 @@ import React from "react" -import { act, fireEvent, screen, waitFor } from "@testing-library/react" +import { act, fireEvent, screen } from "@testing-library/react" import { history, render } from "../test_helpers" import { SignInPage } from "./login" import { server } from "../test_helpers/server" From 2be726bc4af3344c4be666970980e370490c28a1 Mon Sep 17 00:00:00 2001 From: Presley Date: Fri, 18 Mar 2022 14:48:32 +0000 Subject: [PATCH 42/42] run typegen postinstall --- site/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/site/package.json b/site/package.json index e581498f9b40d..f20d3ccabb8f6 100644 --- a/site/package.json +++ b/site/package.json @@ -4,6 +4,7 @@ "repository": "https://github.com/coder/coder", "private": true, "scripts": { + "postinstall": "yarn typegen", "build": "NODE_ENV=production webpack build --config=webpack.prod.ts", "build:analyze": "NODE_ENV=production webpack --profile --progress --json --config=webpack.prod.ts > out/stats.json && webpack-bundle-analyzer out/stats.json out", "dev": "webpack-dev-server --config=webpack.dev.ts",