diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..019e3b8eb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,94 @@ +# HyperDX API and App Server Dockerfile +# This Dockerfile creates a single image with both the API and App servers + +ARG NODE_VERSION=22.16.0 + +# Base stage with Node.js and dependencies +FROM node:${NODE_VERSION}-alpine AS base + +WORKDIR /app + +# Copy workspace configuration files +COPY .yarn ./.yarn +COPY .yarnrc.yml yarn.lock package.json nx.json .prettierrc .prettierignore ./ + +# Copy package.json files for all packages +COPY ./packages/common-utils/package.json ./packages/common-utils/ +COPY ./packages/api/package.json ./packages/api/ +COPY ./packages/app/package.json ./packages/app/ + +# Install dependencies +RUN apk add --no-cache libc6-compat +RUN yarn install --mode=skip-build && yarn cache clean + +# Builder stage +FROM base AS builder + +WORKDIR /app + +# Copy source code for all packages +COPY ./packages/common-utils ./packages/common-utils +COPY ./packages/api ./packages/api +COPY ./packages/app ./packages/app + +# Set build environment variables +ENV NEXT_TELEMETRY_DISABLED 1 +ENV NEXT_PUBLIC_IS_LOCAL_MODE false +ENV NX_DAEMON=false + +# Build packages in dependency order +RUN yarn workspace @hyperdx/common-utils build +RUN yarn workspace @hyperdx/api build +RUN yarn workspace @hyperdx/app build + +# Production stage +FROM node:${NODE_VERSION}-alpine AS production + +ARG CODE_VERSION=2.1.1 + +ENV CODE_VERSION=2.1.1 +ENV NODE_ENV production + +# Install concurrently for running multiple processes +RUN npm install -g concurrently@9.1.0 + +# Create non-root user +RUN addgroup -g 1001 -S nodejs +RUN adduser -S nodejs -u 1001 + +USER nodejs + +WORKDIR /app + +# Copy built API +COPY --chown=nodejs:nodejs --from=builder /app/packages/api/dist ./packages/api/dist +COPY --chown=nodejs:nodejs --from=builder /app/packages/api/package.json ./packages/api/package.json + +# Copy built App (Next.js) +COPY --chown=nodejs:nodejs --from=builder /app/packages/app/.next ./packages/app/.next +COPY --chown=nodejs:nodejs --from=builder /app/packages/app/public ./packages/app/public +COPY --chown=nodejs:nodejs --from=builder /app/packages/app/package.json ./packages/app/package.json +COPY --chown=nodejs:nodejs --from=builder /app/packages/app/next.config.js ./packages/app/next.config.js + +# Copy built common-utils +COPY --chown=nodejs:nodejs --from=builder /app/packages/common-utils/dist ./packages/common-utils/dist +COPY --chown=nodejs:nodejs --from=builder /app/packages/common-utils/package.json ./packages/common-utils/package.json + +# Copy node_modules for runtime dependencies +COPY --chown=nodejs:nodejs --from=builder /app/node_modules ./node_modules +COPY --chown=nodejs:nodejs --from=builder /app/packages/api/node_modules ./packages/api/node_modules +COPY --chown=nodejs:nodejs --from=builder /app/packages/app/node_modules ./packages/app/node_modules +COPY --chown=nodejs:nodejs --from=builder /app/packages/common-utils/node_modules ./packages/common-utils/node_modules + +# Copy and set up entry script +COPY --chown=nodejs:nodejs docker/hyperdx/entry.prod.sh /etc/local/entry.sh +RUN chmod +x /etc/local/entry.sh + +# Expose ports +EXPOSE 8000 8080 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8000/health || exit 1 + +ENTRYPOINT ["sh", "/etc/local/entry.sh"] \ No newline at end of file diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 000000000..959611225 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,87 @@ +pipeline { + agent { + kubernetes { + label 'hyperdx' + yaml """ +apiVersion: v1 +kind: Pod +spec: + containers: + - name: dind + image: sc-mum-armory.platform.internal/devops/dind:v2 + securityContext: + privileged: true + env: + - name: DOCKER_HOST + value: tcp://localhost:2375 + - name: DOCKER_TLS_CERTDIR + value: "" + volumeMounts: + - name: dind-storage + mountPath: /var/lib/docker + readinessProbe: + tcpSocket: + port: 2375 + initialDelaySeconds: 30 + periodSeconds: 10 + livenessProbe: + tcpSocket: + port: 2375 + initialDelaySeconds: 30 + periodSeconds: 20 + - name: builder + image: sc-mum-armory.platform.internal/devops/builder-image-armory + command: + - sleep + - infinity + env: + - name: DOCKER_HOST + value: tcp://localhost:2375 + - name: DOCKER_BUILDKIT + value: "0" + volumeMounts: + - name: jenkins-sa + mountPath: /root/.gcp/ + volumes: + - name: dind-storage + emptyDir: {} + - name: jenkins-sa + secret: + secretName: jenkins-sa +""" + } + } + + environment { + sc_regions="mumbai" + GITHUB_TOKEN = credentials('github-access') + app="hyperdx" + buildarg_DEPLOYMENT_ID="hyperdx-$GIT_COMMIT" + buildarg_GITHUB_TOKEN="${GITHUB_TOKEN}" + } + + stages { + stage('build') { + steps { + container('builder') { + sh "armory build" + } + } + } + + stage('push') { + when { + anyOf { + branch 'main' + branch 'staging' + branch 'feature/*' + } + } + steps { + container('builder') { + sh "armory push" + } + } + } + } +} \ No newline at end of file diff --git a/docker/hyperdx/entry.prod.sh b/docker/hyperdx/entry.prod.sh index cf619611d..88a0d2283 100644 --- a/docker/hyperdx/entry.prod.sh +++ b/docker/hyperdx/entry.prod.sh @@ -8,13 +8,25 @@ export OPAMP_PORT=${HYPERDX_OPAMP_PORT:-4320} export IS_LOCAL_APP_MODE="REQUIRED_AUTH" echo "" +echo "Starting HyperDX services..." echo "Visit the HyperDX UI at $FRONTEND_URL" echo "" +# Check if required files exist +if [ ! -f "./packages/api/dist/index.js" ]; then + echo "ERROR: API build not found at ./packages/api/dist/index.js" + exit 1 +fi + +if [ ! -d "./packages/app/.next" ]; then + echo "ERROR: App build not found at ./packages/app/.next" + exit 1 +fi + # Use concurrently to run both the API and App servers npx concurrently \ "--kill-others-on-fail" \ "--names=API,APP,ALERT-TASK" \ - "PORT=${HYPERDX_API_PORT:-8000} HYPERDX_APP_PORT=${HYPERDX_APP_PORT:-8080} node -r ./packages/api/tracing ./packages/api/index" \ - "cd ./packages/app/packages/app && HOSTNAME='0.0.0.0' HYPERDX_API_PORT=${HYPERDX_API_PORT:-8000} PORT=${HYPERDX_APP_PORT:-8080} node server.js" \ - "node -r ./packages/api/tracing ./packages/api/tasks/index check-alerts" + "PORT=${HYPERDX_API_PORT:-8000} HYPERDX_APP_PORT=${HYPERDX_APP_PORT:-8080} node -r ./packages/api/dist/tracing.js ./packages/api/dist/index.js" \ + "cd ./packages/app && HOSTNAME='0.0.0.0' HYPERDX_API_PORT=${HYPERDX_API_PORT:-8000} PORT=${HYPERDX_APP_PORT:-8080} yarn start" \ + "node -r ./packages/api/dist/tracing.js ./packages/api/dist/tasks/index.js check-alerts" diff --git a/packages/app/src/AutocompleteInput.tsx b/packages/app/src/AutocompleteInput.tsx index 03110a485..f2c942eab 100644 --- a/packages/app/src/AutocompleteInput.tsx +++ b/packages/app/src/AutocompleteInput.tsx @@ -284,6 +284,7 @@ export default function AutocompleteInput({ if (queryHistoryType && value) { setQueryHistory(value); } + setIsInputDropdownOpen(false); // Close suggestion box on Enter onSubmit?.(); } } diff --git a/packages/app/src/DBSearchPage.tsx b/packages/app/src/DBSearchPage.tsx index 40c1c2e8b..4a45a7e37 100644 --- a/packages/app/src/DBSearchPage.tsx +++ b/packages/app/src/DBSearchPage.tsx @@ -1,3 +1,4 @@ +/* eslint-disable */ import { FormEvent, FormEventHandler, @@ -58,7 +59,6 @@ import { notifications } from '@mantine/notifications'; import { useIsFetching } from '@tanstack/react-query'; import CodeMirror from '@uiw/react-codemirror'; -import { useTimeChartSettings } from '@/ChartUtils'; import { ContactSupportText } from '@/components/ContactSupportText'; import DBDeltaChart from '@/components/DBDeltaChart'; import DBHeatmapChart from '@/components/DBHeatmapChart'; @@ -79,10 +79,8 @@ import { Tags } from '@/components/Tags'; import { TimePicker } from '@/components/TimePicker'; import WhereLanguageControlled from '@/components/WhereLanguageControlled'; import { IS_LOCAL_MODE } from '@/config'; -import { - useAliasMapFromChartConfig, - useQueriedChartConfig, -} from '@/hooks/useChartConfig'; +import { useAuthEmails } from '@/hooks/useAuthEmails'; +import { useAliasMapFromChartConfig } from '@/hooks/useChartConfig'; import { useExplainQuery } from '@/hooks/useExplainQuery'; import { withAppNav } from '@/layout'; import { @@ -99,14 +97,14 @@ import { useSource, useSources, } from '@/source'; -import { parseTimeQuery, useNewTimeQuery } from '@/timeQuery'; +import { dateRangeToString, useNewTimeQuery } from '@/timeQuery'; import { QUERY_LOCAL_STORAGE, useLocalStorage, usePrevious } from '@/utils'; import { SQLPreview } from './components/ChartSQLPreview'; import PatternTable from './components/PatternTable'; import { useSqlSuggestions } from './hooks/useSqlSuggestions'; import api from './api'; -import { LOCAL_STORE_CONNECTIONS_KEY } from './connection'; +import { LOCAL_STORE_CONNECTIONS_KEY, useConnections } from './connection'; import { DBSearchPageAlertModal } from './DBSearchPageAlertModal'; import { SearchConfig } from './types'; @@ -118,6 +116,7 @@ const SearchConfigSchema = z.object({ where: z.string(), whereLanguage: z.enum(['sql', 'lucene']), orderBy: z.string(), + connection: z.string().optional(), filters: z.array( z.union([ z.object({ @@ -393,8 +392,13 @@ function SaveSearchModal({ ); } -// TODO: This is a hack to set the default time range -const defaultTimeRange = parseTimeQuery('Past 15m', false) as [Date, Date]; +// Create a fixed 15-minute time range that won't auto-update +const createFixedTimeRange = (): [Date, Date] => { + const end = new Date(); + const start = new Date(end.getTime() - 15 * 60 * 1000); // 15 minutes ago + return [start, end]; +}; +const defaultTimeRange = createFixedTimeRange(); function useLiveUpdate({ isLive, @@ -455,6 +459,12 @@ function useSearchedConfigToChartConfig({ id: source, }); +const { data: connections } = useConnections(); +const connectionName = useMemo( + () => connections?.find(c => c.id === sourceObj?.connection)?.name, + [connections, sourceObj?.connection], +); + return useMemo(() => { if (sourceObj != null) { return { @@ -478,6 +488,7 @@ function useSearchedConfigToChartConfig({ timestampValueExpression: sourceObj.timestampValueExpression, implicitColumnExpression: sourceObj.implicitColumnExpression, connection: sourceObj.connection, + connectionName, displayType: DisplayType.Search, orderBy: orderBy || @@ -489,7 +500,7 @@ function useSearchedConfigToChartConfig({ } return { data: null, isLoading }; - }, [sourceObj, isLoading, select, filters, where, whereLanguage]); + }, [sourceObj, isLoading, select, filters, where, whereLanguage, orderBy]); } // This is outside as it needs to be a stable reference @@ -500,6 +511,7 @@ const queryStateMap = { whereLanguage: parseAsStringEnum<'sql' | 'lucene'>(['sql', 'lucene']), filters: parseAsJson(), orderBy: parseAsString, + connection: parseAsString, }; function DBSearchPage() { @@ -541,7 +553,7 @@ function DBSearchPage() { ); const [_isLive, setIsLive] = useQueryState('isLive', parseAsBoolean); - const isLive = _isLive ?? true; + const isLive = _isLive ?? false; // Default to false instead of true useEffect(() => { if (analysisMode === 'delta' || analysisMode === 'pattern') { @@ -567,17 +579,50 @@ function DBSearchPage() { [sources, lastSelectedSourceId], ); - const { - control, - watch, - setValue, - reset, - handleSubmit, - getValues, - formState, - setError, - resetField, - } = useForm({ + const { data: connections } = useConnections(); + const { data: inputSourceObjs } = useSources(); + + // Get initial connection from URL or derive from source + const getInitialConnection = useCallback(() => { + // First try URL connection + if (searchedConfig.connection) { + return searchedConfig.connection; + } + // Then try to get connection from current source + if (searchedConfig.source && inputSourceObjs) { + const sourceObj = inputSourceObjs.find( + s => s.id === searchedConfig.source, + ); + if (sourceObj?.connection) { + return sourceObj.connection; + } + } + + // Then try connection from last selected source + if (lastSelectedSourceId && inputSourceObjs) { + const lastSource = inputSourceObjs.find( + s => s.id === lastSelectedSourceId, + ); + if (lastSource?.connection) { + return lastSource.connection; + } + } + + // Finally fallback to first available connection + return connections?.[0]?.id || undefined; + }, [ + searchedConfig.connection, + searchedConfig.source, + inputSourceObjs, + lastSelectedSourceId, + connections, + ]); + + const { control, watch, setValue, reset, handleSubmit, formState } = useForm< + SearchConfigFromSchema & { + connection?: string; + } + >({ values: { select: searchedConfig.select || '', where: searchedConfig.where || '', @@ -585,6 +630,7 @@ function DBSearchPage() { source: searchedConfig.source || defaultSourceId, filters: searchedConfig.filters ?? [], orderBy: searchedConfig.orderBy ?? '', + connection: getInitialConnection(), }, resetOptions: { keepDirtyValues: true, @@ -594,8 +640,6 @@ function DBSearchPage() { }); const inputSource = watch('source'); - // const { data: inputSourceObj } = useSource({ id: inputSource }); - const { data: inputSourceObjs } = useSources(); const inputSourceObj = inputSourceObjs?.find(s => s.id === inputSource); // When source changes, make sure select and orderby fields are set to default @@ -609,14 +653,26 @@ function DBSearchPage() { const [rowId, setRowId] = useQueryState('rowWhere'); + // Create initial fixed time display value + const initialTimeDisplay = useMemo(() => { + return dateRangeToString(defaultTimeRange, false); + }, []); + const [displayedTimeInputValue, setDisplayedTimeInputValue] = - useState('Live Tail'); + useState(initialTimeDisplay); + + // Set isLive to false when starting with a fixed time range + useEffect(() => { + if (displayedTimeInputValue === initialTimeDisplay && _isLive == null) { + setIsLive(false); + } + }, [displayedTimeInputValue, _isLive, setIsLive, initialTimeDisplay]); const { from, to, isReady, searchedTimeRange, onSearch, onTimeRangeSelect } = useNewTimeQuery({ - initialDisplayValue: 'Live Tail', + initialDisplayValue: initialTimeDisplay, initialTimeRange: defaultTimeRange, - showRelativeInterval: isLive ?? true, + showRelativeInterval: isLive ?? false, setDisplayedTimeInputValue, updateInput: !isLive, }); @@ -625,13 +681,9 @@ function DBSearchPage() { // If live tail is null, and time range is null, let's live tail useEffect(() => { if (_isLive == null && isReady) { - if (from == null && to == null) { - setIsLive(true); - } else { - setIsLive(false); - } + setIsLive(false); } - }, [_isLive, setIsLive, from, to, isReady]); + }, [_isLive, setIsLive, isReady]); // Sync url state back with form state // (ex. for history navigation) @@ -646,9 +698,14 @@ function DBSearchPage() { source: searchedConfig?.source ?? undefined, filters: searchedConfig?.filters ?? [], orderBy: searchedConfig?.orderBy ?? '', + // Use URL connection if available, otherwise preserve current form connection + connection: + searchedConfig?.connection ?? + watch('connection') ?? + getInitialConnection(), }); } - }, [searchedConfig, reset, prevSearched]); + }, [searchedConfig, reset, prevSearched, getInitialConnection, watch]); // Populate searched query with saved search if the query params have // been wiped (ex. clicking on the same saved search again) @@ -715,7 +772,15 @@ function DBSearchPage() { const onSubmit = useCallback(() => { onSearch(displayedTimeInputValue); handleSubmit( - ({ select, where, whereLanguage, source, filters, orderBy }) => { + ({ + select, + where, + whereLanguage, + source, + filters, + orderBy, + connection, + }) => { setSearchedConfig({ select, where, @@ -723,6 +788,7 @@ function DBSearchPage() { source, filters, orderBy, + connection, }); }, )(); @@ -774,6 +840,20 @@ function DBSearchPage() { ); // Clear all search filters searchFilters.clearAllFilters(); + // Trigger refresh for new source + setTimeout(() => debouncedSubmit(), 0); + } + } + + // If the user changes the connection dropdown, update URL state and trigger data refresh + if (name === 'connection' && type === 'change') { + if (data.connection) { + setSearchedConfig(prev => ({ + ...prev, + connection: data.connection, + })); + // Trigger refresh for new connection + setTimeout(() => debouncedSubmit(), 0); } } }); @@ -785,6 +865,8 @@ function DBSearchPage() { inputSourceObjs, searchFilters, setLastSelectedSourceId, + setSearchedConfig, + debouncedSubmit, ]); const onTableScroll = useCallback( @@ -912,19 +994,10 @@ function DBSearchPage() { pause: isAnyQueryFetching || !queryReady || !isTabVisible, }); - // This ensures we only render this conditionally on the client - // otherwise we get SSR hydration issues - const [shouldShowLiveModeHint, setShouldShowLiveModeHint] = useState(false); - useEffect(() => { - setShouldShowLiveModeHint(isLive === false); - }, [isLive]); + // Live Tail disabled; no resume hint needed const { data: me } = api.useMe(); - const handleResumeLiveTail = useCallback(() => { - setIsLive(true); - setDisplayedTimeInputValue('Live Tail'); - onSearch('Live Tail'); - }, [onSearch, setIsLive]); + // Live Tail disabled; no resume handler const dbSqlRowTableConfig = useMemo(() => { if (chartConfig == null) { @@ -935,7 +1008,7 @@ function DBSearchPage() { ...chartConfig, dateRange: searchedTimeRange, }; - }, [me?.team, chartConfig, searchedTimeRange]); + }, [chartConfig, searchedTimeRange]); const displayedColumns = splitAndTrimWithBracket( dbSqlRowTableConfig?.select ?? @@ -996,7 +1069,8 @@ function DBSearchPage() { // Only trigger if we haven't searched yet (no time range in URL) const searchParams = new URLSearchParams(window.location.search); if (!searchParams.has('from') && !searchParams.has('to')) { - onSearch('Live Tail'); + const newTimeRange = new Date(defaultTimeRange[0]); + onSearch(newTimeRange.toISOString()); } } }, [isReady, queryReady, isChartConfigLoading, onSearch]); @@ -1063,7 +1137,7 @@ function DBSearchPage() { onTimeRangeSelect(d1, d2); setIsLive(false); }, - [onTimeRangeSelect], + [onTimeRangeSelect, setIsLive], ); const onTimeChartError = useCallback( @@ -1103,6 +1177,55 @@ function DBSearchPage() { setNewSourceModalOpened(true); }, []); + // Parse demo auth emails from environment variable + const { authArray } = useAuthEmails(); + + // Ensure connection is set when data becomes available + useEffect(() => { + const currentConnection = watch('connection'); + // Wait until sources are loaded so we can derive connection from selected source if present + if (!currentConnection && inputSourceObjs && connections && connections.length > 0) { + const preferredConnectionId = getInitialConnection(); + if (preferredConnectionId) { + setValue('connection', preferredConnectionId); + // Update URL state as well + setSearchedConfig(prev => ({ + ...prev, + connection: preferredConnectionId, + })); + } + } + }, [connections, inputSourceObjs, setValue, watch, getInitialConnection, setSearchedConfig]); + + // Ensure a matching initial source for the selected connection + const selectedConnectionId = watch('connection'); + useEffect(() => { + if (!selectedConnectionId || !inputSourceObjs) return; + const allowedKinds = [SourceKind.Log, SourceKind.Trace]; + const firstMatching = inputSourceObjs.find( + s => + s.connection === selectedConnectionId && allowedKinds.includes(s.kind), + ); + if (!firstMatching) return; + const currentSourceId = watch('source'); + const currentSource = inputSourceObjs.find( + s => s.id === (currentSourceId as any), + ); + // If current source is missing, pick the first matching source for the connection + if (!currentSource) { + setValue('source', firstMatching.id, { + shouldDirty: true, + shouldTouch: true, + }); + return; + } + // If there is a mismatch, prefer keeping the current source and align the connection to it + if (currentSource.connection !== selectedConnectionId) { + setValue('connection', currentSource.connection as any); + setSearchedConfig(prev => ({ ...prev, connection: currentSource.connection })); + } + }, [selectedConnectionId, inputSourceObjs, setValue, setSearchedConfig, watch]); + return ( {!IS_LOCAL_MODE && isAlertModalOpen && ( @@ -1117,28 +1240,35 @@ function DBSearchPage() {
{/* */} + {/* + + */} - + + + - - - - - - - + {authArray[me?.email as keyof typeof authArray] && ( + + + + + + + + )} Sources { - if (range === 'Live Tail') { - setIsLive(true); - } else { - setIsLive(false); - } + // Live Tail disabled + setIsLive(false); onSearch(range); }} - showLive={analysisMode === 'results'} + showLive={false} /> - - - )} + {/* Live Tail disabled; no resume hint */} {chartConfig && dbSqlRowTableConfig && analysisMode === 'results' && ( @@ -1733,7 +1838,7 @@ function DBSearchPage() { const DBSearchPageDynamic = dynamic(async () => DBSearchPage, { ssr: false }); -// @ts-ignore +// @ts-expect-error: Next.js dynamic component layout typing is not declared here DBSearchPageDynamic.getLayout = withAppNav; export default DBSearchPageDynamic; diff --git a/packages/app/src/TeamPage.tsx b/packages/app/src/TeamPage.tsx index cedb300f9..a0166612e 100644 --- a/packages/app/src/TeamPage.tsx +++ b/packages/app/src/TeamPage.tsx @@ -32,7 +32,6 @@ import { } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; import { notifications } from '@mantine/notifications'; -import { UseQueryResult } from '@tanstack/react-query'; import CodeMirror, { placeholder } from '@uiw/react-codemirror'; import { ConnectionForm } from '@/components/ConnectionForm'; @@ -41,6 +40,7 @@ import { TableSourceForm } from '@/components/SourceForm'; import { IS_LOCAL_MODE } from '@/config'; import { PageHeader } from './components/PageHeader'; +import { useAuthEmails } from './hooks/useAuthEmails'; import api from './api'; import { useConnections } from './connection'; import { DEFAULT_SEARCH_ROW_LIMIT } from './defaults'; @@ -295,7 +295,7 @@ function SourcesSection() { function TeamMembersSection() { const hasAdminAccess = true; - const { data: me, isLoading: isLoadingMe } = api.useMe(); + const { data: me } = api.useMe(); const { data: team } = api.useTeam(); const { data: members, @@ -331,13 +331,14 @@ function TeamMembersSection() { const saveTeamInvitation = api.useSaveTeamInvitation(); const deleteTeamMember = api.useDeleteTeamMember(); const deleteTeamInvitation = api.useDeleteTeamInvitation(); + const { hasAccess } = useAuthEmails(); const sendTeamInviteAction = (email: string) => { if (email) { saveTeamInvitation.mutate( { email }, { - onSuccess: resp => { + onSuccess: () => { notifications.show({ color: 'green', message: @@ -345,11 +346,11 @@ function TeamMembersSection() { }); refetchInvitations(); }, - onError: e => { + onError: (e: any) => { if (e instanceof HTTPError) { e.response .json() - .then(res => { + .then((res: any) => { notifications.show({ color: 'red', message: res.message, @@ -396,18 +397,18 @@ function TeamMembersSection() { deleteTeamInvitation.mutate( { id: encodeURIComponent(id) }, { - onSuccess: resp => { + onSuccess: () => { notifications.show({ color: 'green', message: 'Deleted team invite', }); refetchInvitations(); }, - onError: e => { + onError: (e: any) => { if (e instanceof HTTPError) { e.response .json() - .then(res => { + .then((res: any) => { notifications.show({ color: 'red', message: res.message, @@ -440,18 +441,18 @@ function TeamMembersSection() { deleteTeamMember.mutate( { userId: encodeURIComponent(id) }, { - onSuccess: resp => { + onSuccess: () => { notifications.show({ color: 'green', message: 'Deleted team member', }); refetchMembers(); }, - onError: e => { + onError: (e: any) => { if (e instanceof HTTPError) { e.response .json() - .then(res => { + .then((res: any) => { notifications.show({ color: 'red', message: res.message, @@ -549,24 +550,26 @@ function TeamMembersSection() { )} - {!member.isCurrentUser && hasAdminAccess && ( - - - - )} + {!member.isCurrentUser && + hasAdminAccess && + hasAccess(me?.email) && ( + + + + )} ))} @@ -962,8 +965,10 @@ function IntegrationsSection() { } function TeamNameSection() { - const { data: team, isLoading, refetch: refetchTeam } = api.useTeam(); + const { data: team, refetch: refetchTeam } = api.useTeam(); const setTeamName = api.useSetTeamName(); + const { data: me } = api.useMe(); + const { hasAccess } = useAuthEmails(); const hasAdminAccess = true; const [isEditingTeamName, setIsEditingTeamName] = useState(false); const form = useForm<{ name: string }>({ @@ -977,7 +982,7 @@ function TeamNameSection() { setTeamName.mutate( { name: values.name }, { - onError: e => { + onError: () => { notifications.show({ color: 'red', message: 'Failed to update team name', @@ -994,7 +999,7 @@ function TeamNameSection() { }, ); }, - [refetchTeam, setTeamName, team?.name], + [refetchTeam, setTeamName], ); return ( @@ -1045,7 +1050,7 @@ function TeamNameSection() { ) : (
{team.name}
- {hasAdminAccess && ( + {hasAdminAccess && hasAccess(me?.email) && (