diff --git a/bau-ui/autocomplete/autocomplete.js b/bau-ui/autocomplete/autocomplete.js index 248a8863..cc333670 100644 --- a/bau-ui/autocomplete/autocomplete.js +++ b/bau-ui/autocomplete/autocomplete.js @@ -64,7 +64,9 @@ export default function (context, componentOptions = {}) { placeholder, Option, options, - getOptionLabel = ({ label }) => label, + defaultOption, + getOptionLabel, + getOptionValue, onSelect = () => {}, id, required, @@ -81,14 +83,9 @@ export default function (context, componentOptions = {}) { const List = list(context); const Spinner = spinner(context, { variant, color, size }); - const selectedState = bau.state(props.value); - bau.derive(() => { - if (selectedState.val) { - inputShadowEl.value = getOptionLabel(selectedState.val); - onSelect(selectedState.val); - } - }); - const inputState = bau.state(""); + const selectedState = bau.state(defaultOption); + + const inputState = bau.state(props.value); const openState = bau.state(false); const itemIndexActive = bau.state(0); @@ -267,6 +264,13 @@ export default function (context, componentOptions = {}) { `, }); + bau.derive(() => { + if (selectedState.val) { + inputShadowEl.value = getOptionValue(selectedState.val); + onSelect(selectedState.val); + } + }); + return div( { ...props, diff --git a/bau-ui/autocomplete/index.d.ts b/bau-ui/autocomplete/index.d.ts index a793bfa9..56fea783 100644 --- a/bau-ui/autocomplete/index.d.ts +++ b/bau-ui/autocomplete/index.d.ts @@ -8,8 +8,10 @@ declare module "@grucloud/bau-ui/autocomplete" { label: string; placeholder: string; size?: string; - getOptionLabel?: (item: object | string) => string; + getOptionLabel: (item: object | string) => HTMLElement | string; + getOptionValue: (item: object | string) => string; Option: (props) => HTMLElement; + defaultOption?: any; } & DefaultDesignProps; type Component = import("../bau-ui").Component< diff --git a/bau-ui/examples/bau-storybook/src/pages/autocomplete/autocomplete-default-option.ts b/bau-ui/examples/bau-storybook/src/pages/autocomplete/autocomplete-default-option.ts new file mode 100644 index 00000000..9a644214 --- /dev/null +++ b/bau-ui/examples/bau-storybook/src/pages/autocomplete/autocomplete-default-option.ts @@ -0,0 +1,42 @@ +import { Context } from "@grucloud/bau-ui/context"; +import autocomplete from "@grucloud/bau-ui/autocomplete"; + +export default (context: Context) => { + const { bau, css } = context; + const { section, div, span } = bau.tags; + + const Autocomplete = autocomplete(context); + + const defaultCode = "AD"; + + const options = [ + { code: "AD", label: "Andorra", phone: "376" }, + { code: "AF", label: "Afghanistan", phone: "93" }, + ]; + + const Option = (option: any) => + div( + { + class: css` + display: flex; + justify-content: space-between; + gap: 0.5rem; + `, + }, + span(option.label), + span(option.code) + ); + return () => + section( + Autocomplete({ + options, + Option, + defaultOption: options.find(({ code }) => code == defaultCode), + getOptionValue: ({ code }: any) => code, + getOptionLabel: ({ label }: any) => label, + label: "Country", + placeholder: "Search countries", + id: "country", + }) + ); +}; diff --git a/bau-ui/examples/bau-storybook/src/pages/autocomplete/autocomplete-example-default.ts b/bau-ui/examples/bau-storybook/src/pages/autocomplete/autocomplete-example-default.ts index aad3a41c..d0653e5f 100644 --- a/bau-ui/examples/bau-storybook/src/pages/autocomplete/autocomplete-example-default.ts +++ b/bau-ui/examples/bau-storybook/src/pages/autocomplete/autocomplete-example-default.ts @@ -30,6 +30,7 @@ export default (context: Context) => { Autocomplete({ options, Option, + getOptionValue: ({ code }: any) => code, getOptionLabel: ({ label }: any) => label, label: "Country", placeholder: "Search countries", diff --git a/bau-ui/examples/bau-storybook/src/pages/autocomplete/autocomplete-grid-item.ts b/bau-ui/examples/bau-storybook/src/pages/autocomplete/autocomplete-grid-item.ts index f77aa0cc..f96284d3 100644 --- a/bau-ui/examples/bau-storybook/src/pages/autocomplete/autocomplete-grid-item.ts +++ b/bau-ui/examples/bau-storybook/src/pages/autocomplete/autocomplete-grid-item.ts @@ -30,6 +30,7 @@ export default (context: Context, componentsOptions?: any) => { ...props, options, Option, + getOptionValue: ({ code }: any) => code, getOptionLabel: ({ label }: any) => label, label: "Country", placeholder: "Search countries", diff --git a/bau-ui/examples/bau-storybook/src/pages/autocomplete/autocomplete-loading.ts b/bau-ui/examples/bau-storybook/src/pages/autocomplete/autocomplete-loading.ts index f2d89125..dc87e003 100644 --- a/bau-ui/examples/bau-storybook/src/pages/autocomplete/autocomplete-loading.ts +++ b/bau-ui/examples/bau-storybook/src/pages/autocomplete/autocomplete-loading.ts @@ -66,6 +66,7 @@ export default (context: Context) => { Autocomplete({ options: dataState.val, Option, + getOptionValue: ({ name }: any) => name.common, getOptionLabel: ({ name }: any) => name.common, label: "Country", placeholder: "Search countries", diff --git a/bau-ui/examples/bau-storybook/src/pages/autocomplete/autocomplete.examples.ts b/bau-ui/examples/bau-storybook/src/pages/autocomplete/autocomplete.examples.ts index 3c4b8d06..e46b89dc 100644 --- a/bau-ui/examples/bau-storybook/src/pages/autocomplete/autocomplete.examples.ts +++ b/bau-ui/examples/bau-storybook/src/pages/autocomplete/autocomplete.examples.ts @@ -7,6 +7,10 @@ import autocompleteDefault from "./autocomplete-example-default.ts"; // @ts-ignore import codeExampleDefault from "./autocomplete-example-default.ts?raw"; +import autocompleteDefaultOption from "./autocomplete-default-option.ts"; +// @ts-ignore +import codeDefaultOption from "./autocomplete-default-option.ts?raw"; + import autocompleteLoading from "./autocomplete-loading.ts"; // @ts-ignore import codeExampleLoading from "./autocomplete-loading.ts?raw"; @@ -31,6 +35,12 @@ export const autocompleteSpec = { code: codeExampleLoading, createComponent: autocompleteLoading, }, + { + title: "Default Option", + description: "A autocomplete with a default option.", + code: codeDefaultOption, + createComponent: autocompleteDefaultOption, + }, ], gridItem: autocompleteGridItem, }; diff --git a/bau-ui/examples/bau-storybook/src/pages/select/select-aws-region.ts b/bau-ui/examples/bau-storybook/src/pages/select/select-aws-region.ts index 504ec826..ed47cf70 100644 --- a/bau-ui/examples/bau-storybook/src/pages/select/select-aws-region.ts +++ b/bau-ui/examples/bau-storybook/src/pages/select/select-aws-region.ts @@ -35,6 +35,7 @@ export default (context: Context) => { options, Option, label: "Select a region", + getOptionValue: (label: any) => label, getOptionLabel: (label: any) => label, }) ); diff --git a/bau-ui/examples/bau-storybook/src/pages/select/select-default-option.ts b/bau-ui/examples/bau-storybook/src/pages/select/select-default-option.ts new file mode 100644 index 00000000..194c9b3d --- /dev/null +++ b/bau-ui/examples/bau-storybook/src/pages/select/select-default-option.ts @@ -0,0 +1,46 @@ +import select from "@grucloud/bau-ui/select"; +import { Context } from "@grucloud/bau-ui/context"; + +export default (context: Context) => { + const { bau, css } = context; + const { section, div, span } = bau.tags; + + const Select = select(context); + + const defaultCode = "AD"; + + const options = [ + { code: "AD", label: "Andorra", phone: "376" }, + { + code: "AE", + label: "United Arab Emirates", + phone: "971", + }, + { code: "AF", label: "Afghanistan", phone: "93" }, + ]; + + const Option = (option: any) => + div( + { + class: css` + display: flex; + justify-content: space-between; + gap: 0.5rem; + `, + }, + span(option.label), + span(option.code) + ); + + return () => + section( + Select({ + options, + Option, + defaultOption: options.find(({ code }) => code == defaultCode), + getOptionValue: ({ code }: any) => code, + getOptionLabel: ({ label }: any) => label, + label: "Select a country...", + }) + ); +}; diff --git a/bau-ui/examples/bau-storybook/src/pages/select/select-example-default.ts b/bau-ui/examples/bau-storybook/src/pages/select/select-example-default.ts index 8b6fcf55..38d8334f 100644 --- a/bau-ui/examples/bau-storybook/src/pages/select/select-example-default.ts +++ b/bau-ui/examples/bau-storybook/src/pages/select/select-example-default.ts @@ -35,6 +35,7 @@ export default (context: Context) => { Select({ options, Option, + getOptionValue: ({ code }: any) => code, getOptionLabel: ({ label }: any) => label, label: "Select a country...", }) diff --git a/bau-ui/examples/bau-storybook/src/pages/select/select-grid-item.ts b/bau-ui/examples/bau-storybook/src/pages/select/select-grid-item.ts index 685a9919..8f8fbf1d 100644 --- a/bau-ui/examples/bau-storybook/src/pages/select/select-grid-item.ts +++ b/bau-ui/examples/bau-storybook/src/pages/select/select-grid-item.ts @@ -35,6 +35,7 @@ export default (context: Context, componentOptions?: any) => { ...props, options, Option, + getOptionValue: ({ code }: any) => code, getOptionLabel: ({ label }: any) => label, label: "Select a country...", }); diff --git a/bau-ui/examples/bau-storybook/src/pages/select/select-loading.ts b/bau-ui/examples/bau-storybook/src/pages/select/select-loading.ts new file mode 100644 index 00000000..42e5d81e --- /dev/null +++ b/bau-ui/examples/bau-storybook/src/pages/select/select-loading.ts @@ -0,0 +1,78 @@ +import { Context } from "@grucloud/bau-ui/context"; +import select from "@grucloud/bau-ui/select"; +import button from "@grucloud/bau-ui/button"; + +export default (context: Context) => { + const { bau, css } = context; + const { section, div, span } = bau.tags; + + const Button = button(context, { variant: "outline" }); + const Select = select(context); + + const dataState = bau.state([]); + const loadingState = bau.state(false); + const errorMessageState = bau.state(""); + + async function fetchData({ url, transform = (x: any) => x }: any) { + try { + loadingState.val = true; + const response = await fetch(url, {}); + if (response.ok) { + const json = await response.json(); + dataState.val = transform(json); + } else { + errorMessageState.val = response.statusText; + } + } catch (error: any) { + errorMessageState.val = error.message; + } finally { + loadingState.val = false; + } + } + const fetchCountries = () => + fetchData({ + url: "https://restcountries.com/v3.1/all?fields=name,flag", + transform: (data: any) => + data.sort((a: any, b: any) => + a.name.common.localeCompare(b.name.common) + ), + }); + + fetchCountries(); + + const Option = (option: any) => + div( + { + class: css` + display: flex; + justify-content: space-between; + gap: 0.5rem; + `, + }, + span(option.flag), + span(option.name.common) + ); + + return () => + section( + div( + { + class: css` + display: flex; + gap: 1rem; + `, + }, + () => + Select({ + options: dataState.val, + Option, + getOptionValue: ({ name }: any) => name.common, + getOptionLabel: ({ name }: any) => name.common, + label: "Country", + id: "country", + loading: loadingState.val, + }), + Button({ onclick: () => fetchCountries() }, "Reload") + ) + ); +}; diff --git a/bau-ui/examples/bau-storybook/src/pages/select/select.examples.ts b/bau-ui/examples/bau-storybook/src/pages/select/select.examples.ts index 04cc2ae3..f2fd3d36 100644 --- a/bau-ui/examples/bau-storybook/src/pages/select/select.examples.ts +++ b/bau-ui/examples/bau-storybook/src/pages/select/select.examples.ts @@ -8,10 +8,18 @@ import selectDefault from "./select-example-default.ts"; // @ts-ignore import codeExampleDefault from "./select-example-default.ts?raw"; +import selectDefaultOption from "./select-default-option.ts"; +// @ts-ignore +import codeDefaultOption from "./select-default-option.ts?raw"; + import selectAwsRegion from "./select-aws-region.ts"; // @ts-ignore import codeExampleAwsRegion from "./select-aws-region.ts?raw"; +import selectLoading from "./select-loading.ts"; +// @ts-ignore +import codeExampleLoading from "./select-loading.ts?raw"; + export const selectSpec = { title: "Select", package: "select", @@ -26,12 +34,24 @@ export const selectSpec = { code: codeExampleDefault, createComponent: selectDefault, }, + { + title: "Default Option", + description: "Select with a default option", + code: codeDefaultOption, + createComponent: selectDefaultOption, + }, { title: "Select AWS region", description: "Select the AWS region", code: codeExampleAwsRegion, createComponent: selectAwsRegion, }, + { + title: "Loading Indicator", + description: "Select with a loading indicator", + code: codeExampleLoading, + createComponent: selectLoading, + }, ], gridItem: selectGridItem, }; diff --git a/bau-ui/examples/bau-storybook/src/pages/stepper/cloud-config/configGoogle.ts b/bau-ui/examples/bau-storybook/src/pages/stepper/cloud-config/configGoogle.ts index e91f92d3..41ead9e4 100644 --- a/bau-ui/examples/bau-storybook/src/pages/stepper/cloud-config/configGoogle.ts +++ b/bau-ui/examples/bau-storybook/src/pages/stepper/cloud-config/configGoogle.ts @@ -47,7 +47,6 @@ export default (context: Context) => { reader.readAsText(file); reader.onload = () => { try { - debugger; if (reader.result) { // @ts-ignore const contentJson = JSON.parse(reader.result); diff --git a/bau-ui/select/index.d.ts b/bau-ui/select/index.d.ts index bc1f783a..c1ed407e 100644 --- a/bau-ui/select/index.d.ts +++ b/bau-ui/select/index.d.ts @@ -6,7 +6,9 @@ declare module "@grucloud/bau-ui/select" { options: object[]; id?: string; label: string; - getOptionLabel?: (item: object | string) => string; + defaultOption?: any; + getOptionLabel: (item: object | string) => HTMLElement | string; + getOptionValue: (item: object | string) => string; Option: (props) => HTMLElement; } & DefaultDesignProps; diff --git a/bau-ui/select/select.js b/bau-ui/select/select.js index 6c8a7847..f29418d8 100644 --- a/bau-ui/select/select.js +++ b/bau-ui/select/select.js @@ -3,6 +3,7 @@ import { toPropsAndChildren } from "@grucloud/bau/bau.js"; import popover from "../popover/popover.js"; import button from "../button/button.js"; import list from "../list/list.js"; +import spinner from "../spinner/spinner.js"; import { Colors } from "../constants.js"; @@ -35,7 +36,9 @@ export default function (context, componentOptions = {}) { & button { &::after { content: "\u25BC"; - padding: 0.3rem; + } + &.loading::after { + display: none; } } ${colorsToCss()} @@ -50,13 +53,21 @@ export default function (context, componentOptions = {}) { label, Option, options, - getOptionLabel = ({ label }) => label, + defaultOption, + getOptionLabel, + getOptionValue, + onSelect = () => {}, + loading, ...props }, ...children ] = toPropsAndChildren(args); - const inputState = bau.state(props.value); + const Spinner = spinner(context, { variant, color, size }); + + const inputState = bau.state( + defaultOption ? getOptionLabel(defaultOption) : label + ); const openState = bau.state(false); const itemIndexActive = bau.state(0); @@ -84,10 +95,11 @@ export default function (context, componentOptions = {}) { ({ option, index }) => (event) => { inputState.val = getOptionLabel(option); - selectEl.value = inputState.val; + selectEl.value = getOptionValue(option); selectEl.setCustomValidity(""); itemIndexActive.val = index; dialogClose(); + onSelect(option); event.preventDefault(); }; @@ -114,6 +126,7 @@ export default function (context, componentOptions = {}) { case "Enter": if (popoverEl.open) { inputState.val = getOptionLabel(options[itemIndexActive.val]); + selectEl.value = getOptionValue(option); dialogClose(); } else { dialogOpen(); @@ -147,9 +160,12 @@ export default function (context, componentOptions = {}) { color, variant, size, + class: loading == true && "loading", + disabled: loading, }, () => !inputState.val && label, - inputState + inputState, + () => loading == true && Spinner({ visibility: loading }) ); const popoverEl = Popover({ @@ -162,7 +178,9 @@ export default function (context, componentOptions = {}) { const selectEl = select( props, option({ value: "" }, "--Select Category--"), - options.map((opt) => option(getOptionLabel(opt))) + options.map((opt) => + option({ value: getOptionValue(opt) }, getOptionLabel(opt)) + ) ); selectEl.value = props.value; diff --git a/bau-ui/tabs/tabs.js b/bau-ui/tabs/tabs.js index 0e12b610..7bb18eb9 100644 --- a/bau-ui/tabs/tabs.js +++ b/bau-ui/tabs/tabs.js @@ -33,8 +33,7 @@ export default function (context, options = {}) { text-decoration: none; } text-align: center; - padding: 0.5rem; - padding-bottom: 0rem; + padding: 0.5rem 1rem 0 1rem; color: inherit; cursor: pointer; font-weight: var(--font-weight-semibold); diff --git a/examples/gccd/scripts/awsRegions.js b/examples/gccd/scripts/awsRegions.js index ba661538..0e3ac0e9 100755 --- a/examples/gccd/scripts/awsRegions.js +++ b/examples/gccd/scripts/awsRegions.js @@ -8,7 +8,7 @@ import { writeFile, runCommand } from "./regionsUtils.js"; const { pipe, tap, get, tryCatch } = rubico; const { pluck, callProp } = rubicox; -const filename = "../src/components/infra/awsRegion.json"; +const filename = "../src/components/cloudAuthentication/awsRegion.json"; // Retrieves the aws regions with 'aws ec2 describe-regions --region us-east-1 --profile default --output json', // Transform the result: diff --git a/examples/gccd/scripts/azureRegions.js b/examples/gccd/scripts/azureRegions.js index 88c0c5fe..acdf4a1c 100755 --- a/examples/gccd/scripts/azureRegions.js +++ b/examples/gccd/scripts/azureRegions.js @@ -8,7 +8,7 @@ import { writeFile, runCommand } from "./regionsUtils.js"; const { pipe, tap, get, tryCatch, filter, map, pick } = rubico; const { groupBy, values, callProp } = rubicox; -const filename = "../src/components/infra/azureRegion.json"; +const filename = "../src/components/cloudAuthentication/azureRegion.json"; // Retrieves the azure regions with 'az account list-locations', // Transform the result: diff --git a/examples/gccd/scripts/googleRegions.js b/examples/gccd/scripts/googleRegions.js index 456e316b..6a5a1638 100755 --- a/examples/gccd/scripts/googleRegions.js +++ b/examples/gccd/scripts/googleRegions.js @@ -5,10 +5,10 @@ import rubicox from "rubico/x/index.js"; import { writeFile, runCommand } from "./regionsUtils.js"; -const { pipe, tap, get, tryCatch, filter, eq, map, pick } = rubico; -const { pluck, callProp } = rubicox; +const { pipe, tap, get, tryCatch, filter, eq, map, all } = rubico; -const filename = "../src/components/infra/googleRegion.json"; +const { callProp, last } = rubicox; +const filename = "../src/components/cloudAuthentication/googleRegion.json"; // Retrieves the google cloud regions with 'gcloud compute regions list --format=json', const toSelect = pipe([ @@ -16,8 +16,13 @@ const toSelect = pipe([ assert(regions); }), filter(eq(get("status"), "UP")), - pluck("name"), - callProp("sort", (a, b) => a.localeCompare(b)), + map( + all({ + name: get("name"), + zones: pipe([get("zones"), map(pipe([callProp("split", "/"), last]))]), + }) + ), + callProp("sort", (a, b) => a.name.localeCompare(b.name)), tap((regions) => { assert(true); }), diff --git a/examples/gccd/src/components/cloudAuthentication/configGoogleFormContent.ts b/examples/gccd/src/components/cloudAuthentication/configGoogleFormContent.ts index 399876ae..6229e066 100644 --- a/examples/gccd/src/components/cloudAuthentication/configGoogleFormContent.ts +++ b/examples/gccd/src/components/cloudAuthentication/configGoogleFormContent.ts @@ -1,11 +1,11 @@ import rubico from "rubico"; -const { get, pipe, map, tap } = rubico; -import rubicox from "rubico/x"; -const { isEmpty, callProp, last } = rubicox; +const { get, pipe, tap } = rubico; import { Context } from "@grucloud/bau-ui/context"; import fileInput from "@grucloud/bau-ui/fileInput"; import alert from "@grucloud/bau-ui/alert"; import spinner from "@grucloud/bau-ui/spinner"; +import radioButton from "@grucloud/bau-ui/radioButton"; +import input from "@grucloud/bau-ui/input"; import selectGoogleRegion from "./selectGoogleRegion"; import selectGoogleZone from "./selectGoogleZone"; @@ -16,50 +16,53 @@ type ConfigGoogleFormContentProp = { GOOGLE_CREDENTIALS?: Record; GOOGLE_REGION?: string; GOOGLE_ZONE?: string; + GOOGLE_PROJECT_ID?: string; onConfig: (config: object) => void; }; export const googleFormElementToData = (event: any) => { - const { GOOGLE_REGION, GOOGLE_ZONE } = event.target.elements; + const { GOOGLE_REGION, GOOGLE_ZONE, GOOGLE_PROJECT_ID } = + event.target.elements; return { GOOGLE_REGION: GOOGLE_REGION.value, GOOGLE_ZONE: GOOGLE_ZONE.value, + GOOGLE_PROJECT_ID: GOOGLE_PROJECT_ID.value, }; }; export default (context: Context) => { const { bau, config, css } = context; - const { section, div, ol, li, span, em, a, table, tbody, th, tr, td, label } = - bau.tags; + const { + section, + div, + ol, + li, + span, + em, + a, + table, + tbody, + th, + tr, + td, + label, + legend, + header, + fieldset, + strong, + h3, + } = bau.tags; const { svg, use } = bau.tagsNS("http://www.w3.org/2000/svg"); const query = useQuery(context); + const RadioButton = radioButton(context); + const getProjectQuery = query( - async ({ project_id, token }: any) => { - try { - const response = await fetch( - "https://corsproxy.io/?" + - encodeURIComponent( - `https://compute.googleapis.com/compute/v1/projects/${project_id}/regions` - ), - { - headers: { - Accept: "application/json", - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, - } - ); - if (response.ok) { - const { items } = await response.json(); - return items.filter(({ status }: any) => status === "UP"); - } - throw response; - } catch (error) { - throw error; - } + async () => { + const { default: regions } = await import("./googleRegion.json"); + return regions; }, { initialState: [] } ); @@ -71,31 +74,31 @@ export default (context: Context) => { const Alert = alert(context, { color: "danger" }); const className = css` align-items: flex-start; - & ol { & > li { padding: 0.3rem 0; } } `; + const classNameTable = css` + border-collapse: collapse; + & td, + th { + border-top: 1px solid var(--color-emphasis-100); + border-bottom: 1px solid var(--color-emphasis-100); + padding: 0.5rem; + text-align: left; + } + & th { + font-size: smaller; + color: var(--font-color-secondary); + } + `; - const CredentialFile = ({ fileName, content }: any) => { - return table( + const ServicePrincipalFile = ({ fileName, content }: any) => + table( { - class: css` - border-collapse: collapse; - & td, - th { - border-top: 1px solid var(--color-emphasis-100); - border-bottom: 1px solid var(--color-emphasis-100); - padding: 0.5rem; - text-align: left; - } - & th { - font-size: smaller; - color: var(--font-color-secondary); - } - `, + class: classNameTable, }, tbody( tr(th("Credential File"), td(fileName)), @@ -103,7 +106,17 @@ export default (context: Context) => { tr(th("Service Account"), td(content.client_email)) ) ); - }; + + const WorkloadIdentityFile = ({ fileName, content }: any) => + table( + { + class: classNameTable, + }, + tbody( + tr(th("Credential File"), td(fileName)), + tr(th("Audience"), td(content.audience)) + ) + ); const FileInputLabel = ({}: any) => div( @@ -129,51 +142,52 @@ export default (context: Context) => { GOOGLE_CREDENTIALS = {}, GOOGLE_REGION, GOOGLE_ZONE, + GOOGLE_PROJECT_ID = "", }: ConfigGoogleFormContentProp) { + getProjectQuery.run(); const fileState = bau.state("No file selected"); const contentState = bau.state(GOOGLE_CREDENTIALS); + const project_id = bau.state(GOOGLE_PROJECT_ID); + bau.derive(() => { + if (contentState.val?.project_id) { + project_id.val = contentState.val?.project_id; + } + }); - const project_id = bau.derive(() => contentState.val?.project_id); const regionState = bau.state(GOOGLE_REGION); const tokenState = bau.state(""); + const radioState = bau.state( + GOOGLE_CREDENTIALS.type == "service_account" ? "sp" : "federated" + ); + + const oninput = (event: any) => { + radioState.val = event.target.id; + }; const zonesState = bau.derive( pipe([ - () => { - console.log("zonesState region", regionState.val); - }, () => + regionState.val && getProjectQuery.data.val.find( - ({ name }: any) => name == regionState.val + ({ name }: any) => regionState.val === name ), tap((zones: any) => { console.log("zones", zones); }), get("zones", []), - map(pipe([callProp("split", "/"), last])), - tap((zones: any) => { - console.log("zones", zones); - }), ]) ); bau.derive(async () => { if (contentState.val?.private_key) { - const token = await googleAuthorize({ - credentials: contentState.val, - }); - //console.log("token", token); - tokenState.val = token.access_token; - - if ( - token.access_token && - project_id && - isEmpty(getProjectQuery.data.val) - ) { - getProjectQuery.run({ - project_id: project_id.val, - token: token.access_token, + try { + const token = await googleAuthorize({ + credentials: contentState.val, }); + //console.log("token", token); + tokenState.val = token.access_token; + } catch (error) { + errorMessage.val = "Error authenticating"; } } }); @@ -193,14 +207,13 @@ export default (context: Context) => { // @ts-ignore const contentJson = JSON.parse(reader.result); contentState.val = contentJson; - if (contentJson.project_id) { + if (contentJson.type) { onConfig(contentJson); } else { errorMessage.val = "File is not a GCP crendential file."; } } } catch (error) { - debugger; errorMessage.val = "Error parsing file."; } }; @@ -212,54 +225,194 @@ export default (context: Context) => { } }; - return section( - { class: className }, - ol( - li( - "Visit the ", - a( - { - href: "https://console.cloud.google.com/iam-admin/serviceaccounts", - target: "_blank", - }, - "service account page" + const Input = input(context); + + const WorkloadIdentity = ({}: any) => + section( + h3("Create an identity pool"), + ol( + li( + "Visit the ", + a( + { + href: "https://console.cloud.google.com/iam-admin/workload-identity-pools", + target: "_blank", + }, + "Workload Identity Pools" + ), + " page on the google cloud console" ), - " on the google cloud console" + li("Click on ", em("CREATE POOL")), + li("Enter a name, for instance ", em("grucloud-my-project-dev")), + li("Enter a Pool ID, for instance ", em("grucloud-my-project-dev")), + + li("Click on ", em("CONTINUE")) ), - li("Select your project"), - li("Click on ", em("CREATE SERVICE ACCOUNT"), ""), - li( - "Set the ", - em("Service account name"), - " to 'grucloud' for instance" + h3("Add a provider to pool"), + ol( + li("Select the ", em("OpenID Connect (OIDC)"), " provider"), + li( + "Enter the ", + em("Provider Name and ID"), + " such as ", + em("grucloud-my-project-dev") + ), + li("Enter the issuer ", strong("https://app.grucloud.com")) ), - li("Click on ", em("CREATE"), ""), - li("Select the basic role 'Viewer'"), - li("Click on ", em("CONTINUE"), ""), - li("Click on ", em("DONE"), ""), - li( - "Go to the ", - em("Actions"), - " column, click on the three dot icon of the newly created service account" + h3("Configure provider attributes"), + ol( + li( + "Set the ", + strong("google.subject"), + " to ", + strong(`assertion.sub`) + ), + li("Click on ", em("Save")) ), - li("Click on ", em("Manage keys"), ""), - li("Click on ", em("ADD KEYS"), ", then ", em("Create new key"), ""), - li( - "Click on ", - em("CREATE"), - " to download the credential file in JSON format." - ) + h3("Grant Access"), + ol( + li("Click on the button ", strong("GRANT ACCESS")), + li("Select an existing service account or create a new one"), + li("Click on ", em("Save")), + li( + "In the ", + em("Configure your application "), + "dialog, select the provider previously created." + ), + li( + "In the ", + em("OIDC ID token path"), + " field, enter ", + strong("oauth/token") + ), + li("Click on ", em("DOWNLOAD CONFIG")) + ), + FileInput({ + "data-input-google-upload": true, + Component: FileInputLabel, + name: "file", + accept: "application/JSON", + onchange, + }), + () => errorMessage.val && Alert(errorMessage.val), + () => + WorkloadIdentityFile({ + fileName: fileState.val, + content: contentState.val, + }) + ); + + const ServicePrincipal = () => + section( + ol( + li( + "Visit the ", + a( + { + href: "https://console.cloud.google.com/iam-admin/serviceaccounts", + target: "_blank", + }, + "service account page" + ), + " on the google cloud console" + ), + li("Select your project"), + li("Click on ", em("CREATE SERVICE ACCOUNT"), ""), + li( + "Set the ", + em("Service account name"), + " to 'grucloud' for instance" + ), + li("Click on ", em("CREATE"), ""), + li("Select the basic role 'Viewer'"), + li("Click on ", em("CONTINUE"), ""), + li("Click on ", em("DONE"), ""), + li( + "Go to the ", + em("Actions"), + " column, click on the three dot icon of the newly created service account" + ), + li("Click on ", em("Manage keys"), ""), + li("Click on ", em("ADD KEYS"), ", then ", em("Create new key"), ""), + li( + "Click on ", + em("CREATE"), + " to download the credential file in JSON format." + ) + ), + FileInput({ + "data-input-google-upload": true, + Component: FileInputLabel, + name: "file", + accept: "application/JSON", + onchange, + }), + () => errorMessage.val && Alert(errorMessage.val), + () => + ServicePrincipalFile({ + fileName: fileState.val, + content: contentState.val, + }) + ); + + return section( + { class: className }, + fieldset( + { + class: css` + display: flex; + flex-direction: column; + gap: 1rem; + border: 1px solid var(--color-emphasis-500); + & header { + display: inline-flex; + justify-content: flex-start; + & label { + flex-direction: row; + } + } + `, + }, + legend("Authentication Type"), + header( + label( + "Workload identity", + RadioButton({ + id: "federated", + name: "kind", + checked: radioState.val == "federated", + value: radioState, + oninput, + }) + ), + label( + "Service Principal", + RadioButton({ + id: "password", + name: "kind", + checked: radioState.val == "sp", + value: radioState, + oninput, + }) + ) + ), + () => + radioState.val == "federated" + ? WorkloadIdentity({ GOOGLE_PROJECT_ID }) + : ServicePrincipal() + ), + label( + "Project Id", + Input({ + placeholder: "Project Id", + name: "GOOGLE_PROJECT_ID", + value: project_id, + minLength: 8, + maxLength: 64, + size: 32, + required: true, + }) ), - FileInput({ - "data-input-google-upload": true, - Component: FileInputLabel, - name: "file", - accept: "application/JSON", - onchange, - }), - () => errorMessage.val && Alert(errorMessage.val), - () => - CredentialFile({ fileName: fileState.val, content: contentState.val }), label("Select the region:", () => div( { @@ -285,7 +438,6 @@ export default (context: Context) => { label("Select the zone:", () => SelectGoogleZone({ value: GOOGLE_ZONE, - project_id: project_id.val, zones: zonesState.val, }) ) diff --git a/examples/gccd/src/components/cloudAuthentication/googleRegion.json b/examples/gccd/src/components/cloudAuthentication/googleRegion.json index 341973f8..13f0e23c 100644 --- a/examples/gccd/src/components/cloudAuthentication/googleRegion.json +++ b/examples/gccd/src/components/cloudAuthentication/googleRegion.json @@ -1,40 +1,315 @@ [ - "asia-east1", - "asia-east2", - "asia-northeast1", - "asia-northeast2", - "asia-northeast3", - "asia-south1", - "asia-south2", - "asia-southeast1", - "asia-southeast2", - "australia-southeast1", - "australia-southeast2", - "europe-central2", - "europe-north1", - "europe-southwest1", - "europe-west1", - "europe-west10", - "europe-west12", - "europe-west2", - "europe-west3", - "europe-west4", - "europe-west6", - "europe-west8", - "europe-west9", - "me-central1", - "me-west1", - "northamerica-northeast1", - "northamerica-northeast2", - "southamerica-east1", - "southamerica-west1", - "us-central1", - "us-east1", - "us-east4", - "us-east5", - "us-south1", - "us-west1", - "us-west2", - "us-west3", - "us-west4" + { + "name": "asia-east1", + "zones": [ + "asia-east1-a", + "asia-east1-b", + "asia-east1-c" + ] + }, + { + "name": "asia-east2", + "zones": [ + "asia-east2-c", + "asia-east2-b", + "asia-east2-a" + ] + }, + { + "name": "asia-northeast1", + "zones": [ + "asia-northeast1-a", + "asia-northeast1-b", + "asia-northeast1-c" + ] + }, + { + "name": "asia-northeast2", + "zones": [ + "asia-northeast2-b", + "asia-northeast2-c", + "asia-northeast2-a" + ] + }, + { + "name": "asia-northeast3", + "zones": [ + "asia-northeast3-a", + "asia-northeast3-c", + "asia-northeast3-b" + ] + }, + { + "name": "asia-south1", + "zones": [ + "asia-south1-b", + "asia-south1-a", + "asia-south1-c" + ] + }, + { + "name": "asia-south2", + "zones": [ + "asia-south2-a", + "asia-south2-c", + "asia-south2-b" + ] + }, + { + "name": "asia-southeast1", + "zones": [ + "asia-southeast1-a", + "asia-southeast1-b", + "asia-southeast1-c" + ] + }, + { + "name": "asia-southeast2", + "zones": [ + "asia-southeast2-a", + "asia-southeast2-c", + "asia-southeast2-b" + ] + }, + { + "name": "australia-southeast1", + "zones": [ + "australia-southeast1-c", + "australia-southeast1-a", + "australia-southeast1-b" + ] + }, + { + "name": "australia-southeast2", + "zones": [ + "australia-southeast2-a", + "australia-southeast2-c", + "australia-southeast2-b" + ] + }, + { + "name": "europe-central2", + "zones": [ + "europe-central2-b", + "europe-central2-c", + "europe-central2-a" + ] + }, + { + "name": "europe-north1", + "zones": [ + "europe-north1-b", + "europe-north1-c", + "europe-north1-a" + ] + }, + { + "name": "europe-southwest1", + "zones": [ + "europe-southwest1-b", + "europe-southwest1-a", + "europe-southwest1-c" + ] + }, + { + "name": "europe-west1", + "zones": [ + "europe-west1-b", + "europe-west1-c", + "europe-west1-d" + ] + }, + { + "name": "europe-west10", + "zones": [ + "europe-west10-c", + "europe-west10-a", + "europe-west10-b" + ] + }, + { + "name": "europe-west12", + "zones": [ + "europe-west12-c", + "europe-west12-a", + "europe-west12-b" + ] + }, + { + "name": "europe-west2", + "zones": [ + "europe-west2-a", + "europe-west2-b", + "europe-west2-c" + ] + }, + { + "name": "europe-west3", + "zones": [ + "europe-west3-c", + "europe-west3-a", + "europe-west3-b" + ] + }, + { + "name": "europe-west4", + "zones": [ + "europe-west4-c", + "europe-west4-b", + "europe-west4-a" + ] + }, + { + "name": "europe-west6", + "zones": [ + "europe-west6-b", + "europe-west6-c", + "europe-west6-a" + ] + }, + { + "name": "europe-west8", + "zones": [ + "europe-west8-a", + "europe-west8-b", + "europe-west8-c" + ] + }, + { + "name": "europe-west9", + "zones": [ + "europe-west9-b", + "europe-west9-a", + "europe-west9-c" + ] + }, + { + "name": "me-central1", + "zones": [ + "me-central1-a", + "me-central1-b", + "me-central1-c" + ] + }, + { + "name": "me-central2", + "zones": [ + "me-central2-c", + "me-central2-a", + "me-central2-b" + ] + }, + { + "name": "me-west1", + "zones": [ + "me-west1-b", + "me-west1-a", + "me-west1-c" + ] + }, + { + "name": "northamerica-northeast1", + "zones": [ + "northamerica-northeast1-a", + "northamerica-northeast1-b", + "northamerica-northeast1-c" + ] + }, + { + "name": "northamerica-northeast2", + "zones": [ + "northamerica-northeast2-b", + "northamerica-northeast2-a", + "northamerica-northeast2-c" + ] + }, + { + "name": "southamerica-east1", + "zones": [ + "southamerica-east1-a", + "southamerica-east1-b", + "southamerica-east1-c" + ] + }, + { + "name": "southamerica-west1", + "zones": [ + "southamerica-west1-a", + "southamerica-west1-b", + "southamerica-west1-c" + ] + }, + { + "name": "us-central1", + "zones": [ + "us-central1-a", + "us-central1-b", + "us-central1-c", + "us-central1-f" + ] + }, + { + "name": "us-east1", + "zones": [ + "us-east1-b", + "us-east1-c", + "us-east1-d" + ] + }, + { + "name": "us-east4", + "zones": [ + "us-east4-a", + "us-east4-b", + "us-east4-c" + ] + }, + { + "name": "us-east5", + "zones": [ + "us-east5-c", + "us-east5-b", + "us-east5-a" + ] + }, + { + "name": "us-south1", + "zones": [ + "us-south1-c", + "us-south1-a", + "us-south1-b" + ] + }, + { + "name": "us-west1", + "zones": [ + "us-west1-a", + "us-west1-b", + "us-west1-c" + ] + }, + { + "name": "us-west2", + "zones": [ + "us-west2-c", + "us-west2-b", + "us-west2-a" + ] + }, + { + "name": "us-west3", + "zones": [ + "us-west3-a", + "us-west3-b", + "us-west3-c" + ] + }, + { + "name": "us-west4", + "zones": [ + "us-west4-c", + "us-west4-a", + "us-west4-b" + ] + } ] \ No newline at end of file diff --git a/examples/gccd/src/components/cloudAuthentication/selectAwsRegion.ts b/examples/gccd/src/components/cloudAuthentication/selectAwsRegion.ts index 1b8732a2..b21b3d9b 100644 --- a/examples/gccd/src/components/cloudAuthentication/selectAwsRegion.ts +++ b/examples/gccd/src/components/cloudAuthentication/selectAwsRegion.ts @@ -22,6 +22,8 @@ export default (context: Context) => { options: AwsRegions, label: "Select region", getOptionLabel: (label) => label, + getOptionValue: (label) => label, + ...props, }); }; diff --git a/examples/gccd/src/components/cloudAuthentication/selectGoogleZone.ts b/examples/gccd/src/components/cloudAuthentication/selectGoogleZone.ts index 0b574627..424ffadd 100644 --- a/examples/gccd/src/components/cloudAuthentication/selectGoogleZone.ts +++ b/examples/gccd/src/components/cloudAuthentication/selectGoogleZone.ts @@ -7,14 +7,12 @@ export default (context: Context) => { const SelectNative = selectNative(context); return function SelectGoogleZone(props: any) { - const { project_id, zones = [] } = props; - //console.log("SelectGoogleZone zones", zones, project_id); + const { zones = [] } = props; return SelectNative( { required: "required", title: "Select a zone", name: "GOOGLE_ZONE", - disabled: !project_id, ...props, }, option({ value: "" }, "--Please choose a zone--"), diff --git a/examples/gccd/src/components/gitRepository/gitRepositoryForm.ts b/examples/gccd/src/components/gitRepository/gitRepositoryForm.ts new file mode 100644 index 00000000..35132d95 --- /dev/null +++ b/examples/gccd/src/components/gitRepository/gitRepositoryForm.ts @@ -0,0 +1,55 @@ +import { Context } from "@grucloud/bau-ui/context"; +import form from "@grucloud/bau-ui/form"; +import loadingButton from "@grucloud/bau-ui/loadingButton"; + +import gitRepositoryFormContent from "./gitRepositoryFormContent"; + +export default function (context: Context) { + const { bau, stores } = context; + const { h1, header, footer } = bau.tags; + const LoadingButton = loadingButton(context, { + color: "primary", + variant: "solid", + }); + const Form = form(context); + + const GitRepositoryFormContent = gitRepositoryFormContent(context); + + return function GitRepositoryForm(props: any) { + const { org_id, project_id, workspace_id } = props; + stores.gitRepository.getByIdQuery.run({ org_id, project_id, workspace_id }); + + const onsubmit = async (event: any) => { + event.preventDefault(); + const { branch, repository, git_credentials } = event.target.elements; + await stores.gitRepository.createQuery.run( + { org_id, project_id, workspace_id }, + { + branch: branch.value, + repository_url: repository.value, + git_credential_id: git_credentials.value, + } + ); + }; + + return () => + stores.gitRepository.getByIdQuery.loading.val + ? "Loading" + : Form( + { onsubmit }, + header(h1("Link a new Git Repository")), + GitRepositoryFormContent( + stores.gitRepository.getByIdQuery.data.val + ), + footer( + LoadingButton( + { + type: "submit", + loading: stores.gitRepository.createQuery.loading, + }, + "Save" + ) + ) + ); + }; +} diff --git a/examples/gccd/src/components/gitRepository/gitRepositoryFormContent.ts b/examples/gccd/src/components/gitRepository/gitRepositoryFormContent.ts new file mode 100644 index 00000000..492b93a9 --- /dev/null +++ b/examples/gccd/src/components/gitRepository/gitRepositoryFormContent.ts @@ -0,0 +1,163 @@ +import { Context } from "@grucloud/bau-ui/context"; +import input from "@grucloud/bau-ui/input"; +import select from "@grucloud/bau-ui/select"; +import autocomplete from "@grucloud/bau-ui/autocomplete"; +//import tableSkeleton from "../tableSkeleton"; + +export default (context: Context) => { + const { bau, stores, css } = context; + const { section, header, p, label, div, ol, li } = bau.tags; + const { listRepoQuery, listBranchesQuery } = stores.gitHub; + // const TableSkeleton = tableSkeleton(context, { + // class: css` + // height: 2rem; + // margin: 0.5rem; + // min-width: 20rem; + // `, + // }); + + const Autocomplete = autocomplete(context, { variant: "outline" }); + const Select = select(context, { variant: "outline" }); + + const Input = input(context); + + const checkedGitProviderState = bau.state("github"); + + return function GitRepositoryFormContent(props: any) { + const { git_credential_id } = props; + const gitProviderState = bau.state({ git_credential_id, username: "" }); + const GitProvider = () => + label( + "Git Provider", + Select({ + options: stores.gitCredential.getAllByOrgQuery.data.val, + Option: (opt) => div(opt.provider_type), + getOptionLabel: (props: any) => props.provider_type, + getOptionValue: (props: any) => props.git_credential_id, + label: "Git Provider", + placeholder: "Select Git Provider", + name: "git_credentials", + required: true, + defaultOption: stores.gitCredential.getAllByOrgQuery.data.val.find( + ({ git_credential_id }: any) => + git_credential_id == props.git_credential_id + ), + loading: () => stores.gitCredential.getAllByOrgQuery.loading.val, + onSelect: (item: any) => { + gitProviderState.val = item; + }, + }), + bau.bind({ + deps: [gitProviderState], + render: () => () => { + if (gitProviderState.val.username) { + listRepoQuery.run(gitProviderState.val); + } + return undefined; + }, + }) + ); + const GitRepositoryUrlGitHub = ({}: any) => + label("Repository URL", () => + Autocomplete({ + options: listRepoQuery.data.val ?? [], + Option: (opt) => div(opt.name), + getOptionLabel: ({ clone_url }: any) => clone_url, + getOptionValue: ({ clone_url }: any) => clone_url, + label: "Repositories", + placeholder: "Search repository", + name: "repository", + required: true, + defaultOption: listRepoQuery.data.val.find( + ({ clone_url }: any) => clone_url == props.repository_url + ), + onSelect: (item: any) => { + const { username } = gitProviderState.val; + if (username) { + listBranchesQuery.run({ + username, + repo: item.name, + }); + } + }, + loading: listRepoQuery.loading.val, + }) + ); + + const GitRepositoryUrlStandard = () => + label( + "Repository URL", + Input({ + autofocus: true, + placeholder: + "Git repository URL: https://git-codecommit.us-east-2.amazonaws.com/v1/repos/MyDemoRepo", + name: "repository_url", + minLength: 3, + size: 80, + required: true, + defaultValue: props.repository_url, + }) + ); + + const GitRepositoryUrl = + ({ username }: any = {}) => + () => + !props.repository_url + ? GitRepositoryUrlGitHub({ username }) + : GitRepositoryUrlStandard(); + + const GitBranchGitHub = ({ branch }: any) => + label("Branch", () => + Autocomplete({ + options: listBranchesQuery.data.val ?? [], + Option: (opt) => div(opt.name), + getOptionValue: ({ name }: any) => name, + getOptionLabel: ({ name }: any) => name, + label: "Branches", + placeholder: "Search branches", + name: "branch", + required: true, + value: branch, + loading: listBranchesQuery.loading.val, + }) + ); + + const GitBranchStandard = ({ branch }: any) => + label( + "Branch", + Input({ + placeholder: "Git Branch", + name: "branch", + minLength: 3, + required: true, + value: branch, + }) + ); + + //TODO + const GitBranch = + ({ branch }: any) => + () => + checkedGitProviderState.val == "github" + ? GitBranchGitHub({ branch }) + : GitBranchStandard({ branch }); + + const className = css` + & ol { + padding-left: 0; + } + `; + + return div( + { class: className }, + header( + p( + "Provide information about the git repository. The resources inventory and generated code are stored on your source code git repository." + ) + ), + section( + ol(li(GitProvider()), li(GitRepositoryUrl()), li(GitBranch(props))) + ) + ); + }; +}; diff --git a/examples/gccd/src/components/infra/gitRepositoryConfig.ts b/examples/gccd/src/components/infra/gitRepositoryConfig.ts index 31987862..a652b5b3 100644 --- a/examples/gccd/src/components/infra/gitRepositoryConfig.ts +++ b/examples/gccd/src/components/infra/gitRepositoryConfig.ts @@ -1,184 +1,186 @@ -import { Context } from "@grucloud/bau-ui/context"; -import form from "@grucloud/bau-ui/form"; -import input from "@grucloud/bau-ui/input"; -import autocomplete from "@grucloud/bau-ui/autocomplete"; -import radioButton from "@grucloud/bau-ui/radioButton"; +// import { Context } from "@grucloud/bau-ui/context"; +// import form from "@grucloud/bau-ui/form"; +// import input from "@grucloud/bau-ui/input"; +// import autocomplete from "@grucloud/bau-ui/autocomplete"; +// import radioButton from "@grucloud/bau-ui/radioButton"; -import buttonsFooter from "./buttonsFooter"; -import buttonPrevious from "./buttonPrevious"; -import buttonNext from "./buttonNext"; +// import buttonsFooter from "./buttonsFooter"; +// import buttonPrevious from "./buttonPrevious"; +// import buttonNext from "./buttonNext"; +// // TODO to delete +// export default (context: Context) => { +// const { bau, stores, css } = context; +// const { section, h1, header, p, label, div, fieldset, legend, ol, li } = +// bau.tags; +// const { listRepoQuery, listBranchesQuery } = stores.gitHub; -export default (context: Context) => { - const { bau, stores, css } = context; - const { section, h1, header, p, label, div, fieldset, legend, ol, li } = - bau.tags; - const { listRepoQuery, listBranchesQuery } = stores.gitHub; +// const Form = form(context); +// const Autocomplete = autocomplete(context, { variant: "outline" }); +// const Input = input(context); +// const RadioButton = radioButton(context); - const Form = form(context); - const Autocomplete = autocomplete(context, { variant: "outline" }); - const Input = input(context); - const RadioButton = radioButton(context); +// const ButtonPrevious = buttonPrevious(context); +// const ButtonNext = buttonNext(context); +// const ButtonsFooter = buttonsFooter(context); - const ButtonPrevious = buttonPrevious(context); - const ButtonNext = buttonNext(context); - const ButtonsFooter = buttonsFooter(context); +// const checkedGitProviderState = bau.state("github"); +// const oninput = (event: any) => { +// checkedGitProviderState.val = event.target.id; +// }; +// const className = css` +// & ol { +// padding-left: 0; +// > li { +// margin-bottom: 1rem; +// } +// } +// & label { +// display: flex; +// } +// `; +// const GitProvider = () => +// fieldset( +// { +// class: css` +// display: inline-flex; +// flex-direction: row; +// gap: 1rem; +// border: 1px solid var(--color-emphasis-500); +// & label { +// flex-direction: row; +// } +// `, +// }, +// legend("Git Provider"), +// label( +// "GitHub", +// RadioButton({ +// id: "github", +// name: "gitProvider", +// checked: true, +// value: checkedGitProviderState, +// oninput, +// }) +// ), +// label( +// "Other Git Provider", +// RadioButton({ +// id: "other", +// name: "gitProvider", +// value: checkedGitProviderState, +// oninput, +// }) +// ) +// ); - const checkedGitProviderState = bau.state("github"); - const oninput = (event: any) => { - checkedGitProviderState.val = event.target.id; - }; - const className = css` - & ol { - padding-left: 0; - > li { - margin-bottom: 1rem; - } - } - & label { - display: flex; - } - `; - const GitProvider = () => - fieldset( - { - class: css` - display: inline-flex; - flex-direction: row; - gap: 1rem; - border: 1px solid var(--color-emphasis-500); - & label { - flex-direction: row; - } - `, - }, - legend("Git Provider"), - label( - "GitHub", - RadioButton({ - id: "github", - name: "gitProvider", - checked: true, - value: checkedGitProviderState, - oninput, - }) - ), - label( - "Other Git Provider", - RadioButton({ - id: "other", - name: "gitProvider", - value: checkedGitProviderState, - oninput, - }) - ) - ); +// const GitRepositoryUrlGitHub = ({ username }: any) => +// label("Repository URL", () => +// Autocomplete({ +// options: listRepoQuery.data.val ?? [], +// Option: (opt) => div(opt.name), +// getOptionValue: ({ clone_url }: any) => clone_url, +// getOptionLabel: ({ clone_url }: any) => clone_url, +// label: "Repositories", +// placeholder: "Search repository", +// name: "repository", +// required: true, +// onSelect: (item: any) => { +// listBranchesQuery.run({ +// username, +// repo: item.name, +// }); +// }, +// loading: listRepoQuery.loading.val, +// }) +// ); - const GitRepositoryUrlGitHub = ({ username }: any) => - label("Repository URL", () => - Autocomplete({ - options: listRepoQuery.data.val ?? [], - Option: (opt) => div(opt.name), - getOptionLabel: ({ clone_url }: any) => clone_url, - label: "Repositories", - placeholder: "Search repository", - name: "repository", - required: true, - onSelect: (item: any) => { - listBranchesQuery.run({ - username, - repo: item.name, - }); - }, - loading: listRepoQuery.loading.val, - }) - ); +// const GitRepositoryUrlStandard = () => +// label( +// "Repository URL", +// Input({ +// autofocus: true, +// placeholder: +// "Git repository URL: https://git-codecommit.us-east-2.amazonaws.com/v1/repos/MyDemoRepo", +// name: "repository", +// minLength: 3, +// required: true, +// }) +// ); - const GitRepositoryUrlStandard = () => - label( - "Repository URL", - Input({ - autofocus: true, - placeholder: - "Git repository URL: https://git-codecommit.us-east-2.amazonaws.com/v1/repos/MyDemoRepo", - name: "repository", - minLength: 3, - required: true, - }) - ); +// const GitRepositoryUrl = +// ({ username }: any) => +// () => +// checkedGitProviderState.val == "github" +// ? GitRepositoryUrlGitHub({ username }) +// : GitRepositoryUrlStandard(); - const GitRepositoryUrl = - ({ username }: any) => - () => - checkedGitProviderState.val == "github" - ? GitRepositoryUrlGitHub({ username }) - : GitRepositoryUrlStandard(); +// const GitBranchGitHub = () => +// label("Branch", () => +// Autocomplete({ +// options: listBranchesQuery.data.val ?? [], +// Option: (opt) => div(opt.name), +// getOptionLabel: ({ name }: any) => name, +// label: "Branches", +// placeholder: "Search branches", +// name: "branch", +// required: true, +// loading: listBranchesQuery.loading.val, +// }) +// ); - const GitBranchGitHub = () => - label("Branch", () => - Autocomplete({ - options: listBranchesQuery.data.val ?? [], - Option: (opt) => div(opt.name), - getOptionLabel: ({ name }: any) => name, - label: "Branches", - placeholder: "Search branches", - name: "branch", - required: true, - loading: listBranchesQuery.loading.val, - }) - ); +// const GitBranchStandard = () => +// label( +// "Branch", +// Input({ +// placeholder: "Git Branch", +// name: "branch", +// minLength: 3, +// required: true, +// }) +// ); - const GitBranchStandard = () => - label( - "Branch", - Input({ - placeholder: "Git Branch", - name: "branch", - minLength: 3, - required: true, - }) - ); +// //TODO +// const GitBranch = () => () => +// checkedGitProviderState.val == "github" +// ? GitBranchGitHub() +// : GitBranchStandard(); - const GitBranch = () => () => - checkedGitProviderState.val == "github" - ? GitBranchGitHub() - : GitBranchStandard(); +// return function GitRepositoryConfig({ +// onclickPrevious, +// onclickGitRepository, +// gitCredential, +// }: any) { +// listRepoQuery.run({ username: gitCredential.username }); - return function GitRepositoryConfig({ - onclickPrevious, - onclickGitRepository, - gitCredential, - }: any) { - listRepoQuery.run({ username: gitCredential.username }); +// const onsubmit = (event: any) => { +// const { repository, branch } = event.target.elements; +// event.preventDefault(); +// onclickGitRepository({ +// url: repository.value, +// branch: branch.value, +// }); +// }; - const onsubmit = (event: any) => { - const { repository, branch } = event.target.elements; - event.preventDefault(); - onclickGitRepository({ - url: repository.value, - branch: branch.value, - }); - }; - - return Form( - { - onsubmit, - name: "form-git-repository-config", - class: className, - }, - header( - h1("Git Repository"), - p( - "Provide information about the git repository. The resources inventory and generated code are stored on your source code git repository." - ) - ), - section( - ol( - li(GitProvider()), - li(GitRepositoryUrl(gitCredential)), - li(GitBranch()) - ) - ), - ButtonsFooter(ButtonPrevious({ onclick: onclickPrevious }), ButtonNext()) - ); - }; -}; +// return Form( +// { +// onsubmit, +// name: "form-git-repository-config", +// class: className, +// }, +// header( +// h1("Git Repository"), +// p( +// "Provide information about the git repository. The resources inventory and generated code are stored on your source code git repository." +// ) +// ), +// section( +// ol( +// li(GitProvider()), +// li(GitRepositoryUrl(gitCredential)), +// li(GitBranch()) +// ) +// ), +// ButtonsFooter(ButtonPrevious({ onclick: onclickPrevious }), ButtonNext()) +// ); +// }; +// }; diff --git a/examples/gccd/src/components/infra/stepperFinal.ts b/examples/gccd/src/components/infra/stepperFinal.ts index 88640d75..adb1f13f 100644 --- a/examples/gccd/src/components/infra/stepperFinal.ts +++ b/examples/gccd/src/components/infra/stepperFinal.ts @@ -32,14 +32,13 @@ export default (context: Context) => { const { id: git_credential_id } = await stores.gitCredentials.createQuery.run(gitCredential); - const { id: git_repository_id } = - await stores.gitRepository.createQuery.run(gitRepository); + const {} = await stores.gitRepository.createQuery.run(gitRepository); const { id } = await stores.infra.createQuery.run({ ...cloudconfig, ...settings, git_credential_id, - git_repository_id, + // git_repository_id, }); window.document.dispatchEvent( diff --git a/examples/gccd/src/components/tableSkeleton.ts b/examples/gccd/src/components/tableSkeleton.ts index eeeaac55..510b5177 100644 --- a/examples/gccd/src/components/tableSkeleton.ts +++ b/examples/gccd/src/components/tableSkeleton.ts @@ -1,7 +1,7 @@ import { type Context } from "@grucloud/bau-ui/context"; import skeleton from "@grucloud/bau-ui/skeleton"; -export default function (context: Context) { +export default function (context: Context, options?: any) { const { bau, css } = context; const { tbody, tr, td } = bau.tags; @@ -10,7 +10,9 @@ export default function (context: Context) { const Skeleton = skeleton(context, { class: css` height: 1rem; + min-width: 5rem; `, + ...options, }); return function TableSkeleton({ rowSize = 10, columnsSize = 4 }) { diff --git a/examples/gccd/src/components/workspace/workspaceDetailContent.ts b/examples/gccd/src/components/workspace/workspaceDetailContent.ts index 35923bf6..79711696 100644 --- a/examples/gccd/src/components/workspace/workspaceDetailContent.ts +++ b/examples/gccd/src/components/workspace/workspaceDetailContent.ts @@ -1,9 +1,10 @@ import { type Context } from "@grucloud/bau-ui/context"; import tableContainer from "@grucloud/bau-ui/tableContainer"; +import button from "@grucloud/bau-ui/button"; export default function (context: Context) { const { bau, css, config } = context; - const { h2, table, tr, td, th, section, a } = bau.tags; + const { h2, table, tr, td, th, section, a, div } = bau.tags; const TableContainer = tableContainer(context, { class: css` & th { @@ -11,6 +12,9 @@ export default function (context: Context) { } `, }); + + const ButtonDelete = button(context, { variant: "outline", color: "danger" }); + return function WorkspaceDetailContent({ org_id, project_id, @@ -35,6 +39,15 @@ export default function (context: Context) { ), tr(th("Workspace"), td(workspace_id)) ) + ), + h2("Danger Zone"), + div( + ButtonDelete( + { + href: `${config.base}/org/${org_id}/projects/${project_id}/workspaces/${workspace_id}/destroy`, + }, + "Danger Zone" + ) ) ); }; diff --git a/examples/gccd/src/pages/cloudAuthentication/googleCreatePage.ts b/examples/gccd/src/pages/cloudAuthentication/googleCreatePage.ts index f2748546..c830a950 100644 --- a/examples/gccd/src/pages/cloudAuthentication/googleCreatePage.ts +++ b/examples/gccd/src/pages/cloudAuthentication/googleCreatePage.ts @@ -35,7 +35,7 @@ export default function (context: Context) { { provider_type: "google", env_vars: { - credentials: contentState.val, + GOOGLE_CREDENTIALS: contentState.val, ...googleFormElementToData(event), }, } diff --git a/examples/gccd/src/pages/gitCredential/gitCredentialEditPage.ts b/examples/gccd/src/pages/gitCredential/gitCredentialEditPage.ts index bf6b2641..d9599d70 100644 --- a/examples/gccd/src/pages/gitCredential/gitCredentialEditPage.ts +++ b/examples/gccd/src/pages/gitCredential/gitCredentialEditPage.ts @@ -7,12 +7,12 @@ import gitCredentialFormContent from "../../components/gitCredential/gitCredenti export default function (context: Context) { const { bau, stores, config, window } = context; - const { h1, p, header, footer, h2 } = bau.tags; + const { h1, p, header, footer } = bau.tags; const ButtonBack = buttonBack(context); const ButtonEdit = button(context, { color: "primary", variant: "solid" }); const Page = page(context); const Form = form(context); - const ButtonDelete = button(context, { variant: "outline", color: "danger" }); + // const ButtonDelete = button(context, { variant: "outline", color: "danger" }); const GitCredentialFormContent = gitCredentialFormContent(context); @@ -38,9 +38,11 @@ export default function (context: Context) { header(h1("Edit Git credentials")), p(), GitCredentialFormContent({ username: "TODO" }), - footer(ButtonEdit({ type: "submit" }, "Save"), ButtonBack()), - h2("Danger Zone"), - ButtonDelete({ href: `${git_credential_id}/destroy` }, "Danger Zone") + footer(ButtonEdit({ type: "submit" }, "Save"), ButtonBack()) + // h2("Danger Zone"), + // div( + // ButtonDelete({ href: `${git_credential_id}/destroy` }, "Danger Zone") + // ) ) ); }; diff --git a/examples/gccd/src/pages/infra/infraStepperPage.ts b/examples/gccd/src/pages/infra/infraStepperPage.ts index 91fcee7c..aa2961af 100644 --- a/examples/gccd/src/pages/infra/infraStepperPage.ts +++ b/examples/gccd/src/pages/infra/infraStepperPage.ts @@ -8,7 +8,7 @@ import configAzure from "../../components/infra/configAzure"; import configGoogle from "../../components/infra/configGoogle"; import infraSettings from "../../components/infra/infraSettings"; import gitCredentialConfig from "../../components/infra/gitCredentialConfig"; -import gitRepositoryConfig from "../../components/infra/gitRepositoryConfig"; +//import gitRepositoryConfig from "../../components/infra/gitRepositoryConfig"; import stepperFinal from "../../components/infra/stepperFinal"; export default (context: Context) => { @@ -21,7 +21,7 @@ export default (context: Context) => { const ConfigAzure = configAzure(context); const ConfigGoogle = configGoogle(context); const GitCredentialConfig = gitCredentialConfig(context); - const GitRepositoryConfig = gitRepositoryConfig(context); + //const GitRepositoryConfig = gitRepositoryConfig(context); const StepperFinal = stepperFinal(context); const providerNameState = bau.state(""); @@ -125,16 +125,16 @@ export default (context: Context) => { Content: () => GitCredentialConfig({ onclickPrevious, onclickGitCredential }), }, - { - name: "Git Repository", - Header, - Content: () => - GitRepositoryConfig({ - onclickPrevious, - onclickGitRepository, - gitCredential: _gitCredential, - }), - }, + // { + // name: "Git Repository", + // Header, + // Content: () => + // GitRepositoryConfig({ + // onclickPrevious, + // onclickGitRepository, + // gitCredential: _gitCredential, + // }), + // }, { name: "Review", Header, diff --git a/examples/gccd/src/pages/workspace/workspaceDetailPage.ts b/examples/gccd/src/pages/workspace/workspaceDetailPage.ts index 12f7e407..3c5f4d14 100644 --- a/examples/gccd/src/pages/workspace/workspaceDetailPage.ts +++ b/examples/gccd/src/pages/workspace/workspaceDetailPage.ts @@ -1,38 +1,35 @@ import { Context } from "@grucloud/bau-ui/context"; import form from "@grucloud/bau-ui/form"; -import spinner from "@grucloud/bau-ui/spinner"; import button from "@grucloud/bau-ui/button"; import dropdownMenu from "@grucloud/bau-ui/dropdownMenu"; +import tabs, { Tabs } from "@grucloud/bau-ui/tabs"; import page from "../../components/page"; import workspaceDetailContent from "../../components/workspace/workspaceDetailContent"; import runList from "../../components/run/runList"; import cloudAuthenticationList from "../../components/cloudAuthentication/cloudAuthenticationList"; +import gitRepositoryForm from "../../components/gitRepository/gitRepositoryForm"; export default function (context: Context) { const { bau, stores, config, css } = context; - const { h1, h2, header, a } = bau.tags; + const { div, h2, header, a, section } = bau.tags; const { getByIdQuery } = stores.workspace; const DropdownMenu = dropdownMenu(context); const Page = page(context); const Form = form(context); - const Spinner = spinner(context, { size: "lg" }); - const ButtonAddWorkspace = button(context, { + const ButtonAdd = button(context, { color: "primary", variant: "solid", }); - const ButtonDelete = button(context, { variant: "outline", color: "danger" }); const WorkspaceDetailContent = workspaceDetailContent(context); const RunList = runList(context); const CloudAuthenticationList = cloudAuthenticationList(context); + const GitRepositoryForm = gitRepositoryForm(context); + return function WorkspaceDetailPage(props: any) { + const { org_id, project_id, workspace_id } = props; - return function WorkspaceDetailPage({ - org_id, - project_id, - workspace_id, - }: any) { getByIdQuery.run({ org_id, project_id, workspace_id }); stores.run.getAllByWorkspaceQuery.run({ org_id, project_id, workspace_id }); stores.cloudAuthentication.getAllByWorkspaceQuery.run({ @@ -40,6 +37,70 @@ export default function (context: Context) { project_id, workspace_id, }); + stores.gitCredential.getAllByOrgQuery.run({ org_id }); + + const tabDefs: Tabs = [ + { + name: "summary", + Header: () => a({ href: "#summary" }, "Workspace Summary"), + Content: () => + section( + header(), + () => + !getByIdQuery.loading.val && + WorkspaceDetailContent(getByIdQuery.data.val) + ), + }, + { + name: "runs", + Header: () => a({ href: "#runs" }, "Runs"), + Content: () => + section( + div( + ButtonAdd( + { + href: `${config.base}/org/${org_id}/projects/${project_id}/workspaces/${workspace_id}/runs/create`, + }, + "+ New Run" + ) + ), + RunList(stores.run.getAllByWorkspaceQuery) + ), + }, + { + name: "cloudAuthentication", + Header: () => + a({ href: "#cloudAuthentication" }, "Cloud Authentication"), + Content: () => + section( + h2("Cloud Authentication"), + DropdownMenu({ + items, + ListItem, + label: "+ Add", + }), + () => + CloudAuthenticationList( + stores.cloudAuthentication.getAllByWorkspaceQuery + ) + ), + }, + { + name: "gitRepository", + Header: () => a({ href: "#gitRepository" }, "Git Repository"), + Content: () => + section( + GitRepositoryForm({ + org_id, + project_id, + workspace_id, + gitCredential: stores.gitCredential, + }) + ), + }, + ]; + + const Tabs = tabs(context, { tabDefs, variant: "plain", color: "neutral" }); const items = [ { @@ -68,63 +129,6 @@ export default function (context: Context) { option.text ); - return Page( - Form( - header( - h1("Workspace Details"), - ButtonAddWorkspace( - { - href: `${config.base}/org/${org_id}/projects/${project_id}/workspaces/${workspace_id}/runs/create`, - }, - "+ New Run" - ) - ), - () => - !getByIdQuery.loading.val && - WorkspaceDetailContent(getByIdQuery.data.val), - h2("Cloud Authentication"), - DropdownMenu({ - items, - ListItem, - label: "+ Add", - }), - // ButtonGroup( - // ButtonAdd( - // { - // href: `${config.base}/org/${org_id}/projects/${project_id}/workspaces/${workspace_id}/cloud_authentication/create/aws`, - // }, - // "+ AWS " - // ), - // ButtonAdd( - // { - // href: `${config.base}/org/${org_id}/projects/${project_id}/workspaces/${workspace_id}/cloud_authentication/create/azure`, - // }, - // "+ Azure " - // ), - // ButtonAdd( - // { - // href: `${config.base}/org/${org_id}/projects/${project_id}/workspaces/${workspace_id}/cloud_authentication/create/google`, - // }, - // "+ Google Cloud " - // ) - // ), - () => - CloudAuthenticationList( - stores.cloudAuthentication.getAllByWorkspaceQuery - ), - h2("Runs"), - RunList(stores.run.getAllByWorkspaceQuery), - h2("Danger Zone"), - ButtonDelete( - { - href: `${config.base}/org/${org_id}/projects/${project_id}/workspaces/${workspace_id}/destroy`, - }, - "Danger Zone" - ) - ), - Spinner({ - visibility: getByIdQuery.loading, - }) - ); + return Page(Form(Tabs(props))); }; } diff --git a/examples/gccd/src/stores/gitRepositoryStore.ts b/examples/gccd/src/stores/gitRepositoryStore.ts index 49b3e9c2..108867f8 100644 --- a/examples/gccd/src/stores/gitRepositoryStore.ts +++ b/examples/gccd/src/stores/gitRepositoryStore.ts @@ -5,21 +5,23 @@ export default function (context: Context) { const { rest } = context; const query = useQuery(context); - const getAllQuery = query(() => rest.get("git_repository"), { - initialState: [], - }); - const createQuery = query((data: any) => rest.post("git_repository", data)); - const getByIdQuery = query((id: string) => rest.get(`git_repository/${id}`)); - const patchQuery = query((id: string, data: object) => - rest.patch(`git_repository/${id}`, data) - ); - const deleteQuery = query((id: string) => rest.del(`git_repository/${id}`)); - return { - getAllQuery, - getByIdQuery, - createQuery, - patchQuery, - deleteQuery, + createQuery: query(({ org_id, project_id, workspace_id }: any, data: any) => + rest.post( + `org/${org_id}/project/${project_id}/workspace/${workspace_id}/git_repository`, + data + ) + ), + getByIdQuery: query(({ org_id, project_id, workspace_id }: any) => + rest.get( + `org/${org_id}/project/${project_id}/workspace/${workspace_id}/git_repository` + ) + ), + patchQuery: query(({ org_id, project_id, workspace_id }: any, data: any) => + rest.patch( + `org/${org_id}/project/${project_id}/workspaces/${workspace_id}/git_repository`, + data + ) + ), }; }