This commit is contained in:
Emily
2024-09-27 20:33:49 +02:00
parent 8e3ad2920f
commit 3c77a727cd
37 changed files with 815 additions and 491752 deletions

View File

@@ -76,7 +76,6 @@ async function confirmSnapshot() {
</div>
<div class="mt-4 justify-center flex w-full">
<UPopover class="w-full" :popper="{ placement: 'bottom' }">
<UButton class="w-full" color="primary" variant="solid">
<div class="flex items-center justify-center w-full gap-2">
@@ -97,8 +96,6 @@ async function confirmSnapshot() {
</div>
</template>
</UPopover>
</div>
<div class="grow"></div>

View File

@@ -0,0 +1,157 @@
<script lang="ts" setup>
import { sub, isSameDay, type Duration } from 'date-fns'
type ChartType = 'bar' | 'line';
const chartTypeOptions: { value: ChartType, label: string }[] = [
{ value: 'bar', label: 'Bar chart' },
{ value: 'line', label: 'Line chart' },
]
type yAxisMode = 'count';
const yAxisModeOptions: { value: yAxisMode, label: string }[] = [
{ value: 'count', label: 'Count fields' },
]
type Slice = 'day' | 'month';
const sliceOptions: Slice[] = ['day', 'month'];
const chartType = ref<ChartType>('line');
const tableName = ref<string>('');
const xAxis = ref<string>('');
const yAxisMode = ref<yAxisMode>('count');
const slice = ref<Slice>('day');
const visualizationName = ref<string>('');
const ranges = [
{ label: 'Last 7 days', duration: { days: 7 } },
{ label: 'Last 14 days', duration: { days: 14 } },
{ label: 'Last 30 days', duration: { days: 30 } },
{ label: 'Last 3 months', duration: { months: 3 } },
{ label: 'Last 6 months', duration: { months: 6 } },
{ label: 'Last year', duration: { years: 1 } }
]
const timeframe = ref<{ start: Date, end: Date }>({ start: sub(new Date(), { days: 14 }), end: new Date() })
function isRangeSelected(duration: Duration) {
return isSameDay(timeframe.value.start, sub(new Date(), duration)) && isSameDay(timeframe.value.end, new Date())
}
function selectRange(duration: Duration) {
timeframe.value = { start: sub(new Date(), duration), end: new Date() }
}
const { createAlert } = useAlert();
const { closeDialog } = useCustomDialog();
const activeProjectId = useActiveProjectId();
const { integrationsCredentials,testConnection } = useSupabase();
async function generate() {
const credentials = integrationsCredentials.data.value;
if (!credentials?.supabase) return createAlert('Credentials not found', 'Please add supabase credentials on the integration page', 'far fa-error', 5000);
const connectionStatus = await testConnection();
if (!connectionStatus) return createAlert('Invalid supabase credentials', 'Please check your supabase credentials on the integration page', 'far fa-error', 5000);
try {
const creation = await $fetch('/api/integrations/supabase/add', {
...signHeaders({
'x-pid': activeProjectId.data.value || '',
'Content-Type': 'application/json'
}),
method: 'POST',
body: JSON.stringify({
name: visualizationName.value,
chart_type: chartType.value,
table_name: tableName.value,
xField: xAxis.value,
yMode: yAxisMode.value,
from: timeframe.value.start,
to: timeframe.value.end,
slice: slice.value
})
})
createAlert('Integration generated', 'Integration generated successfully', 'far fa-check-circle', 5000);
closeDialog();
} catch (ex: any) {
createAlert('Error generating integrations', ex.response._data.message.toString(), 'far fa-error', 5000);
}
}
</script>
<template>
<div class="flex flex-col gap-4">
<div>
<div> Visualization name </div>
<div>
<LyxUiInput class="w-full px-2 py-1" v-model="visualizationName"></LyxUiInput>
</div>
</div>
<div>
<div> Chart type </div>
<USelect v-model="chartType" :options="chartTypeOptions" />
</div>
<div>
<div> Table name </div>
<div>
<LyxUiInput class="w-full px-2 py-1" v-model="tableName"></LyxUiInput>
</div>
</div>
<div>
<div> X axis field </div>
<div>
<LyxUiInput class="w-full px-2 py-1" v-model="xAxis"></LyxUiInput>
</div>
</div>
<div>
<div> Y axis mode </div>
<div>
<USelect v-model="yAxisMode" :options="yAxisModeOptions" />
</div>
</div>
<div>
<div> Timeframe </div>
<div>
<UPopover class="w-full" :popper="{ placement: 'bottom' }">
<UButton class="w-full" color="primary" variant="solid">
<div class="flex items-center justify-center w-full gap-2">
<i class="i-heroicons-calendar-days-20-solid"></i>
{{ timeframe.start.toLocaleDateString() }} - {{ timeframe.end.toLocaleDateString() }}
</div>
</UButton>
<template #panel="{ close }">
<div class="flex items-center sm:divide-x divide-gray-200 dark:divide-gray-800">
<div class="hidden sm:flex flex-col py-4">
<UButton v-for="(range, index) in ranges" :key="index" :label="range.label" color="gray"
variant="ghost" class="rounded-none px-6"
:class="[isRangeSelected(range.duration) ? 'bg-gray-100 dark:bg-gray-800' : 'hover:bg-gray-50 dark:hover:bg-gray-800/50']"
truncate @click="selectRange(range.duration)" />
</div>
<DatePicker v-model="timeframe" @close="close" />
</div>
</template>
</UPopover>
</div>
</div>
<div>
<div> View mode </div>
<div>
<USelect v-model="slice" :options="sliceOptions" />
</div>
</div>
<LyxUiButton type="primary" @click="generate()">
Generate
</LyxUiButton>
</div>
</template>

View File

@@ -0,0 +1,170 @@
<script setup lang="ts">
import type { TSupabaseIntegration } from '@schema/integrations/SupabaseIntegrationSchema';
import type { ChartData, ChartOptions } from 'chart.js';
import { useLineChart, LineChart } from 'vue-chart-3';
const props = defineProps<{ integration_id: string }>();
const activeProjectId = useActiveProjectId();
const supabaseData = ref<{ labels: string[], data: number[] }>();
const supabaseError = ref<string | undefined>(undefined);
const supabaseFetching = ref<boolean>(false);
const { getRemoteData } = useSupabase();
function createGradient() {
const c = document.createElement('canvas');
const ctx = c.getContext("2d");
let gradient: any = `#34B67C22`;
if (ctx) {
gradient = ctx.createLinearGradient(0, 25, 0, 300);
gradient.addColorStop(0, `#34B67C99`);
gradient.addColorStop(0.35, `#34B67C66`);
gradient.addColorStop(1, `#34B67C22`);
} else {
console.warn('Cannot get context for gradient');
}
chartData.value.datasets[0].backgroundColor = [gradient];
}
const chartOptions = ref<ChartOptions<'line'>>({
responsive: true,
maintainAspectRatio: false,
interaction: {
intersect: false,
mode: 'nearest',
axis: 'x',
includeInvisible: true
},
scales: {
y: {
ticks: { display: true },
grid: {
display: true,
drawBorder: false,
color: '#CCCCCC22',
// borderDash: [5, 10]
},
},
x: {
ticks: { display: true },
grid: {
display: true,
drawBorder: false,
color: '#CCCCCC22',
}
}
},
plugins: {
legend: { display: false },
title: { display: false },
tooltip: {
enabled: true,
backgroundColor: 'rgba(0, 0, 0, 0.8)',
titleFont: { size: 16, weight: 'bold' },
bodyFont: { size: 14 },
padding: 10,
cornerRadius: 4,
boxPadding: 10,
caretPadding: 20,
yAlign: 'bottom',
xAlign: 'center',
}
},
});
const chartData = ref<ChartData<'line'>>({
labels: [],
datasets: [
{
data: [],
backgroundColor: ['#34B67C' + '77'],
borderColor: '#34B67C',
borderWidth: 4,
fill: true,
tension: 0.45,
pointRadius: 0,
pointHoverRadius: 10,
hoverBackgroundColor: '#34B67C',
hoverBorderColor: 'white',
hoverBorderWidth: 2,
},
],
});
onMounted(async () => {
supabaseFetching.value = true;
supabaseError.value = undefined;
const integrationData = await $fetch<TSupabaseIntegration>('/api/integrations/supabase/get', {
...signHeaders({
'x-pid': activeProjectId.data.value || '',
'x-integration': props.integration_id
})
});
if (!integrationData) {
supabaseError.value = 'Cannot get integration data';
supabaseFetching.value = false;
return;
}
try {
const data = await getRemoteData(
integrationData.table_name,
integrationData.xField,
integrationData.yMode,
integrationData.from.toString(),
integrationData.to.toString(),
integrationData.slice,
);
if (data.error) {
supabaseError.value = data.error;
supabaseFetching.value = false;
return;
}
supabaseFetching.value = false;
supabaseData.value = data.result;
chartData.value.labels = data.result?.labels || [];
chartData.value.datasets[0].data = data.result?.data || [];
console.log(data.result);
createGradient();
} catch (ex: any) {
if (!ex.response._data) {
supabaseError.value = ex.message.toString();
supabaseFetching.value = false;
} else {
supabaseError.value = ex.response._data.message.toString();
supabaseFetching.value = false;
}
}
});
const { lineChartProps, lineChartRef } = useLineChart({ chartData: chartData, options: chartOptions });
</script>
<template>
<div v-if="!supabaseFetching">
<div v-if="!supabaseError">
<LineChart ref="lineChartRef" v-bind="lineChartProps"> </LineChart>
</div>
<div v-if="supabaseError"> {{ supabaseError }} </div>
</div>
<div v-if="supabaseFetching">
Getting remote data...
</div>
</template>

View File

@@ -4,6 +4,7 @@
const ACCESS_TOKEN_STATE_KEY = 'access_token';
const ACCESS_TOKEN_COOKIE_KEY = 'access_token';
export function signHeaders(headers?: Record<string, string>) {
const { token } = useAccessToken()
return { headers: { ...(headers || {}), 'Authorization': 'Bearer ' + token.value } }

View File

@@ -0,0 +1,125 @@
import type { TSupabaseIntegration } from "@schema/integrations/SupabaseIntegrationSchema";
import { createClient, SupabaseClient } from "@supabase/supabase-js";
import { format } from 'date-fns';
const activeProjectId = useActiveProjectId();
const computedHeaders = computed<Record<string, string>>(() => {
const signedHeaders = signHeaders();
return {
'x-pid': activeProjectId.data.value || '',
'Authorization': signedHeaders.headers.Authorization
}
})
const integrationsCredentials = useFetch(`/api/integrations/credentials/get`, {
headers: computedHeaders,
onResponse: (e) => {
supabaseUrl.value = e.response._data.supabase.url || '';
supabaseAnonKey.value = e.response._data.supabase.anon_key || '';
supabaseServiceRoleKey.value = e.response._data.supabase.service_role_key || '';
}
});
const supabaseUrl = ref<string>('');
const supabaseAnonKey = ref<string>('');
const supabaseServiceRoleKey = ref<string>('');
const supabaseIntegrations = useFetch<TSupabaseIntegration[]>('/api/integrations/supabase/list', { headers: computedHeaders })
const subabaseClientData: { client: SupabaseClient | undefined } = {
client: undefined
}
async function updateIntegrationsCredentails(data: { supabase_url: string, supabase_anon_key: string, supabase_service_role_key: string }) {
try {
await $fetch(`/api/integrations/credentials/${activeProjectId.data.value}/update`, {
...signHeaders({ 'Content-Type': 'application/json' }),
method: 'POST',
body: JSON.stringify({
supabase_url: data.supabase_url,
supabase_anon_key: data.supabase_anon_key,
supabase_service_role_key: data.supabase_service_role_key
}),
});
integrationsCredentials.refresh();
return { ok: true, error: '' }
} catch (ex: any) {
return { ok: false, error: ex.message.toString() };
}
}
function createSupabaseUrl(supabaseUrl: string) {
let result = supabaseUrl;
if (!result.includes('https://')) result = `https://${result}`;
if (!result.endsWith('.supabase.co')) result = `${result}.supabase.co`;
return result;
}
async function testConnection() {
const url = createSupabaseUrl(supabaseUrl.value);
subabaseClientData.client = createClient(url, supabaseAnonKey.value);
const res = await subabaseClientData.client.from('_t_e_s_t_').select('*').limit(1);
if (res.error?.message.startsWith('TypeError')) return false;
return true;
}
type GroupBy = 'day' | 'month';
const groupByDate = (data: string[], groupBy: GroupBy) => {
return data.reduce((acc, item) => {
const date = new Date(item);
const dateKey = groupBy === 'day'
? format(date, 'yyyy-MM-dd') // Group by day
: format(date, 'yyyy-MM'); // Group by month
if (!acc[dateKey]) { acc[dateKey] = []; }
acc[dateKey].push(item);
return acc;
}, {} as Record<string, string[]>);
}
async function getRemoteData(table: string, xField: string, yMode: string, from: string, to: string, slice: string) {
const url = createSupabaseUrl(supabaseUrl.value);
subabaseClientData.client = createClient(url, supabaseAnonKey.value);
const res = await subabaseClientData.client.from(table).select(xField)
.filter(xField, 'gte', from)
.filter(xField, 'lte', to);
if (res.error) return { error: res.error.message };
const grouped = groupByDate(res.data.map((e: any) => e.created_at) || [], slice as any);
const result: { labels: string[], data: number[] } = { labels: [], data: [] }
for (const key in grouped) {
result.labels.push(key);
result.data.push(grouped[key].length);
}
return { result };
}
export function useSupabase() {
return {
getRemoteData,
testConnection,
supabaseIntegrations, integrationsCredentials,
supabaseUrl, supabaseAnonKey,
supabaseServiceRoleKey,
updateIntegrationsCredentails
}
}

View File

@@ -21,7 +21,7 @@ const sections: Section[] = [
{ label: 'Security', to: '/security', icon: 'fal fa-shield' },
{ label: 'Insights (soon)', to: '#', icon: 'fal fa-lightbulb', disabled: true },
{ label: 'Links (soon)', to: '#', icon: 'fal fa-globe-pointer', disabled: true },
{ label: 'Integrations (soon)', to: '#', icon: 'fal fa-cube', disabled: true },
{ label: 'Integrations (soon)', to: '/integrations', icon: 'fal fa-cube', disabled: true },
{ label: 'Settings', to: '/settings', icon: 'fal fa-gear' },
{
grow: true,

View File

@@ -15,6 +15,7 @@
"dependencies": {
"@getbrevo/brevo": "^2.2.0",
"@nuxtjs/tailwindcss": "^6.12.0",
"@supabase/supabase-js": "^2.45.4",
"chart.js": "^3.9.1",
"chartjs-chart-funnel": "^4.2.1",
"chartjs-plugin-annotation": "^2.2.1",

View File

@@ -0,0 +1,105 @@
<script setup lang="ts">
import SupabaseChartDialog from '~/components/integrations/SupabaseChartDialog.vue';
definePageMeta({ layout: 'dashboard' });
const activeProjectId = useActiveProjectId();
const { createAlert } = useAlert();
const {
supabaseUrl, supabaseAnonKey, supabaseServiceRoleKey, integrationsCredentials,
supabaseIntegrations, updateIntegrationsCredentails
} = useSupabase()
async function updateCredentials() {
const res = await updateIntegrationsCredentails({
supabase_url: supabaseUrl.value,
supabase_anon_key: supabaseAnonKey.value,
supabase_service_role_key: supabaseServiceRoleKey.value
});
if (res.ok === true) {
integrationsCredentials.refresh();
createAlert('Credentials updated', 'Credentials updated successfully', 'far fa-error', 4000);
} else {
createAlert('Error updating credentials', res.error, 'far fa-error', 4000);
}
}
const { openDialogEx } = useCustomDialog()
function showChartDialog() {
openDialogEx(SupabaseChartDialog, {
closable: true,
width: '55vw',
height: '65vh'
})
}
</script>
<template>
<div class="home w-full h-full px-10 pt-6 overflow-y-auto">
<CardTitled title="Supabase integration" class="w-full">
<template #header>
<img class="h-10 w-10" :src="'supabase.svg'" alt="Supabase logo">
</template>
<div class="flex gap-6 flex-col w-full">
<div class="flex flex-col">
<div class="text-lyx-text"> Supabase url </div>
<div class="text-lyx-text-dark"> Required to fetch data from supabase </div>
<LyxUiInput v-if="!integrationsCredentials.pending.value" class="w-full mt-2 px-4 py-1"
v-model="supabaseUrl" type="text"></LyxUiInput>
<div v-if="integrationsCredentials.pending.value"> Loading... </div>
</div>
<div class="flex flex-col">
<div class="text-lyx-text"> Supabase anon key </div>
<div class="text-lyx-text-dark"> Required to fetch data from supabase </div>
<LyxUiInput v-if="!integrationsCredentials.pending.value" class="w-full mt-2 px-4 py-1"
v-model="supabaseAnonKey" type="password"></LyxUiInput>
<div v-if="integrationsCredentials.pending.value"> Loading... </div>
</div>
<div class="flex flex-col">
<div class="text-lyx-text"> Supabase service role key </div>
<div class="text-lyx-text-dark"> Only used if you need to bypass RLS </div>
<LyxUiInput v-if="!integrationsCredentials.pending.value" class="w-full mt-2 px-4 py-1"
v-model="supabaseServiceRoleKey" type="password"></LyxUiInput>
<div v-if="integrationsCredentials.pending.value"> Loading... </div>
</div>
<div class="flex gap-3">
<LyxUiButton v-if="!integrationsCredentials.pending.value" @click="updateCredentials()"
type="primary"> Save
</LyxUiButton>
</div>
</div>
</CardTitled>
<LyxUiCard class="mt-6 w-full">
<div class="flex flex-col gap-8">
<div class="flex gap-2 items-center" v-for="supabaseIntegration of supabaseIntegrations.data.value">
<div> {{ supabaseIntegration.name }} </div>
<div> <i class="far fa-edit"></i> </div>
<div> <i class="far fa-trash"></i> </div>
</div>
<div>
<LyxUiButton type="primary" @click="showChartDialog()"> Add supabase chart </LyxUiButton>
</div>
</div>
</LyxUiCard>
<div class="mt-10">
<IntegrationsSupabaseLineChart integration_id="66f6c558d97e4abd408feee0"></IntegrationsSupabaseLineChart>
</div>
</div>
</template>

View File

@@ -14,6 +14,9 @@ importers:
'@nuxtjs/tailwindcss':
specifier: ^6.12.0
version: 6.12.0(rollup@4.18.0)
'@supabase/supabase-js':
specifier: ^2.45.4
version: 2.45.4
chart.js:
specifier: ^3.9.1
version: 3.9.1
@@ -1138,6 +1141,28 @@ packages:
resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==}
engines: {node: '>=18'}
'@supabase/auth-js@2.65.0':
resolution: {integrity: sha512-+wboHfZufAE2Y612OsKeVP4rVOeGZzzMLD/Ac3HrTQkkY4qXNjI6Af9gtmxwccE5nFvTiF114FEbIQ1hRq5uUw==}
'@supabase/functions-js@2.4.1':
resolution: {integrity: sha512-8sZ2ibwHlf+WkHDUZJUXqqmPvWQ3UHN0W30behOJngVh/qHHekhJLCFbh0AjkE9/FqqXtf9eoVvmYgfCLk5tNA==}
'@supabase/node-fetch@2.6.15':
resolution: {integrity: sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==}
engines: {node: 4.x || >=6.0.0}
'@supabase/postgrest-js@1.16.1':
resolution: {integrity: sha512-EOSEZFm5pPuCPGCmLF1VOCS78DfkSz600PBuvBND/IZmMciJ1pmsS3ss6TkB6UkuvTybYiBh7gKOYyxoEO3USA==}
'@supabase/realtime-js@2.10.2':
resolution: {integrity: sha512-qyCQaNg90HmJstsvr2aJNxK2zgoKh9ZZA8oqb7UT2LCh3mj9zpa3Iwu167AuyNxsxrUE8eEJ2yH6wLCij4EApA==}
'@supabase/storage-js@2.7.0':
resolution: {integrity: sha512-iZenEdO6Mx9iTR6T7wC7sk6KKsoDPLq8rdu5VRy7+JiT1i8fnqfcOr6mfF2Eaqky9VQzhP8zZKQYjzozB65Rig==}
'@supabase/supabase-js@2.45.4':
resolution: {integrity: sha512-E5p8/zOLaQ3a462MZnmnz03CrduA5ySH9hZyL03Y+QZLIOO4/Gs8Rdy4ZCKDHsN7x0xdanVEWWFN3pJFQr9/hg==}
'@swc/helpers@0.3.17':
resolution: {integrity: sha512-tb7Iu+oZ+zWJZ3HJqwx8oNwSDIU440hmVMDPhpACWQWnrZHK99Bxs70gT1L2dnr5Hg50ZRWEFkQCAnOVVV0z1Q==}
@@ -1223,6 +1248,9 @@ packages:
'@types/pdfkit@0.13.4':
resolution: {integrity: sha512-ixGNDHYJCCKuamY305wbfYSphZ2WPe8FPkjn8oF4fHV+PgPV4V+hecPh2VOS2h4RNtpSB3zQcR4sCpNvvrEb1A==}
'@types/phoenix@1.6.5':
resolution: {integrity: sha512-xegpDuR+z0UqG9fwHqNoy3rI7JDlvaPh2TY47Fl80oq6g+hXT+c/LEuE43X48clZ6lOfANl5WrPur9fYO1RJ/w==}
'@types/qs@6.9.16':
resolution: {integrity: sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==}
@@ -1244,6 +1272,9 @@ packages:
'@types/whatwg-url@11.0.5':
resolution: {integrity: sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==}
'@types/ws@8.5.12':
resolution: {integrity: sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==}
'@ungap/structured-clone@1.2.0':
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
@@ -1491,9 +1522,6 @@ packages:
'@vue/shared@3.4.27':
resolution: {integrity: sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==}
'@vue/shared@3.5.7':
resolution: {integrity: sha512-NBE1PBIvzIedxIc2RZiKXvGbJkrZ2/hLf3h8GlS4/sP9xcXEZMFWOazFkNd6aGeUCMaproe5MHVYB3/4AW9q9g==}
'@vue/shared@3.5.8':
resolution: {integrity: sha512-mJleSWbAGySd2RJdX1RBtcrUBX6snyOc0qHpgk3lGi4l9/P/3ny3ELqFWqYdkXIwwNN/kdm8nD9ky8o6l/Lx2A==}
@@ -6632,6 +6660,48 @@ snapshots:
'@sindresorhus/merge-streams@2.3.0': {}
'@supabase/auth-js@2.65.0':
dependencies:
'@supabase/node-fetch': 2.6.15
'@supabase/functions-js@2.4.1':
dependencies:
'@supabase/node-fetch': 2.6.15
'@supabase/node-fetch@2.6.15':
dependencies:
whatwg-url: 5.0.0
'@supabase/postgrest-js@1.16.1':
dependencies:
'@supabase/node-fetch': 2.6.15
'@supabase/realtime-js@2.10.2':
dependencies:
'@supabase/node-fetch': 2.6.15
'@types/phoenix': 1.6.5
'@types/ws': 8.5.12
ws: 8.17.0
transitivePeerDependencies:
- bufferutil
- utf-8-validate
'@supabase/storage-js@2.7.0':
dependencies:
'@supabase/node-fetch': 2.6.15
'@supabase/supabase-js@2.45.4':
dependencies:
'@supabase/auth-js': 2.65.0
'@supabase/functions-js': 2.4.1
'@supabase/node-fetch': 2.6.15
'@supabase/postgrest-js': 1.16.1
'@supabase/realtime-js': 2.10.2
'@supabase/storage-js': 2.7.0
transitivePeerDependencies:
- bufferutil
- utf-8-validate
'@swc/helpers@0.3.17':
dependencies:
tslib: 2.6.2
@@ -6719,6 +6789,8 @@ snapshots:
dependencies:
'@types/node': 20.12.12
'@types/phoenix@1.6.5': {}
'@types/qs@6.9.16': {}
'@types/resize-observer-browser@0.1.11': {}
@@ -6735,6 +6807,10 @@ snapshots:
dependencies:
'@types/webidl-conversions': 7.0.3
'@types/ws@8.5.12':
dependencies:
'@types/node': 20.12.12
'@ungap/structured-clone@1.2.0': {}
'@unhead/dom@1.9.11':
@@ -7152,7 +7228,7 @@ snapshots:
'@volar/language-core': 1.11.1
'@volar/source-map': 1.11.1
'@vue/compiler-dom': 3.4.27
'@vue/shared': 3.5.7
'@vue/shared': 3.5.8
computeds: 0.0.1
minimatch: 9.0.4
muggle-string: 0.3.1
@@ -7200,8 +7276,6 @@ snapshots:
'@vue/shared@3.4.27': {}
'@vue/shared@3.5.7': {}
'@vue/shared@3.5.8': {}
'@vueuse/components@10.10.0(vue@3.4.27(typescript@5.4.2))':

View File

@@ -0,0 +1,15 @@
<svg width="98" height="100" viewBox="0 0 98 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M56.8944 98.338C54.3397 101.555 49.1599 99.7924 49.0983 95.6846L48.1982 35.6025H88.5973C95.9147 35.6025 99.9957 44.0542 95.4457 49.7849L56.8944 98.338Z" fill="url(#paint0_linear_99_24683)"/>
<path d="M56.8944 98.338C54.3397 101.555 49.1599 99.7924 49.0983 95.6846L48.1982 35.6025H88.5973C95.9147 35.6025 99.9957 44.0542 95.4457 49.7849L56.8944 98.338Z" fill="url(#paint1_linear_99_24683)" fill-opacity="0.2"/>
<path d="M40.464 1.66109C43.0187 -1.55638 48.1986 0.206562 48.2601 4.31445L48.6546 64.3964H8.76106C1.44348 64.3964 -2.63767 55.9448 1.91262 50.214L40.464 1.66109Z" fill="#3ECF8E"/>
<defs>
<linearGradient id="paint0_linear_99_24683" x1="48.1982" y1="48.9242" x2="84.1036" y2="63.9829" gradientUnits="userSpaceOnUse">
<stop stop-color="#249361"/>
<stop offset="1" stop-color="#3ECF8E"/>
</linearGradient>
<linearGradient id="paint1_linear_99_24683" x1="32.2797" y1="27.1289" x2="48.6544" y2="57.9534" gradientUnits="userSpaceOnUse">
<stop/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,23 @@
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
import { IntegrationsCredentialsModel } from '@schema/integrations/IntegrationsCredentialsSchema';
export default defineEventHandler(async event => {
const project_id = getHeader(event, 'x-pid');
if (!project_id) return;
const user = getRequestUser(event);
const project = await getUserProjectFromId(project_id, user);
if (!project) return;
const credentials = await IntegrationsCredentialsModel.findOne({ project_id });
return {
supabase: {
anon_key: credentials?.supabase_anon_key || '',
service_role_key: credentials?.supabase_service_role_key || '',
url: credentials?.supabase_url || ''
}
}
});

View File

@@ -0,0 +1,23 @@
import { IntegrationsCredentialsModel } from "@schema/integrations/IntegrationsCredentialsSchema";
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
export default defineEventHandler(async event => {
const project_id = getHeader(event, 'x-pid');
if (!project_id) return;
const user = getRequestUser(event);
const project = await getUserProjectFromId(project_id, user);
if (!project) return;
const body = await readBody(event);
const res = await IntegrationsCredentialsModel.updateOne({ project_id }, {
supabase_anon_key: body.supabase_anon_key || '',
supabase_service_role_key: body.supabase_service_role_key || '',
supabase_url: body.supabase_url || '',
}, { upsert: true });
return { ok: res.acknowledged };
});

View File

@@ -0,0 +1,29 @@
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
import { SupabaseIntegrationModel } from "@schema/integrations/SupabaseIntegrationSchema";
export default defineEventHandler(async event => {
const project_id = getHeader(event, 'x-pid');
if (!project_id) return;
const user = getRequestUser(event);
const project = await getUserProjectFromId(project_id, user);
if (!project) return;
const { chart_type, table_name, xField, yMode, from, to, slice, name } = await readBody(event);
if (!project.premium) {
const supabaseIntegrationsCount = await SupabaseIntegrationModel.countDocuments({ project_id });
if (supabaseIntegrationsCount > 0) return setResponseStatus(event, 400, 'LIMIT_REACHED');
}
await SupabaseIntegrationModel.create({
name,
project_id, chart_type,
table_name, xField, yMode,
from, to, slice,
});
return { ok: true };
});

View File

@@ -0,0 +1,18 @@
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
import { SupabaseIntegrationModel } from '@schema/integrations/SupabaseIntegrationSchema';
export default defineEventHandler(async event => {
const project_id = getHeader(event, 'x-pid');
if (!project_id) return;
const user = getRequestUser(event);
const project = await getUserProjectFromId(project_id, user);
if (!project) return;
const integration_id = getHeader(event, 'x-integration');
const integration = await SupabaseIntegrationModel.findOne({ _id: integration_id });
return integration;
});

View File

@@ -0,0 +1,16 @@
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
import { SupabaseIntegrationModel } from '@schema/integrations/SupabaseIntegrationSchema';
export default defineEventHandler(async event => {
const project_id = getHeader(event, 'x-pid');
if (!project_id) return;
const user = getRequestUser(event);
const project = await getUserProjectFromId(project_id, user);
if (!project) return;
const integrations = await SupabaseIntegrationModel.find({ project_id });
return integrations;
});