diff --git a/ui/app/pages/environments/environments.tsx b/ui/app/pages/environments/environments.tsx index efec7f22..7fa329a5 100644 --- a/ui/app/pages/environments/environments.tsx +++ b/ui/app/pages/environments/environments.tsx @@ -34,7 +34,7 @@ export const useEnvironments = () => { await configureAxios(instanceKey!) return fetchEnvironments(workspaceKey!, projectKey!) }, - enabled: !!instanceKey && !!workspaceKey, + enabled: !!instanceKey && !!workspaceKey && !!projectKey, } ) return query diff --git a/ui/app/pages/flags/api.tsx b/ui/app/pages/flags/api.tsx index abc214ae..1231ed9f 100644 --- a/ui/app/pages/flags/api.tsx +++ b/ui/app/pages/flags/api.tsx @@ -1,5 +1,6 @@ import { axios } from '../../lib/axios' import { FlagbaseParams } from '../../lib/use-flagbase-params' +import { TargetingResponse, TargetingRuleResponse } from '../targeting/api' import { UpdateBody } from '../workspaces/api' export type Flag = { @@ -77,7 +78,7 @@ export const fetchTargeting = async ({ projectKey, environmentKey, flagKey, -}: Partial) => { +}: Partial): Promise => { const { data } = await axios.get(`/targeting/${workspaceKey}/${projectKey}/${environmentKey}/${flagKey}`) return data } @@ -87,7 +88,7 @@ export const fetchTargetingRules = async ({ projectKey, environmentKey, flagKey, -}: Partial) => { +}: Partial): Promise => { const { data } = await axios.get(`/targeting/${workspaceKey}/${projectKey}/${environmentKey}/${flagKey}/rules`) return data } @@ -98,7 +99,7 @@ export const fetchTargetingRule = async ({ environmentKey, flagKey, ruleKey, -}: Partial) => { +}: Partial): Promise => { const { data } = await axios.get( `/targeting/${workspaceKey}/${projectKey}/${environmentKey}/${flagKey}/rules/${ruleKey}` ) diff --git a/ui/app/pages/flags/constants.ts b/ui/app/pages/flags/constants.ts index d9572961..a149bc90 100644 --- a/ui/app/pages/flags/constants.ts +++ b/ui/app/pages/flags/constants.ts @@ -10,8 +10,8 @@ export const flagsColumn = [ }, { title: 'Key', - dataIndex: 'key', - key: 'key', + dataIndex: 'flagKey', + key: 'flagKey', }, { title: 'Description', diff --git a/ui/app/pages/flags/flags.tsx b/ui/app/pages/flags/flags.tsx index bcc203a9..9ef6c642 100644 --- a/ui/app/pages/flags/flags.tsx +++ b/ui/app/pages/flags/flags.tsx @@ -54,6 +54,7 @@ const convertFlags = ({ flags, environment }: { flags: Flag[]; environment: Envi return Object.values(flags).map((flag: Flag, index: number) => { return { id: index, + key: flag.attributes.key, title: flag.attributes.name, href: `${flag.attributes.key}/environments/${environment?.attributes.key}`, name: {flag.attributes.name}, @@ -68,7 +69,7 @@ const convertFlags = ({ flags, environment }: { flags: Flag[]; environment: Envi ), action: , - key: flag.attributes.key, + flagKey: flag.attributes.key, } }) } diff --git a/ui/app/pages/instances/instances.constants.tsx b/ui/app/pages/instances/instances.constants.tsx index 55f1c734..b001d4ca 100644 --- a/ui/app/pages/instances/instances.constants.tsx +++ b/ui/app/pages/instances/instances.constants.tsx @@ -21,8 +21,14 @@ export const instanceColumns = [ ] export const InstanceSchema = Yup.object().shape({ + name: Yup.string().min(2, 'Too Short!').max(50, 'Too Long!').required('This field is required'), key: Yup.string().min(2, 'Too Short!').max(50, 'Too Long!').required('This field is required'), - connectionString: Yup.string().min(2, 'Too Short!').max(50, 'Too Long!').required('This field is required'), + connectionString: Yup.string() + .matches( + /((https?):\/\/)?(www.)?[a-z0-9]+(\.[a-z]{2,}){1,3}(#?\/?[a-zA-Z0-9#]+)*\/?(\?[a-zA-Z0-9-_]+=[a-zA-Z0-9-%]+&?)?$/, + 'Please enter a valid Flagbase instance URL' + ) + .required('Please enter a valid Flagbase instance URL'), accessKey: Yup.string().min(2, 'Too Short!').max(50, 'Too Long!').required('This field is required'), accessSecret: Yup.string().min(2, 'Too Short!').max(50, 'Too Long!').required('This field is required'), }) diff --git a/ui/app/pages/instances/instances.modal.tsx b/ui/app/pages/instances/instances.modal.tsx index 7596e9c4..b149ed54 100644 --- a/ui/app/pages/instances/instances.modal.tsx +++ b/ui/app/pages/instances/instances.modal.tsx @@ -20,10 +20,10 @@ export const AddNewInstanceModal = ({ visible, setVisible }: ReactState) => { const { isSuccess, isError } = mutation const onSubmit = (values: OmittedInstance, { setSubmitting }: FormikHelpers) => { - setIsLoading(true); + setIsLoading(true) mutation.mutate(values) setSubmitting(false) - setTimeout(() => setIsLoading(false), 2000); + setTimeout(() => setIsLoading(false), 2000) } useEffect(() => { @@ -106,7 +106,12 @@ export const AddNewInstanceModal = ({ visible, setVisible }: ReactState) => { placeholder="Secret" type="password" /> - diff --git a/ui/app/pages/projects/api.ts b/ui/app/pages/projects/api.ts index 9c282fc9..28119f82 100644 --- a/ui/app/pages/projects/api.ts +++ b/ui/app/pages/projects/api.ts @@ -20,8 +20,8 @@ export const fetchProjects = async (workspaceKey: string) => { return result.data } -export const deleteProject = async (ProjectKey: string) => { - return axios.delete(`/projects/${ProjectKey}`) +export const deleteProject = async ({ workspaceKey, projectKey }: { workspaceKey: string; projectKey: string }) => { + return axios.delete(`/projects/${workspaceKey}/${projectKey}`) } export const createProject = async (name: string, description: string, tags: string[], workspaceKey: string) => { diff --git a/ui/app/pages/projects/projects.edit.tsx b/ui/app/pages/projects/projects.edit.tsx index e775e326..c8f64b06 100644 --- a/ui/app/pages/projects/projects.edit.tsx +++ b/ui/app/pages/projects/projects.edit.tsx @@ -62,7 +62,7 @@ const EditProject = () => { throw new Error('Missing required params') } - const { mutate: remove } = useRemoveProject(instanceKey, workspaceKey) + const { mutate: remove } = useRemoveProject() const { mutate: update, isSuccess, error } = useUpdateProject(instanceKey) if (!project) { diff --git a/ui/app/pages/projects/projects.tsx b/ui/app/pages/projects/projects.tsx index 67998cb3..3e256fe8 100644 --- a/ui/app/pages/projects/projects.tsx +++ b/ui/app/pages/projects/projects.tsx @@ -64,11 +64,13 @@ export const convertProjects = ({ }) } -export const useRemoveProject = (instanceKey: string, workspaceKey: string) => { +export const useRemoveProject = () => { + const { instanceKey, workspaceKey } = useFlagbaseParams() const queryClient = useQueryClient() const mutation = useMutation({ + mutationKey: ['projects', instanceKey, workspaceKey], mutationFn: async (projectKey: string) => { - await deleteProject(projectKey) + await deleteProject({ projectKey, workspaceKey }) }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['projects', instanceKey, workspaceKey] }) @@ -77,9 +79,11 @@ export const useRemoveProject = (instanceKey: string, workspaceKey: string) => { return mutation } -export const useAddProject = (instanceKey: string, workspaceKey: string) => { +export const useAddProject = () => { + const { instanceKey, workspaceKey } = useFlagbaseParams() const queryClient = useQueryClient() const mutation = useMutation({ + mutationKey: ['projects', instanceKey, workspaceKey], mutationFn: async (values: Omit) => { await createProject(values.name, values.description, values.tags, workspaceKey) }, @@ -99,7 +103,6 @@ export const useProjects = (options?: any) => { return fetchProjects(workspaceKey!) }, enabled: !!instanceKey && !!workspaceKey, - refetchOnWindowFocus: false, }) return query } diff --git a/ui/app/pages/targeting/targeting.tsx b/ui/app/pages/targeting/targeting.tsx index 785e342d..3eb134ad 100644 --- a/ui/app/pages/targeting/targeting.tsx +++ b/ui/app/pages/targeting/targeting.tsx @@ -23,11 +23,11 @@ import EmptyState from '../../../components/empty-state' import CodeUsageModal from '../../../components/code-usage-modal' import { useMutation, useQuery, useQueryClient } from 'react-query' import { getTargetingKey, getTargetingRulesKey } from '../../router/loaders' -import { configureAxios } from '../../lib/axios' import { fetchTargeting, fetchTargetingRules } from '../flags/api' import { useVariations } from '../variations/variations' import { TargetingRules } from './targeting-rules' import { useNotification } from '../../hooks/use-notification' +import { configureAxios } from '../../lib/axios' type VariationResponse = { type: 'variation' diff --git a/ui/components/page-layout/page-layout.tsx b/ui/components/page-layout/page-layout.tsx index f2201b4f..872ffd84 100644 --- a/ui/components/page-layout/page-layout.tsx +++ b/ui/components/page-layout/page-layout.tsx @@ -2,7 +2,6 @@ import React, { createElement, Fragment, ReactNode, useEffect } from 'react' import { Link, Outlet, useLocation, useMatches, useParams } from 'react-router-dom' import flag from '../../assets/flagbaseLogo.svg' -import flagOld from '../../assets/flag.svg' import { useState } from 'react' import { Disclosure, Transition, Popover, Dialog } from '@headlessui/react' import { @@ -11,7 +10,6 @@ import { ChevronDownIcon, ChevronRightIcon, XMarkIcon, - CodeBracketIcon } from '@heroicons/react/24/outline' import { useInstances } from '../../app/pages/instances/instances' import { @@ -36,6 +34,7 @@ import { Flag } from '../../app/pages/flags/api' import { useVariations } from '../../app/pages/variations/variations' import { useSDKs } from '../../app/pages/sdks/sdks' import { Footer } from './footer' +import { CopyRow } from '../table' import CodeUsageModal from '../code-usage-modal' const instancesDescription = `An "instance" refers to a Flagbase core installation, running on a single VPS or clustered in a datacenter.` @@ -552,7 +551,7 @@ export const PageHeadings = () => { } else if (instanceKey && workspaceKey && projectKey && flagKey) { setPageHeading({ title: activeFlag?.attributes.name || flagKey, - subtitle: 'Feature Flag', + subtitle: , backHref: `/${instanceKey}/workspaces/${workspaceKey}/projects/${projectKey}/flags`, tabs: [ { diff --git a/ui/components/table/table.tsx b/ui/components/table/table.tsx index fc132e26..5b06bd8e 100644 --- a/ui/components/table/table.tsx +++ b/ui/components/table/table.tsx @@ -1,9 +1,11 @@ -import React from 'react' +import React, { useState } from 'react' import { Table as AntdTable, TableColumnProps, TableProps as AntdTableProps, TableColumnType } from 'antd' import styled from '@emotion/styled' import EmptyState from '../empty-state' import Button from '../button/button' import { useNavigate } from 'react-router-dom' +import { DocumentDuplicateIcon } from '@heroicons/react/20/solid' +import { Notification } from '../notification/notification' export type TableProps = { loading: boolean @@ -18,6 +20,37 @@ const StyledTable = styled(AntdTable)` } ` +export const CopyRow = ({ text }: { text: string }) => { + const [copied, setCopied] = useState(false) + return ( +
+
{text}
+ + setCopied(show)} + /> +
+ ) +} + const Table: React.FC = ({ loading, columns, @@ -40,6 +73,7 @@ const Table: React.FC = ({ onRow={(record, rowIndex) => { return { onClick: (event) => { + event.preventDefault() const { href } = record if (href) { navigate(href)