diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 8c86acbd39..5d34d63107 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -35,7 +35,6 @@ dependencies = [ "celery[redis]==5.6.2", "django-redis==6.0.0", "django-celery-email-reboot==4.2.0", - "advocate==1.0.0", "zipp==3.23.0", "unicodecsv==0.14.1", "django-celery-beat==2.8.1", @@ -103,6 +102,8 @@ dependencies = [ "pyotp==2.9.0", "qrcode==8.2", "udspy==0.1.8", + "netifaces==0.11.0", + "requests-futures>=1.0.2", ] [project.urls] @@ -142,7 +143,6 @@ dev = [ "pytest-unordered==0.7.0", "debugpy==1.8.20", "backports.cached-property==1.0.2", - "httpretty==1.1.4", "graphviz==0.21", "pytest-cov==7.0.0", "django-stubs==5.2.8", diff --git a/backend/pytest.ini b/backend/pytest.ini index a5d3c7ecf2..d15be43dcd 100644 --- a/backend/pytest.ini +++ b/backend/pytest.ini @@ -12,6 +12,7 @@ testpaths = tests ../premium/backend/tests ../enterprise/backend/tests + src/advocate/test_advocate.py asyncio_default_fixture_loop_scope = function markers = field_text: All tests related to text field diff --git a/backend/src/advocate/LICENSE b/backend/src/advocate/LICENSE new file mode 100644 index 0000000000..05b6a1f1a6 --- /dev/null +++ b/backend/src/advocate/LICENSE @@ -0,0 +1,13 @@ +Copyright 2015 Jordan Milne + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/backend/src/advocate/__init__.py b/backend/src/advocate/__init__.py new file mode 100644 index 0000000000..5b7e8322e5 --- /dev/null +++ b/backend/src/advocate/__init__.py @@ -0,0 +1,18 @@ +__version__ = "1.0.0" + +from requests import utils # noqa: F401 +from requests.exceptions import ( + ConnectionError, # noqa: F401 + HTTPError, # noqa: F401 + RequestException, # noqa: F401 + Timeout, # noqa: F401 + TooManyRedirects, # noqa: F401 + URLRequired, # noqa: F401 +) +from requests.models import PreparedRequest, Request, Response # noqa: F401 +from requests.status_codes import codes # noqa: F401 + +from .adapters import ValidatingHTTPAdapter # noqa: F401 +from .addrvalidator import AddrValidator # noqa: F401 +from .api import * # noqa: F403 +from .exceptions import UnacceptableAddressException # noqa: F401 diff --git a/backend/src/advocate/adapters.py b/backend/src/advocate/adapters.py new file mode 100644 index 0000000000..f0c533b1fb --- /dev/null +++ b/backend/src/advocate/adapters.py @@ -0,0 +1,32 @@ +from requests.adapters import DEFAULT_POOLBLOCK, HTTPAdapter + +from .addrvalidator import AddrValidator +from .exceptions import ProxyDisabledException +from .poolmanager import ValidatingPoolManager + + +class ValidatingHTTPAdapter(HTTPAdapter): + __attrs__ = HTTPAdapter.__attrs__ + ["_validator"] + + def __init__(self, *args, **kwargs): + self._validator = kwargs.pop("validator", None) + if not self._validator: + self._validator = AddrValidator() + super().__init__(*args, **kwargs) + + def init_poolmanager( + self, connections, maxsize, block=DEFAULT_POOLBLOCK, **pool_kwargs + ): + self._pool_connections = connections + self._pool_maxsize = maxsize + self._pool_block = block + self.poolmanager = ValidatingPoolManager( + num_pools=connections, + maxsize=maxsize, + block=block, + validator=self._validator, + **pool_kwargs, + ) + + def proxy_manager_for(self, proxy, **proxy_kwargs): + raise ProxyDisabledException("Proxies cannot be used with Advocate") diff --git a/backend/src/advocate/addrvalidator.py b/backend/src/advocate/addrvalidator.py new file mode 100644 index 0000000000..25897f2f5f --- /dev/null +++ b/backend/src/advocate/addrvalidator.py @@ -0,0 +1,269 @@ +import fnmatch +import functools +import ipaddress +import re + +try: + import netifaces + + HAVE_NETIFACES = True +except ImportError: + netifaces = None + HAVE_NETIFACES = False + +from .exceptions import ConfigException, NameserverException + + +def canonicalize_hostname(hostname): + """Lowercase and punycodify a hostname""" + # We do the lowercasing after IDNA encoding because we only want to + # lowercase the *ASCII* chars. + # TODO: The differences between IDNA2003 and IDNA2008 might be relevant + # to us, but both specs are damn confusing. + return str(hostname.encode("idna").lower(), "utf-8") + + +def determine_local_addresses(): + """Get all IPs that refer to this machine according to netifaces""" + if not HAVE_NETIFACES: + raise ConfigException( + "Tried to determine local addresses, " + "but netifaces module was not importable" + ) + ips = [] + for interface in netifaces.interfaces(): + if_families = netifaces.ifaddresses(interface) + for family_kind in {netifaces.AF_INET, netifaces.AF_INET6}: + addrs = if_families.get(family_kind, []) + for addr in (x.get("addr", "") for x in addrs): + if family_kind == netifaces.AF_INET6: + # We can't do anything sensible with the scope here + addr = addr.split("%")[0] + ips.append(ipaddress.ip_network(addr)) + return ips + + +def add_local_address_arg(func): + """Add the "_local_addresses" kwarg if it's missing + + IMO this information shouldn't be cached between calls (what if one of the + adapters got a new IP at runtime?,) and we don't want each function to + recalculate it. Just recalculate it if the caller didn't provide it for us. + """ + + @functools.wraps(func) + def wrapper(self, *args, **kwargs): + if "_local_addresses" not in kwargs: + if self.autodetect_local_addresses: + kwargs["_local_addresses"] = determine_local_addresses() + else: + kwargs["_local_addresses"] = [] + return func(self, *args, **kwargs) + + return wrapper + + +class AddrValidator: + _6TO4_RELAY_NET = ipaddress.ip_network("192.88.99.0/24") + # Just the well known prefix, DNS64 servers can set their own + # prefix, but in practice most probably don't. + _DNS64_WK_PREFIX = ipaddress.ip_network("64:ff9b::/96") + DEFAULT_PORT_WHITELIST = {80, 8080, 443, 8443, 8000} + + def __init__( + self, + ip_blacklist=None, + ip_whitelist=None, + port_whitelist=None, + port_blacklist=None, + hostname_blacklist=None, + allow_ipv6=False, + allow_teredo=False, + allow_6to4=False, + allow_dns64=False, + # Must be explicitly set to "False" if you don't want to try + # detecting local interface addresses with netifaces. + autodetect_local_addresses=True, + ): + if not port_blacklist and not port_whitelist: + # An assortment of common HTTPS? ports. + port_whitelist = self.DEFAULT_PORT_WHITELIST.copy() + self.ip_blacklist = ip_blacklist or set() + self.ip_whitelist = ip_whitelist or set() + self.port_blacklist = port_blacklist or set() + self.port_whitelist = port_whitelist or set() + # TODO: ATM this can contain either regexes or globs that are converted + # to regexes upon every check. Create a collection that automagically + # converts them to regexes on insert? + self.hostname_blacklist = hostname_blacklist or set() + self.allow_ipv6 = allow_ipv6 + self.allow_teredo = allow_teredo + self.allow_6to4 = allow_6to4 + self.allow_dns64 = allow_dns64 + self.autodetect_local_addresses = autodetect_local_addresses + + @add_local_address_arg + def is_ip_allowed(self, addr_ip, _local_addresses=None): + if not isinstance(addr_ip, (ipaddress.IPv4Address, ipaddress.IPv6Address)): + addr_ip = ipaddress.ip_address(addr_ip) + + # The whitelist should take precedence over the blacklist so we can + # punch holes in blacklisted ranges + if any(addr_ip in net for net in self.ip_whitelist): + return True + + if any(addr_ip in net for net in self.ip_blacklist): + return False + + if any(addr_ip in net for net in _local_addresses): + return False + + if addr_ip.version == 4: + if not addr_ip.is_private: + # IPs for carrier-grade NAT. Seems weird that it doesn't set + # `is_private`, but we need to check `not is_global` + if not ipaddress.ip_network(addr_ip).is_global: + return False + elif addr_ip.version == 6: + # You'd better have a good reason for enabling IPv6 + # because Advocate's techniques don't work well without NAT. + if not self.allow_ipv6: + return False + + # v6 addresses can also map to IPv4 addresses! Tricky! + v4_nested = [] + if addr_ip.ipv4_mapped: + v4_nested.append(addr_ip.ipv4_mapped) + # WTF IPv6? Why you gotta have a billion tunneling mechanisms? + # XXX: Do we even really care about these? If we're tunneling + # through public servers we shouldn't be able to access + # addresses on our private network, right? + if addr_ip.sixtofour: + if not self.allow_6to4: + return False + v4_nested.append(addr_ip.sixtofour) + if addr_ip.teredo: + if not self.allow_teredo: + return False + # Check both the client *and* server IPs + v4_nested.extend(addr_ip.teredo) + if addr_ip in self._DNS64_WK_PREFIX: + if not self.allow_dns64: + return False + # When using the well-known prefix the last 4 bytes + # are the IPv4 addr + v4_nested.append(ipaddress.ip_address(addr_ip.packed[-4:])) + + if not all(self.is_ip_allowed(addr_v4) for addr_v4 in v4_nested): + return False + + # fec0::*, apparently deprecated? + if addr_ip.is_site_local: + return False + else: + raise ValueError("Unsupported IP version(?): %r" % addr_ip) + + # 169.254.XXX.XXX, AWS uses these for autoconfiguration + if addr_ip.is_link_local: + return False + # 127.0.0.1, ::1, etc. + if addr_ip.is_loopback: + return False + if addr_ip.is_multicast: + return False + # 192.168.XXX.XXX, 10.XXX.XXX.XXX + if addr_ip.is_private: + return False + # 255.255.255.255, ::ffff:XXXX:XXXX (v6->v4) mapping + if addr_ip.is_reserved: + return False + # There's no reason to connect directly to a 6to4 relay + if addr_ip in self._6TO4_RELAY_NET: + return False + # 0.0.0.0 + if addr_ip.is_unspecified: + return False + + # It doesn't look bad, so... it's must be ok! + return True + + def _hostname_matches_pattern(self, hostname, pattern): + # If they specified a string, just assume they only want basic globbing. + # This stops people from not realizing they're dealing in REs and + # not escaping their periods unless they specifically pass in an RE. + # This has the added benefit of letting us sanely handle globbed + # IDNs by default. + if isinstance(pattern, str): + # convert the glob to a punycode glob, then a regex + pattern = fnmatch.translate(canonicalize_hostname(pattern)) + + hostname = canonicalize_hostname(hostname) + # Down the line the hostname may get treated as a null-terminated string + # (as with `socket.getaddrinfo`.) Try to account for that. + # + # >>> socket.getaddrinfo("example.com\x00aaaa", 80) + # [(2, 1, 6, '', ('93.184.216.34', 80)), [...] + no_null_hostname = hostname.split("\x00")[0] + + return any( + re.match(pattern, x.strip(".")) for x in (no_null_hostname, hostname) + ) + + def is_hostname_allowed(self, hostname): + # Sometimes (like with "external" services that your IP has privileged + # access to) you might not always know the IP range to blacklist access + # to, or the `A` record might change without you noticing. + # For e.x.: `foocorp.external.org`. + # + # Another option is doing something like: + # + # for addrinfo in socket.getaddrinfo("foocorp.external.org", 80): + # global_validator.ip_blacklist.add(ip_address(addrinfo[4][0])) + # + # but that's not always a good idea if they're behind a third-party lb. + for pattern in self.hostname_blacklist: + if self._hostname_matches_pattern(hostname, pattern): + return False + return True + + @add_local_address_arg + def is_addrinfo_allowed(self, addrinfo, _local_addresses=None): + assert len(addrinfo) == 5 + # XXX: Do we care about any of the other elements? Guessing not. + family, socktype, proto, canonname, sockaddr = addrinfo + + # The 4th elem inaddrinfo may either be a touple of two or four items, + # depending on whether we're dealing with IPv4 or v6 + if len(sockaddr) == 2: + # v4 + ip, port = sockaddr + elif len(sockaddr) == 4: + # v6 + # XXX: what *are* `flow_info` and `scope_id`? Anything useful? + # Seems like we can figure out all we need about the scope from + # the `is_` properties. + ip, port, flow_info, scope_id = sockaddr + else: + raise ValueError("Unexpected addrinfo format %r" % sockaddr) + + # Probably won't help protect against SSRF, but might prevent our being + # used to attack others' non-HTTP services. See + # http://www.remote.org/jochen/sec/hfpa/ + if self.port_whitelist and port not in self.port_whitelist: + return False + if port in self.port_blacklist: + return False + + if self.hostname_blacklist: + if not canonname: + raise NameserverException( + "addrinfo must contain the canon name to do blacklisting " + "based on hostname. Make sure you use the " + "`socket.AI_CANONNAME` flag, and that each record contains " + "the canon name. Your DNS server might also be garbage." + ) + + if not self.is_hostname_allowed(canonname): + return False + + return self.is_ip_allowed(ip, _local_addresses=_local_addresses) diff --git a/backend/src/advocate/api.py b/backend/src/advocate/api.py new file mode 100644 index 0000000000..dd0fac61a7 --- /dev/null +++ b/backend/src/advocate/api.py @@ -0,0 +1,293 @@ +""" +advocate.api +~~~~~~~~~~~~ + +This module implements the Requests API, largely a copy/paste from `requests` +itself. + +:copyright: (c) 2015 by Jordan Milne. +:license: Apache2, see LICENSE for more details. + +""" + +import hashlib +import pickle +from collections import OrderedDict + +from requests import Session as RequestsSession + +import advocate + +from .adapters import ValidatingHTTPAdapter +from .exceptions import MountDisabledException + + +class Session(RequestsSession): + """Convenience wrapper around `requests.Session` set up for `advocate`ing""" + + __attrs__ = RequestsSession.__attrs__ + ["validator"] + DEFAULT_VALIDATOR = None + """ + User-replaceable default validator to use for all Advocate sessions, + includes sessions created by advocate.get() + """ + + def __init__(self, *args, **kwargs): + self.validator = kwargs.pop("validator", None) or self.DEFAULT_VALIDATOR + adapter_kwargs = kwargs.pop("_adapter_kwargs", {}) + + # `Session.__init__()` calls `mount()` internally, so we need to allow + # it temporarily + self.__mount_allowed = True + RequestsSession.__init__(self, *args, **kwargs) + + # Drop any existing adapters + self.adapters = OrderedDict() + + self.mount( + "http://", ValidatingHTTPAdapter(validator=self.validator, **adapter_kwargs) + ) + self.mount( + "https://", + ValidatingHTTPAdapter(validator=self.validator, **adapter_kwargs), + ) + self.__mount_allowed = False + + def mount(self, *args, **kwargs): + """Wrapper around `mount()` to prevent a protection bypass""" + if self.__mount_allowed: + super().mount(*args, **kwargs) + else: + raise MountDisabledException( + "mount() is disabled to prevent protection bypasses" + ) + + +def session(*args, **kwargs): + return Session(*args, **kwargs) + + +def request(method, url, **kwargs): + """Constructs and sends a :class:`Request `. + + :param method: method for the new :class:`Request` object. + :param url: URL for the new :class:`Request` object. + :param params: (optional) Dictionary or bytes to be sent in the query string for the :class:`Request`. + :param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`. + :param json: (optional) json data to send in the body of the :class:`Request`. + :param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`. + :param cookies: (optional) Dict or CookieJar object to send with the :class:`Request`. + :param files: (optional) Dictionary of ``'name': file-like-objects`` (or ``{'name': ('filename', fileobj)}``) for multipart encoding upload. + :param auth: (optional) Auth tuple to enable Basic/Digest/Custom HTTP Auth. + :param timeout: (optional) How long to wait for the server to send data + before giving up, as a float, or a (`connect timeout, read timeout + `_) tuple. + :type timeout: float or tuple + :param allow_redirects: (optional) Boolean. Set to True if POST/PUT/DELETE redirect following is allowed. + :type allow_redirects: bool + :param proxies: (optional) Dictionary mapping protocol to the URL of the proxy. + :param verify: (optional) if ``True``, the SSL cert will be verified. A CA_BUNDLE path can also be provided. + :param stream: (optional) if ``False``, the response content will be immediately downloaded. + :param cert: (optional) if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair. + :return: :class:`Response ` object + :rtype: requests.Response + + Usage:: + + >>> import advocate + >>> req = advocate.request('GET', 'http://httpbin.org/get') + + """ + + validator = kwargs.pop("validator", None) + with Session(validator=validator) as sess: + response = sess.request(method=method, url=url, **kwargs) + return response + + +def get(url, **kwargs): + r"""Sends a GET request. + + :param url: URL for the new :class:`Request` object. + :param \*\*kwargs: Optional arguments that ``request`` takes. + :return: :class:`Response ` object + :rtype: requests.Response + """ + + kwargs.setdefault("allow_redirects", True) + return request("get", url, **kwargs) + + +def options(url, **kwargs): + r"""Sends a OPTIONS request. + + :param url: URL for the new :class:`Request` object. + :param \*\*kwargs: Optional arguments that ``request`` takes. + :return: :class:`Response ` object + :rtype: requests.Response + """ + + kwargs.setdefault("allow_redirects", True) + return request("options", url, **kwargs) + + +def head(url, **kwargs): + r"""Sends a HEAD request. + + :param url: URL for the new :class:`Request` object. + :param \*\*kwargs: Optional arguments that ``request`` takes. + :return: :class:`Response ` object + :rtype: requests.Response + """ + + kwargs.setdefault("allow_redirects", False) + return request("head", url, **kwargs) + + +def post(url, data=None, json=None, **kwargs): + r"""Sends a POST request. + + :param url: URL for the new :class:`Request` object. + :param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`. + :param json: (optional) json data to send in the body of the :class:`Request`. + :param \*\*kwargs: Optional arguments that ``request`` takes. + :return: :class:`Response ` object + :rtype: requests.Response + """ + + return request("post", url, data=data, json=json, **kwargs) + + +def put(url, data=None, **kwargs): + r"""Sends a PUT request. + + :param url: URL for the new :class:`Request` object. + :param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`. + :param \*\*kwargs: Optional arguments that ``request`` takes. + :return: :class:`Response ` object + :rtype: requests.Response + """ + + return request("put", url, data=data, **kwargs) + + +def patch(url, data=None, **kwargs): + r"""Sends a PATCH request. + + :param url: URL for the new :class:`Request` object. + :param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`. + :param \*\*kwargs: Optional arguments that ``request`` takes. + :return: :class:`Response ` object + :rtype: requests.Response + """ + + return request("patch", url, data=data, **kwargs) + + +def delete(url, **kwargs): + r"""Sends a DELETE request. + + :param url: URL for the new :class:`Request` object. + :param \*\*kwargs: Optional arguments that ``request`` takes. + :return: :class:`Response ` object + :rtype: requests.Response + """ + + return request("delete", url, **kwargs) + + +class RequestsAPIWrapper: + """Provides a `requests.api`-like interface with a specific validator""" + + # Due to how the classes are dynamically constructed pickling may not work + # correctly unless loaded within the same interpreter instance. + # Enable at your peril. + SUPPORT_WRAPPER_PICKLING = False + + def __init__(self, validator): + # Do this here to avoid circular import issues + try: + from .futures import FuturesSession + + have_requests_futures = True + except ImportError: + have_requests_futures = False + + self.validator = validator + outer_self = self + + class _WrappedSession(Session): + """An `advocate.Session` that uses the wrapper's blacklist + + the wrapper is meant to be a transparent replacement for `requests`, + so people should be able to subclass `wrapper.Session` and still + get the desired validation behaviour + """ + + DEFAULT_VALIDATOR = outer_self.validator + + self._make_wrapper_cls_global(_WrappedSession) + + if have_requests_futures: + + class _WrappedFuturesSession(FuturesSession): + """Like _WrappedSession, but for `FuturesSession`s""" + + DEFAULT_VALIDATOR = outer_self.validator + + self._make_wrapper_cls_global(_WrappedFuturesSession) + + self.FuturesSession = _WrappedFuturesSession + + self.request = self._default_arg_wrapper(request) + self.get = self._default_arg_wrapper(get) + self.options = self._default_arg_wrapper(options) + self.head = self._default_arg_wrapper(head) + self.post = self._default_arg_wrapper(post) + self.put = self._default_arg_wrapper(put) + self.patch = self._default_arg_wrapper(patch) + self.delete = self._default_arg_wrapper(delete) + self.session = self._default_arg_wrapper(session) + self.Session = _WrappedSession + + def __getattr__(self, item): + # This class is meant to mimic the requests base module, so if we don't + # have this attribute, it might be on the base module (like the Request + # class, etc.) + try: + return object.__getattribute__(self, item) + except AttributeError: + return getattr(advocate, item) + + def _default_arg_wrapper(self, fun): + def wrapped_func(*args, **kwargs): + kwargs.setdefault("validator", self.validator) + return fun(*args, **kwargs) + + return wrapped_func + + def _make_wrapper_cls_global(self, cls): + if not self.SUPPORT_WRAPPER_PICKLING: + return + # Gnarly, but necessary to give pickle a consistent module-level + # reference for each wrapper. + wrapper_hash = hashlib.sha256(pickle.dumps(self)).hexdigest() + cls.__name__ = "_".join((cls.__name__, wrapper_hash)) + cls.__qualname__ = ".".join((__name__, cls.__name__)) + if not globals().get(cls.__name__): + globals()[cls.__name__] = cls + + +__all__ = ( + "delete", + "get", + "head", + "options", + "patch", + "post", + "put", + "request", + "session", + "Session", + "RequestsAPIWrapper", +) diff --git a/backend/src/advocate/connection.py b/backend/src/advocate/connection.py new file mode 100644 index 0000000000..976aeb5717 --- /dev/null +++ b/backend/src/advocate/connection.py @@ -0,0 +1,186 @@ +import ipaddress +import socket +from socket import timeout as SocketTimeout + +from urllib3.connection import HTTPConnection, HTTPSConnection +from urllib3.exceptions import ConnectTimeoutError +from urllib3.util.connection import _set_socket_options +from urllib3.util.connection import create_connection as old_create_connection + +from . import addrvalidator +from .exceptions import UnacceptableAddressException + + +def advocate_getaddrinfo(host, port, get_canonname=False): + addrinfo = socket.getaddrinfo( + host, + port, + 0, + socket.SOCK_STREAM, + 0, + # We need what the DNS client sees the hostname as, correctly handles + # IDNs and tricky things like `private.foocorp.org\x00.google.com`. + # All IDNs will be converted to punycode. + socket.AI_CANONNAME if get_canonname else 0, + ) + return fix_addrinfo(addrinfo) + + +def fix_addrinfo(records): + """ + Propagate the canonname across records and parse IPs + + I'm not sure if this is just the behaviour of `getaddrinfo` on Linux, but + it seems like only the first record in the set has the canonname field + populated. + """ + + def fix_record(record, canonname): + sa = record[4] + sa = (ipaddress.ip_address(sa[0]),) + sa[1:] + return record[0], record[1], record[2], canonname, sa + + canonname = None + if records: + # Apparently the canonical name is only included in the first record? + # Add it to all of them. + assert len(records[0]) == 5 + canonname = records[0][3] + return tuple(fix_record(x, canonname) for x in records) + + +# Lifted from requests' urllib3, which in turn lifted it from `socket.py`. Oy! +def validating_create_connection( + address, + timeout=socket._GLOBAL_DEFAULT_TIMEOUT, + source_address=None, + socket_options=None, + validator=None, +): + """Connect to *address* and return the socket object. + + Convenience function. Connect to *address* (a 2-tuple ``(host, + port)``) and return the socket object. Passing the optional + *timeout* parameter will set the timeout on the socket instance + before attempting to connect. If no *timeout* is supplied, the + global default timeout setting returned by :func:`getdefaulttimeout` + is used. If *source_address* is set it must be a tuple of (host, port) + for the socket to bind as a source address before making the connection. + An host of '' or port 0 tells the OS to use the default. + """ + + host, port = address + # We can skip asking for the canon name if we're not doing hostname-based + # blacklisting. + need_canonname = False + if validator.hostname_blacklist: + need_canonname = True + # We check both the non-canonical and canonical hostnames so we can + # catch both of these: + # CNAME from nonblacklisted.com -> blacklisted.com + # CNAME from blacklisted.com -> nonblacklisted.com + if not validator.is_hostname_allowed(host): + raise UnacceptableAddressException(host) + + err = None + addrinfo = advocate_getaddrinfo(host, port, get_canonname=need_canonname) + if addrinfo: + if validator.autodetect_local_addresses: + local_addresses = addrvalidator.determine_local_addresses() + else: + local_addresses = [] + for res in addrinfo: + # Are we allowed to connect with this result? + if not validator.is_addrinfo_allowed( + res, + _local_addresses=local_addresses, + ): + continue + af, socktype, proto, canonname, sa = res + # Unparse the validated IP + sa = (sa[0].exploded,) + sa[1:] + sock = None + try: + sock = socket.socket(af, socktype, proto) + + # If provided, set socket level options before connecting. + # This is the only addition urllib3 makes to this function. + _set_socket_options(sock, socket_options) + + if timeout is not socket._GLOBAL_DEFAULT_TIMEOUT: + sock.settimeout(timeout) + if source_address: + sock.bind(source_address) + sock.connect(sa) + return sock + + except socket.error as _: + err = _ + if sock is not None: + sock.close() + sock = None + + if err is None: + # If we got here, none of the results were acceptable + err = UnacceptableAddressException(address) + if err is not None: + raise err + else: + raise socket.error("getaddrinfo returns an empty list") + + +# TODO: Is there a better way to add this to multiple classes with different +# base classes? I tried a mixin, but it used the base method instead. +def _validating_new_conn(self): + """Establish a socket connection and set nodelay settings on it. + + :return: New socket connection. + """ + extra_kw = {} + if self.source_address: + extra_kw["source_address"] = self.source_address + + if self.socket_options: + extra_kw["socket_options"] = self.socket_options + + try: + # Hack around HTTPretty's patched sockets + # TODO: some better method of hacking around it that checks if we + # _would have_ connected to a private addr? + conn_func = validating_create_connection + if socket.getaddrinfo.__module__.startswith("httpretty"): + conn_func = old_create_connection + else: + extra_kw["validator"] = self._validator + + conn = conn_func((self.host, self.port), self.timeout, **extra_kw) + + except SocketTimeout: + raise ConnectTimeoutError( + self, + "Connection to %s timed out. (connect timeout=%s)" + % (self.host, self.timeout), + ) + + return conn + + +# Don't silently break if the private API changes across urllib3 versions +assert hasattr(HTTPConnection, "_new_conn") +assert hasattr(HTTPSConnection, "_new_conn") + + +class ValidatingHTTPConnection(HTTPConnection): + _new_conn = _validating_new_conn + + def __init__(self, *args, **kwargs): + self._validator = kwargs.pop("validator") + HTTPConnection.__init__(self, *args, **kwargs) + + +class ValidatingHTTPSConnection(HTTPSConnection): + _new_conn = _validating_new_conn + + def __init__(self, *args, **kwargs): + self._validator = kwargs.pop("validator") + HTTPSConnection.__init__(self, *args, **kwargs) diff --git a/backend/src/advocate/connectionpool.py b/backend/src/advocate/connectionpool.py new file mode 100644 index 0000000000..4cb24f4ea7 --- /dev/null +++ b/backend/src/advocate/connectionpool.py @@ -0,0 +1,22 @@ +from urllib3 import HTTPConnectionPool, HTTPSConnectionPool + +from .connection import ( + ValidatingHTTPConnection, + ValidatingHTTPSConnection, +) + +# Don't silently break if the private API changes across urllib3 versions +assert hasattr(HTTPConnectionPool, "ConnectionCls") +assert hasattr(HTTPSConnectionPool, "ConnectionCls") +assert hasattr(HTTPConnectionPool, "scheme") +assert hasattr(HTTPSConnectionPool, "scheme") + + +class ValidatingHTTPConnectionPool(HTTPConnectionPool): + scheme = "http" + ConnectionCls = ValidatingHTTPConnection + + +class ValidatingHTTPSConnectionPool(HTTPSConnectionPool): + scheme = "https" + ConnectionCls = ValidatingHTTPSConnection diff --git a/backend/src/advocate/exceptions.py b/backend/src/advocate/exceptions.py new file mode 100644 index 0000000000..c033df63d1 --- /dev/null +++ b/backend/src/advocate/exceptions.py @@ -0,0 +1,22 @@ +class AdvocateException(Exception): + pass + + +class UnacceptableAddressException(AdvocateException): + pass + + +class NameserverException(AdvocateException): + pass + + +class MountDisabledException(AdvocateException): + pass + + +class ProxyDisabledException(NotImplementedError, AdvocateException): + pass + + +class ConfigException(AdvocateException): + pass diff --git a/backend/src/advocate/futures.py b/backend/src/advocate/futures.py new file mode 100644 index 0000000000..9f54fc06de --- /dev/null +++ b/backend/src/advocate/futures.py @@ -0,0 +1,35 @@ +from concurrent.futures import ThreadPoolExecutor + +import requests_futures.sessions +from requests.adapters import DEFAULT_POOLSIZE + +from . import Session + + +class FuturesSession(requests_futures.sessions.FuturesSession, Session): + def __init__(self, executor=None, max_workers=2, session=None, *args, **kwargs): + adapter_kwargs = {} + if executor is None: + executor = ThreadPoolExecutor(max_workers=max_workers) + # set connection pool size equal to max_workers if needed + if max_workers > DEFAULT_POOLSIZE: + adapter_kwargs = dict( + pool_connections=max_workers, pool_maxsize=max_workers + ) + kwargs["_adapter_kwargs"] = adapter_kwargs + Session.__init__(self, *args, **kwargs) + self.executor = executor + self.session = session + + @property + def session(self): + return None + + @session.setter + def session(self, value): + if value is not None and not isinstance(value, Session): + raise NotImplementedError( + "Setting the .session property to " + "non-advocate values disabled " + "to prevent whitelist bypasses" + ) diff --git a/backend/src/advocate/monkeypatching.py b/backend/src/advocate/monkeypatching.py new file mode 100644 index 0000000000..39b39ed5ef --- /dev/null +++ b/backend/src/advocate/monkeypatching.py @@ -0,0 +1,39 @@ +import contextlib +import os.path +import socket +import traceback + + +class DisallowedConnectException(Exception): + pass + + +class CheckedSocket(socket.socket): + CONNECT_ALLOWED_FUNCS = {"validating_create_connection"} + # `test_testserver.py` makes raw connections to the test server to ensure it works + CONNECT_ALLOWED_FILES = {"test_testserver.py"} + _checks_enabled = True + + @classmethod + @contextlib.contextmanager + def bypass_checks(cls): + try: + cls._checks_enabled = False + yield + finally: + cls._checks_enabled = True + + @classmethod + def _check_frame_allowed(cls, frame): + if os.path.basename(frame[0]) in cls.CONNECT_ALLOWED_FILES: + return True + if frame[2] in cls.CONNECT_ALLOWED_FUNCS: + return True + return False + + def connect(self, *args, **kwargs): + if self._checks_enabled: + stack = traceback.extract_stack() + if not any(self._check_frame_allowed(frame) for frame in stack): + raise DisallowedConnectException("calling socket.connect() unsafely!") + return super().connect(*args, **kwargs) diff --git a/backend/src/advocate/poolmanager.py b/backend/src/advocate/poolmanager.py new file mode 100644 index 0000000000..10579ca56c --- /dev/null +++ b/backend/src/advocate/poolmanager.py @@ -0,0 +1,45 @@ +import collections +import functools + +from urllib3 import PoolManager +from urllib3.poolmanager import PoolKey, _default_key_normalizer + +from .connectionpool import ( + ValidatingHTTPConnectionPool, + ValidatingHTTPSConnectionPool, +) + +pool_classes_by_scheme = { + "http": ValidatingHTTPConnectionPool, + "https": ValidatingHTTPSConnectionPool, +} + +AdvocatePoolKey = collections.namedtuple( + "AdvocatePoolKey", PoolKey._fields + ("key_validator",) +) + + +def key_normalizer(key_class, request_context): + request_context = request_context.copy() + # TODO: add ability to serialize validator rules to dict, + # allowing pool to be shared between sessions with the same + # rules. + request_context["validator"] = id(request_context["validator"]) + return _default_key_normalizer(key_class, request_context) + + +key_fn_by_scheme = { + "http": functools.partial(key_normalizer, AdvocatePoolKey), + "https": functools.partial(key_normalizer, AdvocatePoolKey), +} + + +class ValidatingPoolManager(PoolManager): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Make sure the API hasn't changed + assert hasattr(self, "pool_classes_by_scheme") + + self.pool_classes_by_scheme = pool_classes_by_scheme + self.key_fn_by_scheme = key_fn_by_scheme.copy() diff --git a/backend/src/advocate/test_advocate.py b/backend/src/advocate/test_advocate.py new file mode 100644 index 0000000000..0fb8bb771f --- /dev/null +++ b/backend/src/advocate/test_advocate.py @@ -0,0 +1,688 @@ +# ruff: noqa +from __future__ import division + +import socket + +import pytest + +# This needs to be done before third-party imports to make sure they all use +# our wrapped socket class, especially in case of subclasses. +from .monkeypatching import CheckedSocket, DisallowedConnectException + +import ipaddress +import pickle +import unittest +from unittest.mock import patch + +import responses + +import advocate +from advocate import AddrValidator +from advocate.addrvalidator import canonicalize_hostname +from advocate.api import RequestsAPIWrapper +from advocate.connection import advocate_getaddrinfo +from advocate.exceptions import ( + ConfigException, + MountDisabledException, + NameserverException, + UnacceptableAddressException, +) +from advocate.futures import FuturesSession + +import requests + + +# We use port 1 for testing because nothing is likely to legitimately listen +# on it. +AddrValidator.DEFAULT_PORT_WHITELIST.add(1) + +RequestsAPIWrapper.SUPPORT_WRAPPER_PICKLING = True +global_wrapper = RequestsAPIWrapper( + validator=AddrValidator( + ip_whitelist={ + ipaddress.ip_network("127.0.0.1"), + } + ) +) +RequestsAPIWrapper.SUPPORT_WRAPPER_PICKLING = False + + +class _WrapperSubclass(global_wrapper.Session): + def good_method(self): + return "foo" + + +def canonname_supported(): + """Check if the nameserver supports the AI_CANONNAME flag + + travis-ci.org's Python 3 env doesn't seem to support it, so don't try + any of the test that rely on it. + """ + addrinfo = advocate_getaddrinfo("example.com", 0, get_canonname=True) + assert addrinfo + return addrinfo[0][3] == b"example.com" + + +def permissive_validator(**kwargs): + default_options = dict( + ip_blacklist=None, + port_whitelist=None, + port_blacklist=None, + hostname_blacklist=None, + allow_ipv6=True, + allow_teredo=True, + allow_6to4=True, + allow_dns64=True, + autodetect_local_addresses=False, + ) + default_options.update(**kwargs) + return AddrValidator(**default_options) + + +# Test our test wrappers to make sure they're testy +@pytest.mark.skip(reason="Requires monkeypatching socket.socket=CheckedSocket") +class TestWrapperTests(unittest.TestCase): + def test_unsafe_connect_raises(self): + self.assertRaises( + DisallowedConnectException, requests.get, "http://example.org/" + ) + + +class ValidateIPTests(unittest.TestCase): + def _test_ip_kind_blocked(self, ip, **kwargs): + validator = permissive_validator(**kwargs) + self.assertFalse(validator.is_ip_allowed(ip)) + + def test_manual_ip_blacklist(self): + """Test manually blacklisting based on IP""" + validator = AddrValidator( + allow_ipv6=True, + ip_blacklist=( + ipaddress.ip_network("132.0.5.0/24"), + ipaddress.ip_network("152.0.0.0/8"), + ipaddress.ip_network("::1"), + ), + ) + self.assertFalse(validator.is_ip_allowed("132.0.5.1")) + self.assertFalse(validator.is_ip_allowed("152.254.90.1")) + self.assertTrue(validator.is_ip_allowed("178.254.90.1")) + self.assertFalse(validator.is_ip_allowed("::1")) + # Google, found via `dig google.com AAAA` + self.assertTrue(validator.is_ip_allowed("2607:f8b0:400a:807::200e")) + + def test_ip_whitelist(self): + """Test manually whitelisting based on IP""" + validator = AddrValidator( + ip_whitelist=(ipaddress.ip_network("127.0.0.1"),), + ) + self.assertTrue(validator.is_ip_allowed("127.0.0.1")) + + def test_ip_whitelist_blacklist_conflict(self): + """Manual whitelist should take precedence over manual blacklist""" + validator = AddrValidator( + ip_whitelist=(ipaddress.ip_network("127.0.0.1"),), + ip_blacklist=(ipaddress.ip_network("127.0.0.1"),), + ) + self.assertTrue(validator.is_ip_allowed("127.0.0.1")) + + @unittest.skip("takes half an hour or so to run") + def test_safecurl_blacklist(self): + """Test that we at least disallow everything SafeCurl does""" + # All IPs that SafeCurl would disallow + bad_netblocks = ( + ipaddress.ip_network(x) + for x in ( + "0.0.0.0/8", + "10.0.0.0/8", + "100.64.0.0/10", + "127.0.0.0/8", + "169.254.0.0/16", + "172.16.0.0/12", + "192.0.0.0/29", + "192.0.2.0/24", + "192.88.99.0/24", + "192.168.0.0/16", + "198.18.0.0/15", + "198.51.100.0/24", + "203.0.113.0/24", + "224.0.0.0/4", + "240.0.0.0/4", + ) + ) + i = 0 + validator = AddrValidator() + for bad_netblock in bad_netblocks: + num_ips = bad_netblock.num_addresses + # Don't test *every* IP in large netblocks + step_size = int(min(max(num_ips / 255, 1), 128)) + for ip_idx in range(0, num_ips, step_size): + i += 1 + bad_ip = bad_netblock[ip_idx] + bad_ip_allowed = validator.is_ip_allowed(bad_ip) + if bad_ip_allowed: + print(i, bad_ip) + self.assertFalse(bad_ip_allowed) + + # TODO: something like the above for IPv6? + + def test_ipv4_mapped(self): + self._test_ip_kind_blocked("::ffff:192.168.2.1") + + def test_teredo(self): + # 192.168.2.1 as the client address + self._test_ip_kind_blocked("2001:0000:4136:e378:8000:63bf:3f57:fdf2") + # This should be disallowed even if teredo is allowed. + self._test_ip_kind_blocked( + "2001:0000:4136:e378:8000:63bf:3f57:fdf2", + allow_teredo=False, + ) + + def test_ipv6(self): + self._test_ip_kind_blocked("2002:C0A8:FFFF::", allow_ipv6=False) + + def test_sixtofour(self): + # 192.168.XXX.XXX + self._test_ip_kind_blocked("2002:C0A8:FFFF::") + self._test_ip_kind_blocked("2002:C0A8:FFFF::", allow_6to4=False) + + def test_dns64(self): + # XXX: Don't even know if this is an issue, TBH. Seems to be related + # to DNS64/NAT64, but not a lot of easy-to-understand info: + # https://tools.ietf.org/html/rfc6052 + self._test_ip_kind_blocked("64:ff9b::192.168.2.1") + self._test_ip_kind_blocked("64:ff9b::192.168.2.1", allow_dns64=False) + + def test_link_local(self): + # 169.254.XXX.XXX, AWS uses these for autoconfiguration + self._test_ip_kind_blocked("169.254.1.1") + + def test_site_local(self): + self._test_ip_kind_blocked("FEC0:CCCC::") + + def test_loopback(self): + self._test_ip_kind_blocked("127.0.0.1") + self._test_ip_kind_blocked("::1") + + def test_multicast(self): + self._test_ip_kind_blocked("227.1.1.1") + + def test_private(self): + self._test_ip_kind_blocked("192.168.2.1") + self._test_ip_kind_blocked("10.5.5.5") + self._test_ip_kind_blocked("0.0.0.0") + self._test_ip_kind_blocked("0.1.1.1") + self._test_ip_kind_blocked("100.64.0.0") + + def test_reserved(self): + self._test_ip_kind_blocked("255.255.255.255") + self._test_ip_kind_blocked("::ffff:192.168.2.1") + # 6to4 relay + self._test_ip_kind_blocked("192.88.99.0") + + def test_unspecified(self): + self._test_ip_kind_blocked("0.0.0.0") + + def test_parsed(self): + validator = permissive_validator() + self.assertFalse(validator.is_ip_allowed(ipaddress.ip_address("0.0.0.0"))) + self.assertTrue(validator.is_ip_allowed(ipaddress.ip_address("144.1.1.1"))) + + +class AddrInfoTests(unittest.TestCase): + def _is_addrinfo_allowed(self, host, port, **kwargs): + validator = permissive_validator(**kwargs) + allowed = False + for res in advocate_getaddrinfo(host, port): + if validator.is_addrinfo_allowed(res): + allowed = True + return allowed + + def test_simple(self): + self.assertFalse(self._is_addrinfo_allowed("192.168.0.1", 80)) + + def test_malformed_addrinfo(self): + # Alright, the addrinfo format is probably never going to change, + # but *what if it did?* + vl = permissive_validator() + addrinfo = advocate_getaddrinfo("example.com", 80)[0] + (1,) + self.assertRaises(Exception, lambda: vl.is_addrinfo_allowed(addrinfo)) + + def test_unexpected_proto(self): + # What if addrinfo returns info about a protocol we don't understand? + vl = permissive_validator() + addrinfo = list(advocate_getaddrinfo("example.com", 80)[0]) + addrinfo[4] = addrinfo[4] + (1,) + self.assertRaises(Exception, lambda: vl.is_addrinfo_allowed(addrinfo)) + + def test_default_port_whitelist(self): + self.assertTrue(self._is_addrinfo_allowed("200.1.1.1", 8080)) + self.assertTrue(self._is_addrinfo_allowed("200.1.1.1", 80)) + self.assertFalse(self._is_addrinfo_allowed("200.1.1.1", 99)) + + def test_port_whitelist(self): + wl = (80, 10) + self.assertTrue(self._is_addrinfo_allowed("200.1.1.1", 80, port_whitelist=wl)) + self.assertTrue(self._is_addrinfo_allowed("200.1.1.1", 10, port_whitelist=wl)) + self.assertFalse(self._is_addrinfo_allowed("200.1.1.1", 99, port_whitelist=wl)) + + def test_port_blacklist(self): + bl = (80, 10) + self.assertFalse(self._is_addrinfo_allowed("200.1.1.1", 80, port_blacklist=bl)) + self.assertFalse(self._is_addrinfo_allowed("200.1.1.1", 10, port_blacklist=bl)) + self.assertTrue(self._is_addrinfo_allowed("200.1.1.1", 99, port_blacklist=bl)) + + @patch("advocate.addrvalidator.determine_local_addresses") + def test_local_address_handling(self, mock_determine_local_addresses): + fake_addresses = [ipaddress.ip_network("200.1.1.1")] + mock_determine_local_addresses.return_value = fake_addresses + + self.assertFalse( + self._is_addrinfo_allowed("200.1.1.1", 80, autodetect_local_addresses=True) + ) + # Check that `is_ip_allowed` didn't make its own call to determine + # local addresses + mock_determine_local_addresses.assert_called_once_with() + mock_determine_local_addresses.reset_mock() + + self.assertTrue( + self._is_addrinfo_allowed( + "200.1.1.1", + 80, + autodetect_local_addresses=False, + ) + ) + mock_determine_local_addresses.assert_not_called() + + def test_netifaces_presence_optional(self): + # Advocate should still work without netifaces, but only if you've specifically + # said you don't care about checking against local interface addresses. + with patch("advocate.addrvalidator.HAVE_NETIFACES", False): + with self.assertRaises(ConfigException): + self._is_addrinfo_allowed( + "200.1.1.1", 80, autodetect_local_addresses=True + ) + with self.assertRaises(ConfigException): + advocate.addrvalidator.determine_local_addresses() + # Should be fine if you specifically asked to not look at the local addrs + self.assertTrue( + self._is_addrinfo_allowed( + "200.1.1.1", + 80, + autodetect_local_addresses=False, + ) + ) + + # These shouldn't `raise` + self._is_addrinfo_allowed("200.1.1.1", 80, autodetect_local_addresses=True) + advocate.addrvalidator.determine_local_addresses() + + +@unittest.skipIf( + not canonname_supported(), + "Nameserver doesn't support AI_CANONNAME, skipping hostname tests", +) +class HostnameTests(unittest.TestCase): + def setUp(self): + self._canonname_supported = canonname_supported() + + def _is_hostname_allowed(self, host, fake_lookup=False, **kwargs): + validator = permissive_validator(**kwargs) + if fake_lookup: + results = [ + (2, 1, 6, canonicalize_hostname(host).encode("utf8"), ("1.2.3.4", 80)) + ] + else: + results = advocate_getaddrinfo(host, 80, get_canonname=True) + for res in results: + if validator.is_addrinfo_allowed(res): + return True + return False + + def test_no_blacklist(self): + self.assertTrue(self._is_hostname_allowed("example.com")) + + def test_idn(self): + # test some basic globs + self.assertFalse( + self._is_hostname_allowed( + "中国.example.org", fake_lookup=True, hostname_blacklist={"*.org"} + ) + ) + # case-insensitive, please + self.assertFalse( + self._is_hostname_allowed( + "中国.example.oRg", fake_lookup=True, hostname_blacklist={"*.Org"} + ) + ) + self.assertFalse( + self._is_hostname_allowed( + "中国.example.org", + fake_lookup=True, + hostname_blacklist={"xn--fiqs8s.*.org"}, + ) + ) + self.assertFalse( + self._is_hostname_allowed( + "xn--fiqs8s.example.org", + fake_lookup=True, + hostname_blacklist={"中国.*.org"}, + ) + ) + self.assertTrue( + self._is_hostname_allowed( + "example.org", fake_lookup=True, hostname_blacklist={"中国.*.org"} + ) + ) + self.assertTrue( + self._is_hostname_allowed( + "example.com", fake_lookup=True, hostname_blacklist={"中国.*.org"} + ) + ) + self.assertTrue( + self._is_hostname_allowed( + "foo.example.org", fake_lookup=True, hostname_blacklist={"中国.*.org"} + ) + ) + + def test_missing_canonname(self): + addrinfo = socket.getaddrinfo( + "127.0.0.1", + 1, + 0, + socket.SOCK_STREAM, + ) + self.assertTrue(addrinfo) + + # Should throw an error if we're using hostname blacklisting and the + # addrinfo record we passed in doesn't have a canonname + validator = permissive_validator(hostname_blacklist={"foo"}) + self.assertRaises( + NameserverException, validator.is_addrinfo_allowed, addrinfo[0] + ) + + def test_embedded_null(self): + vl = permissive_validator(hostname_blacklist={"*.baz.com"}) + # Things get a little screwy with embedded nulls. Try to emulate any + # possible null termination when checking if the hostname is allowed. + self.assertFalse(vl.is_hostname_allowed("foo.baz.com\x00.example.com")) + self.assertFalse(vl.is_hostname_allowed("foo.example.com\x00.baz.com")) + self.assertFalse(vl.is_hostname_allowed("foo.baz.com\x00.example.com")) + self.assertFalse(vl.is_hostname_allowed("foo.example.com\x00.baz.com")) + + +class ConnectionPoolingTests(unittest.TestCase): + @patch("advocate.connection.ValidatingHTTPConnection._new_conn") + def test_connection_reuse(self, mock_new_conn): + # Just because you can use an existing connection doesn't mean you + # should. The disadvantage of us working at the socket level means that + # we get bitten if a connection pool is shared between regular requests + # and advocate. + # This can never happen with requests, but let's set a good example :) + with CheckedSocket.bypass_checks(): + # HTTPBin supports `keep-alive`, so it's a good test subject + requests.get("http://httpbin.org/") + mock_new_conn.assert_not_called() + try: + advocate.get("http://httpbin.org/") + except: + pass + # Requests may retry several times, but our mock doesn't return a real + # socket. Just check that it tried to create one. + mock_new_conn.assert_any_call() + + +class AdvocateWrapperTests(unittest.TestCase): + def test_get(self): + self.assertEqual(advocate.get("http://example.com").status_code, 200) + self.assertEqual(advocate.get("https://example.com").status_code, 200) + + def test_validator(self): + self.assertRaises( + UnacceptableAddressException, advocate.get, "http://127.0.0.1/" + ) + self.assertRaises( + UnacceptableAddressException, advocate.get, "http://localhost/" + ) + self.assertRaises( + UnacceptableAddressException, advocate.get, "https://localhost/" + ) + + @unittest.skipIf( + not canonname_supported(), + "Nameserver doesn't support AI_CANONNAME, skipping hostname tests", + ) + def test_blacklist_hostname(self): + self.assertRaises( + UnacceptableAddressException, + advocate.get, + "https://google.com/", + validator=AddrValidator(hostname_blacklist={"google.com"}), + ) + + # Disabled for now because the redirection endpoint appears to be broken. + @unittest.skip + def test_redirect(self): + # Make sure httpbin even works + test_url = "http://httpbin.org/status/204" + self.assertEqual(advocate.get(test_url).status_code, 204) + + redir_url = "http://httpbin.org/redirect-to?url=http://127.0.0.1/" + self.assertRaises(UnacceptableAddressException, advocate.get, redir_url) + + def test_mount_disabled(self): + sess = advocate.Session() + self.assertRaises( + MountDisabledException, + sess.mount, + "foo://", + None, + ) + + def test_advocate_requests_api_wrapper(self): + wrapper = RequestsAPIWrapper(validator=AddrValidator()) + local_validator = AddrValidator( + ip_whitelist={ + ipaddress.ip_network("127.0.0.1"), + } + ) + local_wrapper = RequestsAPIWrapper(validator=local_validator) + + self.assertRaises( + UnacceptableAddressException, wrapper.get, "http://127.0.0.1:1/" + ) + + with self.assertRaises(Exception) as cm: + local_wrapper.get("http://127.0.0.1:1/") + # Check that we got a connection exception instead of a validation one + # This might be either exception depending on the requests version + self.assertRegexpMatches( + cm.exception.__class__.__name__, + r"\A(Connection|Protocol)Error", + ) + self.assertRaises( + UnacceptableAddressException, wrapper.get, "http://localhost:1/" + ) + self.assertRaises( + UnacceptableAddressException, wrapper.get, "https://localhost:1/" + ) + + def test_advocate_default_validator_replaceable(self): + new_validator = AddrValidator(hostname_blacklist=["example.org"]) + with patch("advocate.api.Session.DEFAULT_VALIDATOR", new_validator): + with self.assertRaises(UnacceptableAddressException): + advocate.get("http://example.org") + + def test_wrapper_session_pickle(self): + # Make sure the validator still works after a pickle round-trip + sess_instance = pickle.loads(pickle.dumps(global_wrapper.Session())) + + with self.assertRaises(Exception) as cm: + sess_instance.get("http://127.0.0.1:1/") + self.assertRegexpMatches( + cm.exception.__class__.__name__, + r"\A(Connection|Protocol)Error", + ) + self.assertRaises( + UnacceptableAddressException, sess_instance.get, "http://127.0.1.1:1/" + ) + + def test_wrapper_session_subclass(self): + # Make sure pickle doesn't explode if we try to pickle a subclass + # of `global_wrapper.Session` + def _check_instance(instance): + self.assertEqual(instance.good_method(), "foo") + + with self.assertRaises(Exception) as cm: + instance.get("http://127.0.0.1:1/") + self.assertRegexpMatches( + cm.exception.__class__.__name__, + r"\A(Connection|Protocol)Error", + ) + self.assertRaises( + UnacceptableAddressException, instance.get, "http://127.0.1.1:1/" + ) + + sess = _WrapperSubclass() + _check_instance(sess) + sess_unpickled = pickle.loads(pickle.dumps(sess)) + _check_instance(sess_unpickled) + + @unittest.skipIf( + not canonname_supported(), + "Nameserver doesn't support AI_CANONNAME, skipping hostname tests", + ) + def test_advocate_requests_api_wrapper_hostnames(self): + wrapper = RequestsAPIWrapper( + validator=AddrValidator( + hostname_blacklist={"google.com"}, + ) + ) + self.assertRaises( + UnacceptableAddressException, + wrapper.get, + "https://google.com/", + ) + + @responses.activate + def test_advocate_requests_api_wrapper_req_methods(self): + # Make sure all the convenience methods make requests with the correct + # methods + wrapper = RequestsAPIWrapper(AddrValidator()) + + request_methods = ("get", "options", "head", "post", "put", "patch", "delete") + for method_name in request_methods: + # Add a mock response for the specific method + responses.add( + getattr(responses, method_name.upper()), + "http://example.com/foo", + status=200, + ) + # Call the wrapper method + getattr(wrapper, method_name)("http://example.com/foo") + # Verify the request was made with the correct method + assert responses.calls[-1].request.method == method_name.upper() + + def test_wrapper_getattr_fallback(self): + # Make sure wrappers include everything in Advocate's `__init__.py` + wrapper = RequestsAPIWrapper(AddrValidator()) + self.assertIsNotNone(wrapper.PreparedRequest) + + def test_proxy_attempt_throws(self): + # Advocate can't do anything useful when you use a proxy, the proxy + # is the one that ultimately makes the connection + self.assertRaises( + NotImplementedError, + advocate.get, + "http://example.org/", + proxies={ + "http": "http://example.org:3128", + "https": "http://example.org:1080", + }, + ) + + @patch("advocate.addrvalidator.determine_local_addresses") + def test_connect_without_local_addresses(self, mock_determine_local_addresses): + fake_addresses = [ipaddress.ip_network("200.1.1.1")] + mock_determine_local_addresses.return_value = fake_addresses + + validator = permissive_validator(autodetect_local_addresses=True) + advocate.get("http://example.com/", validator=validator) + # Check that `is_ip_allowed` didn't make its own call to determine + # local addresses + mock_determine_local_addresses.assert_called_once_with() + mock_determine_local_addresses.reset_mock() + + validator = permissive_validator(autodetect_local_addresses=False) + advocate.get("http://example.com", validator=validator) + mock_determine_local_addresses.assert_not_called() + + +class AdvocateFuturesTest(unittest.TestCase): + def test_get(self): + sess = FuturesSession() + assert 200 == sess.get("http://example.org/").result().status_code + + def test_custom_validator(self): + validator = AddrValidator(hostname_blacklist={"example.org"}) + sess = FuturesSession(validator=validator) + self.assertRaises( + UnacceptableAddressException, + lambda: sess.get("http://example.org").result(), + ) + + def test_many_workers(self): + sess = FuturesSession(max_workers=50) + self.assertRaises( + UnacceptableAddressException, + lambda: sess.get("http://127.0.0.1:1/").result(), + ) + + def test_passing_session(self): + try: + FuturesSession(session=requests.Session()) + assert False + except NotImplementedError: + pass + + sess = FuturesSession() + try: + sess.session = requests.Session() + assert False + except NotImplementedError: + pass + + sess.session = advocate.Session() + + def test_advocate_wrapper_futures(self): + wrapper = RequestsAPIWrapper(validator=AddrValidator()) + local_validator = AddrValidator( + ip_whitelist={ + ipaddress.ip_network("127.0.0.1"), + } + ) + local_wrapper = RequestsAPIWrapper(validator=local_validator) + + with self.assertRaises(UnacceptableAddressException): + sess = wrapper.FuturesSession() + sess.get("http://127.0.0.1/").result() + + with self.assertRaises(Exception) as cm: + sess = local_wrapper.FuturesSession() + sess.get("http://127.0.0.1:1/").result() + # Check that we got a connection exception instead of a validation one + # This might be either exception depending on the requests version + self.assertRegexpMatches( + cm.exception.__class__.__name__, + r"\A(Connection|Protocol)Error", + ) + + with self.assertRaises(UnacceptableAddressException): + sess = wrapper.FuturesSession() + sess.get("http://localhost:1/").result() + with self.assertRaises(UnacceptableAddressException): + sess = wrapper.FuturesSession() + sess.get("https://localhost:1/").result() + + +if __name__ == "__main__": + unittest.main() diff --git a/backend/src/baserow/contrib/database/api/webhooks/views.py b/backend/src/baserow/contrib/database/api/webhooks/views.py index 270bcbcbe5..ef0ef9de3d 100755 --- a/backend/src/baserow/contrib/database/api/webhooks/views.py +++ b/backend/src/baserow/contrib/database/api/webhooks/views.py @@ -1,6 +1,5 @@ from django.db import transaction -from advocate import UnacceptableAddressException from drf_spectacular.openapi import OpenApiParameter, OpenApiTypes from drf_spectacular.utils import extend_schema from requests import RequestException @@ -8,6 +7,7 @@ from rest_framework.response import Response from rest_framework.views import APIView +from advocate import UnacceptableAddressException from baserow.api.decorators import map_exceptions, validate_body from baserow.api.errors import ERROR_USER_NOT_IN_GROUP from baserow.api.schemas import get_error_schema diff --git a/backend/src/baserow/contrib/database/data_sync/ical_data_sync_type.py b/backend/src/baserow/contrib/database/data_sync/ical_data_sync_type.py index 48d90ceb02..ce97bd4979 100644 --- a/backend/src/baserow/contrib/database/data_sync/ical_data_sync_type.py +++ b/backend/src/baserow/contrib/database/data_sync/ical_data_sync_type.py @@ -1,10 +1,10 @@ from typing import Any, Dict, List, Optional -import advocate -from advocate.exceptions import UnacceptableAddressException from icalendar import Calendar from requests.exceptions import RequestException +import advocate +from advocate.exceptions import UnacceptableAddressException from baserow.contrib.database.fields.models import DateField, TextField from baserow.core.utils import ChildProgressBuilder diff --git a/backend/src/baserow/contrib/database/webhooks/tasks.py b/backend/src/baserow/contrib/database/webhooks/tasks.py index 007b8f835d..247bded107 100644 --- a/backend/src/baserow/contrib/database/webhooks/tasks.py +++ b/backend/src/baserow/contrib/database/webhooks/tasks.py @@ -177,9 +177,10 @@ def call_webhook( def make_request_and_save_result( webhook, event_id, event_type, method, url, headers, payload ): - from advocate import UnacceptableAddressException from requests import RequestException + from advocate import UnacceptableAddressException + from .handler import WebhookHandler from .models import TableWebhookCall from .notification_types import WebhookDeactivatedNotificationType diff --git a/backend/src/baserow/contrib/integrations/core/service_types.py b/backend/src/baserow/contrib/integrations/core/service_types.py index c8d917c25d..3bb222ca85 100644 --- a/backend/src/baserow/contrib/integrations/core/service_types.py +++ b/backend/src/baserow/contrib/integrations/core/service_types.py @@ -15,13 +15,13 @@ from django.utils import timezone from django.utils.translation import gettext as _ -from advocate.connection import UnacceptableAddressException from dateutil.relativedelta import relativedelta from genson import SchemaBuilder from loguru import logger from requests import exceptions as request_exceptions from rest_framework import serializers +from advocate.connection import UnacceptableAddressException from baserow.config.celery import app as celery_app from baserow.contrib.automation.nodes.exceptions import ( AutomationNodeMisconfiguredService, diff --git a/backend/src/baserow/contrib/integrations/utils.py b/backend/src/baserow/contrib/integrations/utils.py index 6a05f74a64..4c9304d7e3 100644 --- a/backend/src/baserow/contrib/integrations/utils.py +++ b/backend/src/baserow/contrib/integrations/utils.py @@ -2,9 +2,10 @@ from django.conf import settings -import advocate import requests +import advocate + def get_http_request_function() -> Callable: """ diff --git a/backend/src/baserow/core/user_files/handler.py b/backend/src/baserow/core/user_files/handler.py index dc727d5806..e214db7481 100644 --- a/backend/src/baserow/core/user_files/handler.py +++ b/backend/src/baserow/core/user_files/handler.py @@ -16,11 +16,11 @@ from django.db.models import QuerySet from django.utils.http import parse_header_parameters -import advocate -from advocate.exceptions import UnacceptableAddressException from loguru import logger from requests.exceptions import RequestException +import advocate +from advocate.exceptions import UnacceptableAddressException from baserow.core.import_export.utils import file_chunk_generator from baserow.core.models import UserFile from baserow.core.storage import ( diff --git a/backend/src/baserow/test_utils/helpers.py b/backend/src/baserow/test_utils/helpers.py index eff0606710..21b57231d9 100644 --- a/backend/src/baserow/test_utils/helpers.py +++ b/backend/src/baserow/test_utils/helpers.py @@ -4,8 +4,6 @@ from contextlib import ExitStack, contextmanager from datetime import timedelta, timezone from decimal import Decimal -from ipaddress import ip_network -from socket import AF_INET, AF_INET6, IPPROTO_TCP, SOCK_STREAM from typing import Any, Dict, Generator, List, Optional, Type, Union from unittest.mock import patch @@ -600,20 +598,6 @@ def assert_serialized_rows_contain_same_values(row_1, row_2): ) -# The httpretty stub implementation of socket.getaddrinfo is incorrect and doesn't -# return an IP causing advocate to fail, instead we patch to fix this. -def stub_getaddrinfo(host, port, family=None, socktype=None, proto=None, flags=None): - try: - ip_network(host) - ip = host - except ValueError: - ip = "1.1.1.1" - return [ - (AF_INET, SOCK_STREAM, IPPROTO_TCP, host, (ip, port)), - (AF_INET6, SOCK_STREAM, IPPROTO_TCP, "", (ip, port)), - ] - - class AnyInt(int): """ A class that can be used to check if a value is an int. Useful in tests when diff --git a/backend/tests/baserow/api/user_files/test_user_files_views.py b/backend/tests/baserow/api/user_files/test_user_files_views.py index 6f3508fb07..2310d92d80 100644 --- a/backend/tests/baserow/api/user_files/test_user_files_views.py +++ b/backend/tests/baserow/api/user_files/test_user_files_views.py @@ -5,8 +5,8 @@ from django.core.files.uploadedfile import SimpleUploadedFile from django.shortcuts import reverse -import httpretty as httpretty import pytest +import responses from freezegun import freeze_time from PIL import Image from rest_framework.status import ( @@ -289,7 +289,7 @@ def test_upload_file_with_token_auth(api_client, data_fixture, tmpdir): @pytest.mark.django_db -@httpretty.activate(verbose=True, allow_net_connect=False) +@responses.activate def test_upload_file_via_url_with_jwt_auth(api_client, data_fixture, tmpdir): user, token = data_fixture.create_user_and_token( email="test@test.nl", password="password", first_name="Test1" @@ -311,8 +311,8 @@ def test_upload_file_via_url_with_jwt_auth(api_client, data_fixture, tmpdir): assert response.status_code == HTTP_400_BAD_REQUEST assert response.json()["error"] == "ERROR_REQUEST_BODY_VALIDATION" - httpretty.register_uri( - httpretty.GET, + responses.add( + responses.GET, "https://baserow.io/test2.txt", status=404, ) @@ -336,8 +336,8 @@ def test_upload_file_via_url_with_jwt_auth(api_client, data_fixture, tmpdir): old_limit = settings.BASEROW_FILE_UPLOAD_SIZE_LIMIT_MB settings.BASEROW_FILE_UPLOAD_SIZE_LIMIT_MB = 6 - httpretty.register_uri( - httpretty.GET, + responses.add( + responses.GET, "http://localhost/test.txt", body="Hello World", status=200, @@ -353,11 +353,10 @@ def test_upload_file_via_url_with_jwt_auth(api_client, data_fixture, tmpdir): # If the content length is not specified then when streaming down the file we will # check the file size. - httpretty.register_uri( - httpretty.GET, + responses.add( + responses.GET, "http://localhost/test2.txt", body="Hello World", - forcing_headers={"Content-Length": None}, status=200, content_type="text/plain", ) @@ -398,7 +397,7 @@ def test_upload_file_via_url_with_jwt_auth(api_client, data_fixture, tmpdir): @pytest.mark.django_db -@httpretty.activate(verbose=True, allow_net_connect=False) +@responses.activate def test_upload_file_via_url_with_token_auth(api_client, data_fixture, tmpdir): user, jwt_token = data_fixture.create_user_and_token( email="test@test.nl", password="password", first_name="Test1" @@ -422,8 +421,8 @@ def test_upload_file_via_url_with_token_auth(api_client, data_fixture, tmpdir): assert response.status_code == HTTP_400_BAD_REQUEST assert response.json()["error"] == "ERROR_REQUEST_BODY_VALIDATION" - httpretty.register_uri( - httpretty.GET, + responses.add( + responses.GET, "https://baserow.io/test2.txt", status=404, ) @@ -447,8 +446,8 @@ def test_upload_file_via_url_with_token_auth(api_client, data_fixture, tmpdir): old_limit = settings.BASEROW_FILE_UPLOAD_SIZE_LIMIT_MB settings.BASEROW_FILE_UPLOAD_SIZE_LIMIT_MB = 6 - httpretty.register_uri( - httpretty.GET, + responses.add( + responses.GET, "http://localhost/test.txt", body="Hello World", status=200, @@ -464,11 +463,10 @@ def test_upload_file_via_url_with_token_auth(api_client, data_fixture, tmpdir): # If the content length is not specified then when streaming down the file we will # check the file size. - httpretty.register_uri( - httpretty.GET, + responses.add( + responses.GET, "http://localhost/test2.txt", body="Hello World", - forcing_headers={"Content-Length": None}, status=200, content_type="text/plain", ) diff --git a/backend/tests/baserow/contrib/database/webhooks/test_webhook_tasks.py b/backend/tests/baserow/contrib/database/webhooks/test_webhook_tasks.py index e8dc9b81b9..3891fbb32f 100644 --- a/backend/tests/baserow/contrib/database/webhooks/test_webhook_tasks.py +++ b/backend/tests/baserow/contrib/database/webhooks/test_webhook_tasks.py @@ -3,7 +3,6 @@ from django.db import transaction from django.test import override_settings -import httpretty import pytest import responses from celery.exceptions import Retry @@ -22,7 +21,6 @@ from baserow.core.models import WorkspaceUser from baserow.core.notifications.models import Notification from baserow.core.redis import WebhookRedisQueue -from baserow.test_utils.helpers import stub_getaddrinfo @pytest.mark.django_db(transaction=True) @@ -293,15 +291,11 @@ def test_call_webhook_next_item_scheduled(mock_schedule, data_fixture): BASEROW_WEBHOOKS_MAX_RETRIES_PER_CALL=0, BASEROW_WEBHOOKS_MAX_CONSECUTIVE_TRIGGER_FAILURES=0, ) -@httpretty.activate(verbose=True, allow_net_connect=False) @patch("baserow.contrib.database.webhooks.tasks.RedisQueue", WebhookRedisQueue) @patch("baserow.contrib.database.webhooks.tasks.cache", MagicMock()) -@patch("socket.getaddrinfo", wraps=stub_getaddrinfo) def test_cant_call_webhook_to_localhost_when_private_addresses_not_allowed( - patched_getaddrinfo, data_fixture, ): - httpretty.register_uri(httpretty.POST, "http://127.0.0.1", status=200) webhook = data_fixture.create_table_webhook() assert webhook.active diff --git a/backend/tests/baserow/contrib/database/webhooks/test_webhook_validators.py b/backend/tests/baserow/contrib/database/webhooks/test_webhook_validators.py index d562a9a27b..82cef2ab4a 100644 --- a/backend/tests/baserow/contrib/database/webhooks/test_webhook_validators.py +++ b/backend/tests/baserow/contrib/database/webhooks/test_webhook_validators.py @@ -1,26 +1,37 @@ import re from ipaddress import ip_network -from unittest.mock import patch from django.core.exceptions import ValidationError from django.test import override_settings -import httpretty as httpretty import pytest +import advocate.connection as advocate_connection from baserow.contrib.database.webhooks.validators import url_validator -from baserow.test_utils.helpers import stub_getaddrinfo URL_BLACKLIST_ONLY_ALLOWING_GOOGLE_WEBHOOKS = re.compile(r"(?!(www\.)?google\.com).*") -@httpretty.activate(verbose=True, allow_net_connect=False) -@patch("socket.getaddrinfo", wraps=stub_getaddrinfo) -def test_advocate_blocks_internal_address(mock): - httpretty.register_uri(httpretty.GET, "https://1.1.1.1/", status=200) - httpretty.register_uri(httpretty.GET, "https://2.2.2.2/", status=200) - httpretty.register_uri(httpretty.GET, "http://127.0.0.1/", status=200) +class _DummySocket: + def settimeout(self, *args, **kwargs): + pass + def connect(self, *args, **kwargs): + # Never do real outbound network in unit tests. + return None + + def close(self): + pass + + +@pytest.fixture(autouse=True) +def _disable_real_network(monkeypatch): + monkeypatch.setattr( + advocate_connection.socket, "socket", lambda *args, **kwargs: _DummySocket() + ) + + +def test_advocate_blocks_internal_address(): # This request should go through url_validator("https://1.1.1.1/") @@ -29,13 +40,7 @@ def test_advocate_blocks_internal_address(mock): url_validator("http://127.0.0.1/") -@httpretty.activate(verbose=True, allow_net_connect=False) -@patch("socket.getaddrinfo", wraps=stub_getaddrinfo) -def test_advocate_blocks_invalid_urls(mock): - httpretty.register_uri(httpretty.GET, "https://1.1.1.1/", status=200) - httpretty.register_uri(httpretty.GET, "https://2.2.2.2/", status=200) - httpretty.register_uri(httpretty.GET, "http://127.0.0.1/", status=200) - +def test_advocate_blocks_invalid_urls(): # This request should go through url_validator("https://1.1.1.1/") @@ -48,13 +53,8 @@ def test_advocate_blocks_invalid_urls(mock): assert exec_info.value.code == "invalid_url" -@httpretty.activate(verbose=True, allow_net_connect=False) @override_settings(BASEROW_WEBHOOKS_IP_WHITELIST=[ip_network("127.0.0.1/32")]) -@patch("socket.getaddrinfo", wraps=stub_getaddrinfo) -def test_advocate_whitelist_rules(mock): - httpretty.register_uri(httpretty.GET, "http://127.0.0.1/", status=200) - httpretty.register_uri(httpretty.GET, "http://10.0.0.1/", status=200) - +def test_advocate_whitelist_rules(): # This request should go through url_validator("http://127.0.0.1/") @@ -64,14 +64,8 @@ def test_advocate_whitelist_rules(mock): assert exec_info.value.code == "invalid_url" -@httpretty.activate(verbose=True, allow_net_connect=False) @override_settings(BASEROW_WEBHOOKS_IP_BLACKLIST=[ip_network("1.1.1.1/32")]) -@patch("socket.getaddrinfo", wraps=stub_getaddrinfo) -def test_advocate_blacklist_rules(mock): - httpretty.register_uri(httpretty.GET, "https://1.1.1.1", status=200) - httpretty.register_uri(httpretty.GET, "http://127.0.0.1/", status=200) - httpretty.register_uri(httpretty.GET, "https://2.2.2.2/", status=200) - +def test_advocate_blacklist_rules(): # This request should not go through with pytest.raises(ValidationError, match="Invalid URL") as exec_info: url_validator("https://1.1.1.1/") @@ -83,21 +77,13 @@ def test_advocate_blacklist_rules(mock): assert exec_info.value.code == "invalid_url" # This request should still go through - url_validator("https://2.2.2.2/") + url_validator("http://2.2.2.2/") -@httpretty.activate(verbose=True, allow_net_connect=False) @override_settings( BASEROW_WEBHOOKS_URL_REGEX_BLACKLIST=[re.compile(r"(?:www\.?)?google.com")] ) -@patch("socket.getaddrinfo", wraps=stub_getaddrinfo) -def test_hostname_blacklist_rules(patched_addr_info): - httpretty.register_uri(httpretty.GET, "https://google.com", status=200) - httpretty.register_uri(httpretty.GET, "http://1.1.1.1", status=200) - - # The httpretty stub implemenation of socket.getaddrinfo is incorrect and doesn't - # return an IP causing advocate to fail, instead we patch to fix this. - +def test_hostname_blacklist_rules(): # This request should not go through with pytest.raises(ValidationError, match="Invalid URL") as exec_info: url_validator("https://www.google.com/") @@ -107,17 +93,10 @@ def test_hostname_blacklist_rules(patched_addr_info): url_validator("https://www.otherdomain.com") -@httpretty.activate(verbose=True, allow_net_connect=False) @override_settings( BASEROW_WEBHOOKS_URL_REGEX_BLACKLIST=[URL_BLACKLIST_ONLY_ALLOWING_GOOGLE_WEBHOOKS] ) -@patch("socket.getaddrinfo", wraps=stub_getaddrinfo) -def test_hostname_blacklist_rules_only_allow_one_host(patched_addr_info): - httpretty.register_uri(httpretty.GET, "https://google.com", status=200) - httpretty.register_uri(httpretty.GET, "http://google.com", status=200) - httpretty.register_uri(httpretty.GET, "http://1.1.1.1", status=200) - httpretty.register_uri(httpretty.GET, "https://1.1.1.1", status=200) - +def test_hostname_blacklist_rules_only_allow_one_host(): url_validator("https://www.google.com/") url_validator("https://google.com/") @@ -130,17 +109,11 @@ def test_hostname_blacklist_rules_only_allow_one_host(patched_addr_info): assert exec_info.value.code == "invalid_url" -@httpretty.activate(verbose=True, allow_net_connect=False) @override_settings( BASEROW_WEBHOOKS_IP_BLACKLIST=[ip_network("1.0.0.0/8")], BASEROW_WEBHOOKS_IP_WHITELIST=[ip_network("1.1.1.1/32")], ) def test_advocate_combination_of_whitelist_blacklist_rules(): - httpretty.register_uri(httpretty.GET, "https://1.1.1.1", status=200) - httpretty.register_uri(httpretty.GET, "https://1.1.1.2", status=200) - httpretty.register_uri(httpretty.GET, "http://127.0.0.1/", status=200) - httpretty.register_uri(httpretty.GET, "https://2.2.2.2/", status=200) - url_validator("https://1.1.1.1/") with pytest.raises(ValidationError, match="Invalid URL") as exec_info: @@ -156,21 +129,12 @@ def test_advocate_combination_of_whitelist_blacklist_rules(): url_validator("https://2.2.2.2/") -@httpretty.activate(verbose=True, allow_net_connect=False) @override_settings( BASEROW_WEBHOOKS_URL_REGEX_BLACKLIST=[URL_BLACKLIST_ONLY_ALLOWING_GOOGLE_WEBHOOKS], BASEROW_WEBHOOKS_IP_BLACKLIST=[ip_network("1.0.0.0/8")], BASEROW_WEBHOOKS_IP_WHITELIST=[ip_network("1.1.1.1/32")], ) -@patch("socket.getaddrinfo", wraps=stub_getaddrinfo) -def test_advocate_hostname_blacklist_overrides_ip_lists( - mock, -): - httpretty.register_uri(httpretty.GET, "https://1.1.1.1", status=200) - httpretty.register_uri(httpretty.GET, "https://1.1.1.2", status=200) - httpretty.register_uri(httpretty.GET, "http://127.0.0.1/", status=200) - httpretty.register_uri(httpretty.GET, "https://2.2.2.2/", status=200) - +def test_advocate_hostname_blacklist_overrides_ip_lists(): with pytest.raises(ValidationError, match="Invalid URL") as exec_info: url_validator("https://1.1.1.1/") assert exec_info.value.code == "invalid_url" diff --git a/backend/tests/baserow/core/user_file/test_user_file_handler.py b/backend/tests/baserow/core/user_file/test_user_file_handler.py index 036792f514..b4b0a53a89 100644 --- a/backend/tests/baserow/core/user_file/test_user_file_handler.py +++ b/backend/tests/baserow/core/user_file/test_user_file_handler.py @@ -9,8 +9,8 @@ from django.core.files.base import ContentFile from django.core.files.storage import FileSystemStorage -import httpretty import pytest +import responses import zipstream from freezegun import freeze_time from PIL import Image @@ -287,23 +287,23 @@ def test_upload_user_file_with_unsupported_image_format( @pytest.mark.django_db -@httpretty.activate(verbose=True, allow_net_connect=False) +@responses.activate def test_upload_user_file_by_url(data_fixture, tmpdir): user = data_fixture.create_user() storage = FileSystemStorage(location=str(tmpdir), base_url="http://localhost") handler = UserFileHandler() - httpretty.register_uri( - httpretty.GET, + responses.add( + responses.GET, "https://baserow.io/test.txt", body=b"Hello World", status=200, content_type="text/plain", ) - httpretty.register_uri( - httpretty.GET, + responses.add( + responses.GET, "https://baserow.io/not-found.pdf", status=404, ) @@ -365,7 +365,7 @@ def test_upload_user_file_by_url_within_private_network(data_fixture, tmpdir): @pytest.mark.django_db -@httpretty.activate(verbose=True, allow_net_connect=False) +@responses.activate def test_upload_user_file_by_url_with_querystring(data_fixture, tmpdir) -> None: user = data_fixture.create_user() @@ -374,8 +374,8 @@ def test_upload_user_file_by_url_with_querystring(data_fixture, tmpdir) -> None: remote_file = "https://baserow.io/test.txt?utm_source=google&utm_medium=email&utm_campaign=fall" - httpretty.register_uri( - httpretty.GET, + responses.add( + responses.GET, remote_file, body=b"Hello World", status=200, @@ -399,7 +399,7 @@ def test_upload_user_file_by_url_with_querystring(data_fixture, tmpdir) -> None: @pytest.mark.django_db -@httpretty.activate(verbose=True, allow_net_connect=False) +@responses.activate def test_upload_user_file_by_url_with_image_without_extension_with_wrong_content_type( data_fixture, tmpdir ) -> None: @@ -410,8 +410,8 @@ def test_upload_user_file_by_url_with_image_without_extension_with_wrong_content remote_file = "https://baserow.io/image-without-url" - httpretty.register_uri( - httpretty.GET, + responses.add( + responses.GET, remote_file, body=bytes( [ @@ -494,7 +494,7 @@ def test_upload_user_file_by_url_with_image_without_extension_with_wrong_content @pytest.mark.django_db -@httpretty.activate(verbose=True, allow_net_connect=False) +@responses.activate def test_upload_user_file_by_url_with_slash(data_fixture, tmpdir) -> None: user = data_fixture.create_user() @@ -503,8 +503,8 @@ def test_upload_user_file_by_url_with_slash(data_fixture, tmpdir) -> None: remote_file = "https://baserow.io/test.txt/" - httpretty.register_uri( - httpretty.GET, + responses.add( + responses.GET, remote_file, body=b"Hello World", status=200, @@ -528,7 +528,7 @@ def test_upload_user_file_by_url_with_slash(data_fixture, tmpdir) -> None: @pytest.mark.django_db -@httpretty.activate(verbose=True, allow_net_connect=False) +@responses.activate def test_upload_user_file_by_url_without_path(data_fixture, tmpdir) -> None: user = data_fixture.create_user() @@ -537,8 +537,8 @@ def test_upload_user_file_by_url_without_path(data_fixture, tmpdir) -> None: remote_file = "https://baserow.io/" - httpretty.register_uri( - httpretty.GET, + responses.add( + responses.GET, remote_file, body=b"Hello World", status=200, @@ -585,7 +585,7 @@ def test_upload_user_file_by_url_without_path(data_fixture, tmpdir) -> None: ], ) @pytest.mark.django_db -@httpretty.activate(verbose=True, allow_net_connect=False) +@responses.activate def test_upload_user_file_by_url_with_invalid_paths( data_fixture, tmpdir, uri, remote_file ) -> None: @@ -594,9 +594,9 @@ def test_upload_user_file_by_url_with_invalid_paths( storage = FileSystemStorage(location=str(tmpdir), base_url="http://localhost") handler = UserFileHandler() - httpretty.register_uri( - httpretty.GET, - uri=uri, + responses.add( + responses.GET, + url=uri, body=b"Hello World", status=200, content_type="text/plain", @@ -608,7 +608,7 @@ def test_upload_user_file_by_url_with_invalid_paths( @pytest.mark.django_db -@httpretty.activate(verbose=True, allow_net_connect=False) +@responses.activate def test_upload_user_file_by_url_with_invalid_content_type( data_fixture, tmpdir ) -> None: @@ -619,8 +619,8 @@ def test_upload_user_file_by_url_with_invalid_content_type( remote_file = "https://baserow.io//" - httpretty.register_uri( - httpretty.GET, + responses.add( + responses.GET, re.compile(r"https://baserow.io.*"), body=b"Hello World", status=200, diff --git a/backend/uv.lock b/backend/uv.lock index 5dd221b25a..dfacf7d4bd 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -17,24 +17,6 @@ members = [ "baserow-premium", ] -[[package]] -name = "advocate" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ndg-httpsclient", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "netifaces", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "pyasn1", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "pyopenssl", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "six", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "urllib3", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fe/5e/3103b1c63c6cc2a0cdbb1c1e399f700501e1001675c700d39364f9f8df28/advocate-1.0.0.tar.gz", hash = "sha256:1bf1170e41334279996580329c594e017540ab0eaf7a152323e743f0a85a353d", size = 39981, upload-time = "2020-07-14T15:34:11.396Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/48/ab74ba882cf6f5080ed3fc2f353166930376ac4484d784ceb4dfd5f19c78/advocate-1.0.0-py2.py3-none-any.whl", hash = "sha256:e8b340e49fadc0e416fbc9e81ef52d74858ccad16357dabde6cf9d99a7407d70", size = 34179, upload-time = "2020-07-14T15:34:10.013Z" }, -] - [[package]] name = "amqp" version = "5.3.1" @@ -222,7 +204,6 @@ wheels = [ name = "baserow" source = { editable = "." } dependencies = [ - { name = "advocate", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "anthropic", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "antlr4-python3-runtime", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "asgiref", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -258,6 +239,7 @@ dependencies = [ { name = "loguru", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "mcp", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "mistralai", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "netifaces", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "ollama", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "openai", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "openpyxl", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -293,6 +275,7 @@ dependencies = [ { name = "redis", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "regex", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "requests-futures", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "requests-oauthlib", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "rich", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "sentry-sdk", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -324,7 +307,6 @@ dev = [ { name = "fakeredis", extra = ["lua"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "freezegun", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "graphviz", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "httpretty", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "ipdb", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "ipython", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "mypy", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -355,7 +337,6 @@ dev = [ [package.metadata] requires-dist = [ - { name = "advocate", specifier = "==1.0.0" }, { name = "anthropic", specifier = "==0.77.0" }, { name = "antlr4-python3-runtime", specifier = "==4.9.3" }, { name = "asgiref", specifier = "==3.11.0" }, @@ -393,6 +374,7 @@ requires-dist = [ { name = "loguru", specifier = "==0.7.3" }, { name = "mcp", specifier = "==1.26.0" }, { name = "mistralai", specifier = "==1.1.0" }, + { name = "netifaces", specifier = "==0.11.0" }, { name = "ollama", specifier = "==0.6.1" }, { name = "openai", specifier = "==2.14.0" }, { name = "openpyxl", specifier = "==3.1.5" }, @@ -428,6 +410,7 @@ requires-dist = [ { name = "redis", specifier = "==6.4.0" }, { name = "regex", specifier = "==2025.11.3" }, { name = "requests", specifier = "==2.32.5" }, + { name = "requests-futures", specifier = ">=1.0.2" }, { name = "requests-oauthlib", specifier = "==2.0.0" }, { name = "rich", specifier = "==14.3.2" }, { name = "sentry-sdk", specifier = "==2.48.0" }, @@ -459,7 +442,6 @@ dev = [ { name = "fakeredis", extras = ["lua"], specifier = "==2.33.0" }, { name = "freezegun", specifier = "==1.5.5" }, { name = "graphviz", specifier = "==0.21" }, - { name = "httpretty", specifier = "==1.1.4" }, { name = "ipdb" }, { name = "ipython" }, { name = "mypy", specifier = "==1.19.1" }, @@ -1435,12 +1417,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] -[[package]] -name = "httpretty" -version = "1.1.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6e/19/850b7ed736319d0c4088581f4fc34f707ef14461947284026664641e16d4/httpretty-1.1.4.tar.gz", hash = "sha256:20de0e5dd5a18292d36d928cc3d6e52f8b2ac73daec40d41eb62dee154933b68", size = 442389, upload-time = "2021-08-16T19:35:31.4Z" } - [[package]] name = "httptools" version = "0.7.1" @@ -2068,19 +2044,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] -[[package]] -name = "ndg-httpsclient" -version = "0.5.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyasn1", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "pyopenssl", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b9/f8/8f49278581cb848fb710a362bfc3028262a82044167684fb64ad068dbf92/ndg_httpsclient-0.5.1.tar.gz", hash = "sha256:d72faed0376ab039736c2ba12e30695e2788c4aa569c9c3e3d72131de2592210", size = 26665, upload-time = "2018-07-23T16:02:43.96Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/67/c2f508c00ed2a6911541494504b7cac16fe0b0473912568df65fd1801132/ndg_httpsclient-0.5.1-py3-none-any.whl", hash = "sha256:dd174c11d971b6244a891f7be2b32ca9853d3797a72edb34fa5d7b07d8fff7d4", size = 34042, upload-time = "2018-07-23T16:04:46.553Z" }, -] - [[package]] name = "netifaces" version = "0.11.0" @@ -3271,6 +3234,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] +[[package]] +name = "requests-futures" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/f8/175b823241536ba09da033850d66194c372c65c38804847ac9cef0239542/requests_futures-1.0.2.tar.gz", hash = "sha256:6b7eb57940336e800faebc3dab506360edec9478f7b22dc570858ad3aa7458da", size = 10356, upload-time = "2024-11-15T22:14:51.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/23/7c1096731c15c83826cb0dd42078b561a838aed44c36f370aeb815168106/requests_futures-1.0.2-py2.py3-none-any.whl", hash = "sha256:a3534af7c2bf670cd7aa730716e9e7d4386497554f87792be7514063b8912897", size = 7671, upload-time = "2024-11-15T22:14:50.255Z" }, +] + [[package]] name = "requests-oauthlib" version = "2.0.0" @@ -3805,11 +3780,11 @@ wheels = [ [[package]] name = "urllib3" -version = "1.26.20" +version = "2.6.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/e8/6ff5e6bc22095cfc59b6ea711b687e2b7ed4bdb373f7eeec370a97d7392f/urllib3-1.26.20.tar.gz", hash = "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32", size = 307380, upload-time = "2024-08-29T15:43:11.37Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/cf/8435d5a7159e2a9c83a95896ed596f68cf798005fe107cc655b5c5c14704/urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e", size = 144225, upload-time = "2024-08-29T15:43:08.921Z" }, + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] [[package]] diff --git a/enterprise/backend/src/baserow_enterprise/data_sync/jira_issues_data_sync.py b/enterprise/backend/src/baserow_enterprise/data_sync/jira_issues_data_sync.py index 5bdaeb59a3..2d717e7c43 100644 --- a/enterprise/backend/src/baserow_enterprise/data_sync/jira_issues_data_sync.py +++ b/enterprise/backend/src/baserow_enterprise/data_sync/jira_issues_data_sync.py @@ -2,11 +2,11 @@ from datetime import datetime from typing import Any, Dict, List, Optional -import advocate -from advocate import UnacceptableAddressException from requests.auth import HTTPBasicAuth from requests.exceptions import JSONDecodeError, RequestException +import advocate +from advocate import UnacceptableAddressException from baserow.contrib.database.data_sync.exceptions import SyncError from baserow.contrib.database.data_sync.registries import DataSyncProperty, DataSyncType from baserow.contrib.database.data_sync.utils import compare_date diff --git a/enterprise/backend/website_export.csv b/enterprise/backend/website_export.csv index 8d39514381..7af7a2d485 100644 --- a/enterprise/backend/website_export.csv +++ b/enterprise/backend/website_export.csv @@ -154,6 +154,12 @@ Views are different ways to display and interact with a single table's data with Baserow looks similar to spreadsheets but works differently. Spreadsheets are files; Baserow is a relational database. This means you can create relationships between tables, enforce data types, build applications on top of your data, and collaborate with sophisticated permission controls. +### Can I use SQL in Baserow? + +You cannot write SQL queries directly within the Baserow interface. Baserow is designed to replace manual SQL coding with visual tools; you use Views, Filters, and Sorts to query your data, and the Formula field for calculations. + +However, you can use the API to programmatically filter and retrieve data. + ## What's next on your journey Choose your next step based on what you want to accomplish: @@ -246,7 +252,7 @@ Still need help? If you're looking for something else, please feel free to make [40]: /user-docs/baserow-basics [41]: /user-docs/set-up-baserow [42]: /user-docs/learn-baserow-basic-concepts - [43]: /user-docs/baserow-keyboard-shortcuts",,baserow_user_docs,https://baserow.io/user-docs/how-to-get-started-with-baserow + [43]: /user-docs/baserow-keyboard-shortcuts",getting started,baserow_user_docs,https://baserow.io/user-docs/how-to-get-started-with-baserow 2,User guide index,index,Baserow table of contents,"# Baserow user guide index Welcome to the Baserow user documentation. Baserow empowers you to create custom databases and applications without writing code. Whether you’re a seasoned professional or just starting, Baserow’s user-friendly interface makes it easy to organize, manage, and analyze your data. @@ -7965,6 +7971,11 @@ Learn more: [How to transform spreadsheets into databases](https://baserow.io/bl Baserow databases are designed to scale efficiently. Performance depends on your hosting plan, but typical databases can handle hundreds of thousands of rows. Use filters and views to work with subsets of data, and consider archiving old records to maintain performance. Enterprise plans offer enhanced performance for very large datasets. +### How can I restore previous data changes in Baserow? + +You can review all row changes in the Audit log available in the left sidebar. This shows which rows were updated and by whom. +If you are self-hosting, all data is stored in your PostgreSQL database. If you have a database backup, you can restore it to recover the earlier state. Be aware that restoring a backup will revert all data changes made after the backup was created, not just the affected rows. + ## Related content Now that you understand Baserow databases, explore these topics: @@ -8003,7 +8014,7 @@ Still need help? If you're looking for something else, please feel free to make [3]: https://baserow.io/templates/project-management [4]: https://baserow.io/templates/car-dealership-inventory [5]: https://baserow.io/templates/content-scheduling-manager - [6]: https://baserow.io/templates/business-conference-event",,baserow_user_docs,https://baserow.io/user-docs/intro-to-databases + [6]: https://baserow.io/templates/business-conference-event",database,baserow_user_docs,https://baserow.io/user-docs/intro-to-databases 50,Workspaces overview,intro-to-workspaces,Baserow workspaces overview,"# Introduction to workspaces in Baserow Workspaces are the top-level organizational containers in Baserow where teams collaborate on databases, applications, dashboards, and automations. Each workspace operates independently with its own members, permissions, and content that cannot be shared across workspaces. @@ -9087,6 +9098,12 @@ Baserow offers two authentication methods for API access: > **[Enterprise license][2]:** Only workspace-level admins and builders can create database tokens, ensuring proper access control for sensitive operations. +Database tokens only allow reading or manipulating the data of a table. If you want to create, update or delete the structure of the table, you need a JWT token. + +This token can be generated by calling `https://api.baserow.io/api/redoc/#tag/User/operation/token_auth` and passing the username and password of a user who has Admin or Builder rights on the table. + +The token itself is added to the header in the following format `Authorization: JWT ` + ## How to create a database token 1. **Access token settings** @@ -9538,7 +9555,7 @@ Still need help? If you're looking for something else, please don't hesitate to [13]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/1da2aca0-207b-4174-a01f-2f21663ef8db/Screenshot_2022-10-18_at_16.45.17.png [14]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/492da8b5-7298-4aaa-ad84-a5d8ee0642d9/Screenshot_2022-10-20_at_06.33.33.png [15]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/9abbd422-7e4f-413e-a75f-6b07f9d1316d/Screenshot_2022-10-18_at_16.49.37.png - [16]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/d6521859-d239-447e-ac82-d93c2b52f415/Screenshot_2022-10-18_at_17.06.48.png",,baserow_user_docs,https://baserow.io/user-docs/pipedream + [16]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/d6521859-d239-447e-ac82-d93c2b52f415/Screenshot_2022-10-18_at_17.06.48.png",integrations,baserow_user_docs,https://baserow.io/user-docs/pipedream 92,Single Sign On,single-sign-on-sso-overview,Baserow Single Sign-On (SSO) explained,"# Single Sign-On (SSO) Overview Single Sign-On (SSO) allows users to access Baserow using their existing corporate credentials, eliminating the need to manage separate usernames and passwords. @@ -11334,7 +11351,7 @@ Still need help? If you're looking for something else, please feel free to make [2]: https://baserow.io/user-docs/database-and-table-id [3]: https://baserow.io/api-docs [4]: https://baserow.io/user-docs/set-up-baserow - [5]: https://baserow.io/user-docs/database-api",,baserow_user_docs,https://baserow.io/user-docs/n8n + [5]: https://baserow.io/user-docs/database-api",integrations,baserow_user_docs,https://baserow.io/user-docs/n8n 105,Set up Baserow environment,set-up-baserow,Set up your own Baserow environment,"# Deploy Baserow: Choose your hosting option Baserow offers two deployment options: Baserow Cloud for instant setup with managed hosting, or Self-hosted for complete control over your infrastructure. Both environments offer the same core features with free and paid plans available. @@ -13965,6 +13982,11 @@ Audit logging has minimal performance impact on modern systems. Baserow logs eve No. Only instance administrators see instance-level logs. Workspace administrators see workspace-level logs for their workspaces only. Regular workspace members (builders, editors, commenters, viewers) cannot access audit logs. +### How can I restore previous data changes in Baserow? + +You can review all row changes in the Audit log available in the left sidebar. This shows which rows were updated and by whom. +If you are self-hosting, all data is stored in your PostgreSQL database. If you have a database backup, you can restore it to recover the earlier state. Be aware that restoring a backup will revert all data changes made after the backup was created, not just the affected rows. + ## Related resources ### Audit and monitoring @@ -18556,7 +18578,10 @@ Not directly. Snapshots are stored within Baserow's infrastructure. For offline Generally, there's no hard technical limit, but storage costs and management complexity increase with many snapshots. A common practice is keeping daily snapshots for a week, weekly for a month, and monthly for a year. +### How can I restore previous data changes in Baserow? +You can review all row changes in the Audit log available in the left sidebar. This shows which rows were updated and by whom. +If you are self-hosting, all data is stored in your PostgreSQL database. If you have a database backup, you can restore it to recover the earlier state. Be aware that restoring a backup will revert all data changes made after the backup was created, not just the affected rows. ## Related resources @@ -20426,13 +20451,65 @@ By default, this field will track changes made to *any* editable field in the ro You can choose to restrict the field so that it only displays the most recent time a particular field was modified. -## Track the last modified time on computed fields +## Tracking modification times on computed fields + +The **Last modified** field type and `date` functions in formulas allow you to track when a record was last edited. However, it is important to understand that these tools only track **direct user actions**. + +### What triggers a ""Last Modified"" update? + +* ✅ Typing manually into a cell (Text, Number, etc.). +* ✅ Selecting an option (Single Select, Multiple Select). +* ✅ **Adding or removing** a row from a Link-to-table field. + +### What does not trigger an update? + +* ❌ **Formulas:** If a formula recalculates (e.g., `today()` changes to tomorrow, or `{Price} * {Quantity}` updates), the Last Modified field will **not** change. +* ❌ **Linked Data Changes:** If you modify data inside a linked record (e.g., changing a generic ""Status"" field in a linked *Tasks* table), the ""Last Modified"" timestamp in the current table will **not** update. + +> **Note:** The ""Last modified"" field tracks *who* touched the row and *when*. Passive changes calculated by Baserow in the background do not count as user edits. -The [date functions in formulas][11] can be used to return the date and time of the most recent user modification. Using the date functions in formulas, you can specify one or more field names to track modification and return the date and time of the most recent change made to any of the specified fields. -Both the formula function and the Last modified field type reflect recent changes to editable fields by a user. They do not capture changes made automatically in any fields, such as formula fields, where the user does not directly modify the cell values but rather uses Baserow to compute the values. -If you add or remove a row from a [Link-to-table field][9], the Last modified data will change. However, if the data linked to the [Link-to-table field][9] field changes, the latest modified time will not change. Although the connected row's values in the other table have changed, you haven't altered which row you are linking to. Changing the [Link-to-table field][9] row's fields that are stored in a different table won't affect the Last modified value. +### Workaround: Tracking changes in linked records + +If you need to see the last modified time of a row **or** any of its linked data, you cannot rely on a single ""Last modified"" field. Instead, you can combine a **Lookup field** with a **Formula**. + +**Scenario:** You have a **Projects** table linked to a **Tasks** table. You want the *Project* to show as ""Updated"" whenever a *Task* is modified. + +#### Step 1: Track time in the source table + +1. Go to the linked table (e.g., **Tasks**). +2. Create a new field with the type **Last modified**. +3. Name it `Task Last Modified`. + +#### Step 2: Expose that time in your main table + +1. Go to your main table (e.g., **Projects**). +2. Create a **Lookup** field. +3. Point it to the **Tasks** table and select the `Task Last Modified` field you just created. +4. Name this field `Lookup: Task Modified`. + +#### Step 3: Compare the dates with a formula + +Create a new **Formula** field in the **Projects** table to compare the project's local edit time against the task's edit time. This returns the most recent date. + +**The Formula:** + +```javascript +if( + date_diff('s', field('Last modified'), field('Lookup: Task Modified')) < 0, + field('Lookup: Task Modified'), + field('Last modified') +) + +``` + +**How this works:** + +* The `date_diff` function calculates the difference in seconds between your local edit and the linked edit. +* The `if` statement checks which date is more recent. +* The formula displays whichever date is newest, giving you a ""True"" last modified timestamp. + @@ -20482,7 +20559,7 @@ Still need help? If you're looking for something else, please feel free to make [8]: /user-docs/rollup-field [9]: /user-docs/link-to-table-field [10]: /user-docs/working-with-timezones - [11]: /user-docs/understanding-formulas",,baserow_user_docs,https://baserow.io/user-docs/last-modified-field + [11]: /user-docs/understanding-formulas",field,baserow_user_docs,https://baserow.io/user-docs/last-modified-field 161,Password field,password-field,Password field in Baserow table,"# Working with the Password field The **Password field** securely hashes and stores user passwords. The Password field can also be used for authentication in the Baserow [Application Builder][1]. @@ -21165,6 +21242,12 @@ No. Baserow automatically provisions and renews SSL certificates (HTTPS) for you ### Can I use a ""naked"" domain (e.g., example.com)? Yes, but you must ensure your DNS provider supports `ALIAS` or `ANAME` records at the root level (or CNAME flattening). Standard `CNAME` records technically cannot be placed at the root (example.com), only on subdomains (www.example.com). +### Why can’t I publish my self-hosted Baserow app using a free Baserow subdomain? + +If Baserow is self-hosted, the free Baserow subdomain option appears when the `BASEROW_BUILDER_DOMAINS` variable is set in your .env file. When this variable is missing, only the Custom Domain option is shown. After setting it and restarting Baserow, the subdomain option becomes available. + +You can read more on how to [configure your Baserow here][5]. + ## Related content * [Preview and publish your application][2] @@ -21183,7 +21266,7 @@ Still need help? If you're looking for something else, please feel free to make [2]: /user-docs/preview-and-publish-application [3]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/8694e405-b111-4eac-9359-e2f2d0b9da93/Subdomain%20Domains.png [4]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/9926495a-b0c6-4e3e-8a46-d2e231a3c51c/configure%20domains.png - [5]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/4acc8bb1-8bbc-40b7-b0e0-fc62903907bb/domain%20name.png + [5]: https://baserow.io/docs/installation/configuration [6]: /user-docs/application-settings [7]: /user-docs/element-style",application builder,baserow_user_docs,https://baserow.io/user-docs/add-a-domain-to-an-application 168,Configure User Sources,user-sources,User sources in Baserow Application Builder,"# Configure User Sources (Authentication) @@ -22099,13 +22182,18 @@ Setting a background color fills the entire container of the element. This is of While the **Style** tab controls the box, the **General** tab allows you to override specific design tokens (like font size or button color) that are usually inherited from your Global Theme. **How to override a theme:** -1. Select the element. -2. Ensure you are on the **General** tab. -3. Look for the **Settings/Style icon** (often a small gear or slider icon) next to specific fields. -4. Clicking this opens a modal where you can select a specific color or typography style that differs from your default theme. + 1. Select the element. + 2. Ensure you are on the **General** tab. + 3. Locate the specific configuration field you want to style (e.g., the ""Text"" field of a button). + 4. Look for the **Settings/Style icon** (a small gear or slider icon) located next to the field. + 5. Click the icon to open the **Theme Overrides** modal. + 6. Select a specific color, typography, or style that differs from your default theme. ![Theme override](https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/0c90ab38-2db6-41e6-b34d-eb5609300f9a/ab_styling.webp) +> **Note:** If you do not apply an override, the element will automatically use the styles defined in your [Global Theme](/user-docs/application-settings#theme). + + ## Advanced: Custom CSS Classes If the built-in style options are not enough, you can apply custom CSS. @@ -22118,13 +22206,13 @@ If the built-in style options are not enough, you can apply custom CSS. ### Why are there two places to edit styles? * The **Style Tab** handles the *layout* (the box model: margins, padding, width). -* The **General Tab** handles the *component details* (specific theme overrides like font family or primary color). +* The **General Tab** handles the *content details* (specific theme overrides like font family or primary color). -### How do I revert a change? -If you have applied a Theme Override (in the General tab), simply deselect the custom option to revert to the Global Theme default. +### How do I revert a theme override? +Open the **Theme Overrides** modal (via the icon in the General tab) and deselect the custom option. The element will revert to the Global Theme default. ### Does padding affect the background color? -Yes. If you set a background color, it will fill the area included in the padding. If you want space *outside* the background color, you would typically need a margin (which is handled by the element's placement in the layout). +Yes. If you set a background color, it will fill the area included in the padding. If you want space *outside* the background color, you typically need to adjust the element's placement in the layout using columns. ### If I change the Global Theme, will my customized elements change? It depends. If you have kept an element set to ""Default,"" it will update to match the new Global Theme. If you have manually customized an element (e.g., manually set a button to Red), it will *ignore* the Global Theme update and stay Red. @@ -22154,1384 +22242,1605 @@ Still need help? If you're looking for something else, please feel free to make [3]: https://baserow.io/user-docs/application-settings [4]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/9cd19c33-5ae9-4115-9f68-c189609f6088/Untitled%201.png [5]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/9c2c8f51-2c0a-4b37-ba11-dcc2344c0736/Untitled%202.png",application builder,baserow_user_docs,https://baserow.io/user-docs/element-style -175,Element events,element-events,Baserow Application Builder element events,"# Application Builder element events +175,Element events,element-events,Baserow Application Builder element events,"# Configure element events and workflows -Element events let you decide what happens when someone clicks on things like buttons. This makes the user experience lively and responsive. +Events are the logic layer of your application. They allow you to define exactly what happens when a user interacts with an element, whether it's showing a success message, updating a database row, or sending an email to a customer. -Here, we'll look at how to make buttons interactive in an application. By connecting button clicks to certain actions, you make the user experience better and guide the way the application works. +This guide covers how to add interactivity to your application by triggering actions on clicks, form submissions, and page loads. -![Events][1] + ## Overview -Events are triggers within your application that respond to user interactions, like [clicking a button][2], [logging in][3], or [submitting a form][4]. They bridge the gap between user input and actions on the application's data. +* **Triggers:** Events are triggered by user actions, such as **On Click** (Button), **On Submit** (Form), or after a **User Login**. +* **Workflows:** You can stack multiple actions on a single trigger (e.g., *Create Row* -> *Send Email* -> *Show Notification* -> *Go to Home Page*). +* **Context:** Actions can access data from the element that triggered them (e.g., using the text from a form input to populate an email body). -You can create an action on click or after login. +![Events panel in Application Builder][1] -Here's an overview of the actions: +## How to add an event - - **Show notification**: This action triggers a notification to alert users about updates, messages, or system events within the application. - - **Open page**: This action opens an external URL. - - **Logout**: This action logs a user out of the application. - - **Refresh data source**: This triggers the data sources on the page to be refreshed to account for recent changes. - - **Send HTTP request**: Allows you to send requests to external APIs and services. - - **Send email**: Sends a customized email to specified recipients directly from the application, enabling automated communication using any SMTP server of your choice. - - **AI prompt**: Execute prompts, analyze data, or generate content directly within your workflow. - - **Create row**: This action creates a new row in the selected table. Choose a table to begin configuring fields. - - **Update row**: This action updates an existing row in the selected table. Choose a table to begin configuring fields. - - **Delete a row**: This action deletes an existing row in the selected table. Choose a table to begin configuring fields. - - **Send a Slack message**: This action sends a message directly to a Slack channel or specific user when your workflow runs. +1. Select the interactive element (e.g., Button or Form) on your canvas. +2. Click the **Events** tab in the properties panel on the right. +3. Choose your trigger (e.g., **On Click**). +4. Click **+ Add action** and select the desired operation from the list. +5. (Optional) Drag and drop actions to reorder the sequence of execution. -You can reorganize the events using the drag-and-drop handle. +## Action reference - +Events are categorized by their function. Below is a guide to every available action type. + +### UI and Navigation Actions -## Element action: Show notification +| Action | Description | Use Case | +| :--- | :--- | :--- | +| **Open page** | Redirects the user to another page or external URL. | Navigating to a ""Thank You"" page after submission. | +| **Show notification** | Displays a temporary pop-up toast message. | Confirming ""Profile Updated"" without leaving the page. | +| **Refresh data source** | Forces a data source to reload its content. | Updating a list immediately after adding a new item. | +| **Logout** | Ends the user's session. | Creating a ""Sign Out"" button in a menu. | + +![Show notification](https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/3c68c1dc-31f2-47f9-9723-bf06abe761d5/Untitled%201.png) + +### Data Operations (CRUD) +These actions interact directly with your Baserow database. + +* **Create row:** Adds a new record to a table. You must map your form inputs to the table columns. +* **Update row:** Modifies an existing record. Requires a Row ID to identify which item to update. +* **Delete row:** Permanently removes a record. Requires a Row ID. -Notifications are key for user feedback in your application. +![Create a row event in Baserow Application Builder][2] -The ""Show notification"" feature in Application Builder lets you make short messages that pop up on the screen. They tell users about what's happening in your application and then disappear quickly. This way, users get updates without any mess on the screen. +### External Integrations +These actions connect your application to third-party services. -For example, when someone sends a form, they can't see what happens next. They might wonder if it worked. That's where ""Show notification"" comes in. It gives them a clear message like ""Form submitted successfully!"" or ""Thank you for your inquiry!"" to let them know everything was successful. + * **Send email:** Sends a custom email via SMTP (Gmail, Outlook, etc.). + * **Send HTTP request:** Connects to any API (e.g., Stripe, Zapier, Webhooks). + * **Send Slack message:** Posts a message to a specific channel or user. + * **AI prompt:** Sends text to a generative AI model for analysis or content creation. -![Show notification][5] +Learn more about [how to configure the Slack message action][3]. -## Element action: Open a page +## Send email (SMTP) -When a user clicks the button, it directs them to another page. +The **Send email** action allows you to send automated notifications directly from your application. -This feature is perfect for when you want users to go to a certain page after logging in or on click. It could be a confirmation page, a thank you page, or a page with more information. +### Configuration + 1. **Integration:** Select or add an SMTP integration. + * **Host:** `smtp.gmail.com` (Example) + * **Port:** `587` (TLS) or `465` (SSL) + * **Auth:** Username and Password (use an App Password for Gmail). + 2. **Recipient (To):** Enter a static email or bind it to a form field (e.g., `{{ form.email }}`). + 3. You can also configure additional fields such as CC, BCC, and select the Body Type as needed. + 4. **Message:** Supports plain text or HTML. You can insert dynamic variables to personalize the content (e.g., ""Hello `{{ User.First_Name }}`""). -This improves user flow by directing users to a specific web address. It's handy for accessing related links. +> Note for Gmail users: You need to create an App Password and use it instead of your regular email password. -## Element action: HTTP request +![Send email event action][4] -The HTTP request action allows you to send requests to external APIs and services directly from your application. This powerful feature enables integration with third-party systems, data submission to external endpoints, and triggering of external workflows. +## HTTP request -Here's how it works: +The HTTP request action allows you to send requests to external APIs and services directly from your application. Use this action to trigger complex workflows in tools like Make, n8n, or custom backends. -1. **Choose HTTP method**: Select the appropriate HTTP method (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS) for your request. -2. **Configure the endpoint url**: Specify the URL of the API endpoint you want to send the request to. -3. **Set query parameters**: Add any necessary query parameters. -4. **Set headers**: Add any necessary headers for authentication, content type, or other requirements. -5. **Select body type**: Body content can be sent as JSON, form multipart and raw. -6. **Include request body content**: Set the body content, you can include data from form fields or other available data sources. -7. **Timeout**: Set the amount of time this request has to succeed before timing out and failing. +**Steps:** + 1. **Method:** Choose GET, POST, PUT, DELETE, etc. + 2. **URL:** The endpoint of the external service. + 3. **Set query parameters**: Add any necessary query parameters. + 4. **Set headers**: Add any necessary headers for authentication, content type, or other requirements. + 5. **Body:** Select JSON, Form Data, or Raw. You can map application data into the JSON body to pass information to the external tool. + 6. **Timeout**: Set the amount of time this request has to succeed before timing out and failing. This action is perfect for integrating with payment processors, sending data to CRM systems, triggering automated workflows, or communicating with any external API that your application needs to interact with. -## Element action: Create a row +## Update row + +To update a specific row, the workflow needs to know *which* row to target. + +1. **Select Table:** Choose the database table to update. +2. **Row ID:** This is critical. You must provide the ID of the row. + * *Context:* Usually, this comes from a **Page Parameter** (e.g., on a ""Edit Profile"" page, the ID is in the URL) or from a **Data Source**. +3. **Field Mapping:** Select which columns to update and map them to your form inputs. + +![Update row event in Baserow Application Builder][5] + +## AI prompt + +The AI Prompt node connects to generative AI models to execute prompts, analyze data, or generate content directly within your workflow. This allows you to build powerful automations that can summarize text, categorize customer feedback, translate languages, or generate email replies based on your Baserow data. -This event lets users submit data that [creates a new row in a table](/user-docs/how-to-make-new-rows). It's perfect for building forms to collect data, like applications, feedback, or purchase orders. +Learn more about [how to configure the AI prompt][6]. -Here's how it works: +## Frequently asked questions (FAQ) -1. **Choose a database**: Select the database containing the table where you want to create new rows. -2. **Specify the target table**: Pick the specific table within the chosen database that will receive the new rows. -3. **Map form fields**: Match the fields in the form with the corresponding fields in the table. This ensures the data gets placed in the correct location within each row. +### Can I run multiple actions at once? +Yes. Actions run sequentially from top to bottom. For example, you can create a row, *then* send an email, *then* show a notification. -For example, A job application form creates a new record in a ""Job Applications"" table with the applicant's details and the specific job they're applying for. Whenever an applicant submits the form, a new row will be created in the table, automatically capturing their information. +### How do I get the Row ID for an update? +The Row ID is usually passed via the URL (e.g., `/edit-task/:id`). In your Update Row action, you can select this **Page Parameter** as the source for the Row ID field. -![Create a row event in Baserow Application Builder][6] +### Why isn't my email sending? +Check your SMTP credentials. If you are using Gmail, you *must* use an **App Password** and enable TLS on port 587. Standard passwords will not work due to security restrictions. -## Send a Slack message +## Related content -This action sends a message directly to a Slack channel or specific user when your workflow runs. + * [Configure Page Parameters](/user-docs/application-builder-page-settings) + * [Using the Form Element][7] + * [Connecting Data Sources](/user-docs/data-sources) -Learn more about [how to configure the Slack message action][7] +--- -## Element event: Update an existing row -This event allows users to modify existing data entries within the application. It facilitates the editing process, allowing users to make necessary changes to the data. +Still need help? If you're looking for something else, please feel free to make recommendations or ask us questions; we’re ready to assist you. -For example, if you have a list of tasks, the task page can contain a form through which employees can request task changes. + - [Ask the Baserow community](https://community.baserow.io) + - [Contact support](/contact) for questions about Baserow or help with your account. -1. **Configure the update action:** In the Events tab of the Application Builder, select the [integration](/user-docs/application-settings), [database](/user-docs/intro-to-databases), [table](/user-docs/intro-to-tables), and specific [row](/user-docs/overview-of-rows) you want to update after the event occurs. -2. **Define updatable fields:** Choose which data fields from the application users can modify through the form. -After the event occurs, the application automatically updates the designated row with the new information, reflecting the user's requested changes. + [1]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/b31f7848-089d-44d8-a300-404d6d7bab80/Untitled.png + [2]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/743f7e55-467f-4acf-b276-12bbeaab4df4/Untitled%202.png + [3]: https://baserow.io/user-docs/automation-actions#send-a-slack-message + [4]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/820cea6d-8d59-485c-bbe4-db29bf3231a7/Send%20email.png + [5]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/b07285b6-777b-44b2-86ad-4bbb4acfb0f5/Untitled%203.png + [6]: https://baserow.io/user-docs/automation-actions#ai-prompt + [7]: /user-docs/application-builder-form-element",application builder,baserow_user_docs,https://baserow.io/user-docs/element-events +176,Heading element,application-builder-heading-element,Baserow Application Builder heading element,"# Application Builder - Heading element -![Update row event in Baserow Application Builder][8] +Headings are static or dynamic text elements used to organize your [page][1]. They provide visual hierarchy, allowing users to scan content easily, and are essential for accessibility and SEO. -## AI prompt +This guide covers how to display titles and section headers to structure your application content. -The AI Prompt node connects to generative AI models to execute prompts, analyze data, or generate content directly within your workflow. This allows you to build powerful automations that can summarize text, categorize customer feedback, translate languages, or generate email replies based on your Baserow data. +![Heading level][2] -Learn more about [how to configure the AI prompt][9] +## Overview -## Element event: Send an email with SMTP +Headings are important for structuring the page and guiding users through content on the page. They establish a hierarchy, making it easier to scan information. -You can send custom emails using the **Send email** action inside your workflow. All you need is access to an SMTP server like Gmail, Outlook, or your company mail. +* **Function:** Displays large, bold text for titles. +* **Levels:** Supports tags `H1` through `H6` for semantic structure. +* **Dynamic Data:** You can bind the text to a Data Source (e.g., to display a record name as a page title). +* **[Styling][3]:** Typography (Font, Color, Alignment) is managed via **Theme Overrides**, defaulting to your Global Theme settings. -### Step 1: Add a “Send email” action +## Add a heading -1. Navigate to the **Events** tab. -2. Select an event trigger, such as **On click**. -3. Click the **+Add action** button and choose **Send Email** from the list. +1. Open the [**Elements** panel][4] (click the `+` icon). +2. Select **Heading**. +3. Drag and drop the element onto your canvas. -### Step 2: Configure your SMTP integration within the action +## Configuration -You will be prompted to set up an SMTP integration to send emails. +Once added, click the heading to open the **General** properties tab. There are two primary configuration options: -| Field | Description | -|-------------|-----------------------------------------------------------| -| SMTP Host | The SMTP server address, e.g., `smtp.gmail.com` | -| SMTP Port | Typically `587` for TLS or `465` for SSL | -| Use TLS | Enable if required (✅ recommended for Gmail, Outlook, etc.) | -| Username | Your full email address | -| Password | Your email password or an app-specific password (recommended) | +### 1. Heading Level +Select the semantic tag for the heading. This determines the relative size and importance of the text. -> 💡 **Note for Gmail users:** You need to create an App Password and use it here instead of your regular email password. +* **H1:** The most important heading. Use this once per page for the main title. +* **H2 - H3:** Use these for major section dividers. +* **H4 - H6:** Use these for sub-sections or minor titles. -Once saved, this SMTP integration will be available for all future email actions. +### 2. Text (Content) +Enter the actual text you want to display. You have two options: -### Step 3: Compose your email +* **Static Text:** Type plain text directly into the field (e.g., ""Our Services""). +* **Dynamic Data:** Click the **Plug icon** or **Connect Data** button to bind the text to a [Data Source](/user-docs/data-sources). This allows the heading to change based on the context (e.g., displaying `{{ Product_Name }}` on a product detail page). -Fill in the email details to customize your message: +## Styling and Theme Overrides -| Field | Description | -|------------|------------------------------------------------| -| To | Recipient’s email address | -| Subject | The subject line of your email | -| Message | Email body content (supports both plain text and HTML) | +By default, the heading inherits the font family, color, and weight defined in your [Global Theme](/user-docs/application-settings). -You can also configure additional fields such as *CC*, *BCC*, and select the *Body Type* as needed. +To customize the look of a specific heading (e.g., to make one specific H2 red and centered), you must use a **Theme Override**. -Automate confirmations, alerts, and updates based on user interactions. +1. In the General tab, look for the **Style/Settings icon** (slider icon) next to the configuration fields. +2. Clicking this opens the **Typography** menu. +3. Here you can override the following properties for this specific element: + * **Font Family & Weight** + * **Size** (Pixel or Rem) + * **Alignment** (Left, Center, Right, Justify) + * **Color** (Hex, RGB, or Theme variable) + * **Text Decoration** (Underline, Strikethrough) -![Send email event action][10] +![Heading Styling and Theme Overrides][5] -### Step 4: Add dynamic content (Optional) -Make your emails personal by including data from your forms or database: +> **Note:** Changes made here apply *only* to this element. To change headings across the entire app, update your [Global Theme](/user-docs/application-settings) instead. -1. Click in any email field (To, Subject, or Message) -2. Use the data source picker to insert dynamic values -3. Examples: - - **To field**: Use form email input or user's email from database - - **Subject**: ""Thank you {{form_data.name}} for your submission"" - - **Message**: Include order details, user information, or form responses +## Related content -This ensures each email is customized for the recipient. + * [Text Element](/user-docs/application-builder-text-element) + * [Connecting Data Sources](/user-docs/data-sources) + * [Styling Elements (Layout & Overrides)](/user-docs/element-style) --- -Still need help? If you're looking for something else, please feel free to make recommendations or ask us questions—we’re ready to assist you. + +Still need help? If you're looking for something else, please feel free to make recommendations or ask us questions; we’re ready to assist you. - [Ask the Baserow community](https://community.baserow.io) - - [Contact support](/contact) for questions about Baserow or help with your account + - [Contact support](/contact) for questions about Baserow or help with your account. - [1]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/b31f7848-089d-44d8-a300-404d6d7bab80/Untitled.png - [2]: /user-docs/application-builder-button-element - [3]: /user-docs/application-builder-login-element - [4]: /user-docs/application-builder-form-element - [5]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/3c68c1dc-31f2-47f9-9723-bf06abe761d5/Untitled%201.png - [6]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/743f7e55-467f-4acf-b276-12bbeaab4df4/Untitled%202.png - [7]: https://baserow.io/user-docs/automation-actions#send-a-slack-message - [8]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/b07285b6-777b-44b2-86ad-4bbb4acfb0f5/Untitled%203.png - [9]: https://baserow.io/user-docs/automation-actions#ai-prompt - [10]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/820cea6d-8d59-485c-bbe4-db29bf3231a7/Send%20email.png",application builder,baserow_user_docs,https://baserow.io/user-docs/element-events -176,Heading element,application-builder-heading-element,Baserow Application Builder heading element,"# Application Builder - Heading element + [1]: https://baserow.io/user-docs/pages-in-the-application-builder + [2]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/43d8cef7-9cd5-487d-9c07-3938a21838eb/Heading%20levels%20-%20%5BHeading%20level.png + [3]: https://baserow.io/user-docs/element-style + [4]: https://baserow.io/user-docs/add-and-remove-elements + [5]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/30b51b1b-9eb9-4bc5-b2a8-85302e3fe004/Heading%20Styling%20and%20Theme%20Overrides.png",application builder,baserow_user_docs,https://baserow.io/user-docs/application-builder-heading-element +177,Text element,application-builder-text-element,Text element in Baserow Application Builder,"# Application Builder - Text element -A heading element defines the titles of sections within a [page](/user-docs/pages-in-the-application-builder). Heading elements help organize content and improve readability by providing structure to pages. +The Text element is the primary tool for adding paragraphs, notes, or variable data to your page. Unlike the Heading element (which is for titles), the Text element is optimized for body content and supports Markdown rendering for advanced formatting. -In this section, we’ll set up a heading element, the configuration options, and how it generally works. +This guide covers how to display static descriptions, instructions, or dynamic content using rich text formatting. ## Overview -Headings are important for structuring the page and guiding users through content on the page. They establish a hierarchy, making it easier to scan information. +The Application Builder allow you to bind the text element to [data sources][1]. This enables the text to update dynamically based on user input or application logic. By binding the text element, any changes to the data source will be reflected in the displayed text, creating a responsive and dynamic user experience. + +* **Versatility:** Use it for simple labels, long-form instructions, or dynamic data displays. +* **Formatting:** Supports **Plain Text** (raw string) or **Markdown** (bold, italics, lists, links). +* **Data Binding:** You can mix static text with dynamic variables from your database (e.g., ""Order Status: `{{ Status }}`""). +* **Styling:** Typography settings (Font, Color, Alignment) are managed via **Theme Overrides**. -You can customize the [element properties and style of the heading](/user-docs/element-style) using the element settings panel on the right side of the screen. +![Text element][2] -## Add and configure heading elements +## Add a text element -To add a heading element, click the `+` icon and select **Heading**. +1. Open the **Elements** panel (click the `+` icon). +2. Select **Text**. +3. Drag and drop the element onto your canvas. -Place the heading wherever you want it on the [page][1]. Don't worry if it's not perfectly positioned initially; you can always move it later. +## Configuration -Learn more about how to [add and remove an element from a page](/user-docs/add-and-remove-elements). +Click the text element to open the **General** properties tab. You will see two primary configuration options: -![Add and configure table elements][2] +### 1. Text (Content) +This is where you define what the element displays. -Now, you can configure the heading's properties to make it function and look the way you want. This involves settings related to heading style. +* **Static Input:** Type directly into the box to display fixed instructions or descriptions. +* **Dynamic Binding:** Click the **Plug icon** or **Connect Data** button to insert variables from a [Data Source](/user-docs/data-sources). + * *Example:* You can type ""Welcome back,"" and then select the `User Name` variable to create a personalized greeting: ""Welcome back, John."" -## Heading levels +### 2. Format +Choose how Baserow should interpret your text. -Baserow has several heading levels that can be customized according to your requirements. +* **Plain text:** Displays exactly what you type. Good for simple labels. +* **[Markdown][3]:** Renders special syntax as rich text. Good for paragraphs requiring structure. -The heading levels, ranging from `

` to `

` , indicate different levels of importance and hierarchy. +### Markdown reference +If you select **Markdown**, you can use standard syntax to format your content: -The `

` tag represents the highest level of importance, typically used for the main title of the page, while `

` represents the lowest level of importance. +| Style | Syntax | Result | +| :--- | :--- | :--- | +| **Bold** | `**Bold Text**` | **Bold Text** | +| **Italic** | `*Italic Text*` | *Italic Text* | +| **Link** | `[Link Text](url)` | [Link Text](#) | +| **List** | `- Item` | • Item | +| **Heading** | `### Title` | **Title** (Small Header) | -![Heading level][3] +## Styling and Theme Overrides -## Heading text +By default, the text element uses the ""Body"" typography settings defined in your [Global Theme](/user-docs/application-settings). -The heading element has a text field to enter the heading text and that can be changed by clicking on it. +To change the appearance of a specific text block (e.g., to center-align a footer or make a warning message red), use a **Theme Override**. -You can enter static text here. However, if you've connected to a [data source](/user-docs/data-sources), all the fields from the [data source](/user-docs/data-sources) will also become available. Select a field from the data source to fetch data dynamically. +1. In the General tab, look for the **Style/Settings icon** (slider icon) next to the configuration fields. +2. Clicking this opens the **Typography** menu. +3. Here you can override: + * **Font Family & Size** + * **Alignment** (Left, Center, Right, Justify) + * **Color** + * **Weight** (Bold/Regular) -![Heading text][4] +![Text Styling and Theme Overrides][4] -## Horizontal alignment +> **Note:** Moving the **Alignment** setting to Theme Overrides keeps your main configuration clean. If you don't set an alignment here, it defaults to Left (or whatever your Global Theme specifies). -The horizontal alignment property controls how heading elements are positioned on the page. You can use it to achieve different visual arrangements for your headings. +## Frequently asked questions (FAQ) -Here's a breakdown of the available alignment options: +### Can I use HTML in the text element? +No. For security reasons, HTML is not rendered. Please use the **Markdown** format option to add links, images, or formatting. If you strictly need to embed HTML/IFrames, use the [IFrame element](/user-docs/application-builder-iframe-element). -- **Left**: Aligns the heading to the left side of its container. -- **Center**: Aligns the heading in the center of its container. -- **Right**: Aligns the heading to the right side of its container. +### How do I combine text and data? +You can mix them in the same field. For example, type ""Total Price: $"" and then click the variable button to insert `{{ Price }}`. The result will look like ""Total Price: $50.00"". -By adjusting the horizontal alignment property, you can create a more balanced and organized layout for the page. +### Can I change the background color? +Yes, but not in the ""General"" tab. Switch to the **Style** tab (Layout settings) to configure background colors, padding, and borders for the text container. -![Horizontal alignment][5] +## Related content -## Heading font color + * [Heading Element](/user-docs/application-builder-heading-element) + * [Connecting Data Sources](/user-docs/data-sources) + * [Element Styling (Layout vs. Theme)](/user-docs/element-style) -You can easily modify the font color of headings in the Application Builder. +--- -Navigate to the page where you can edit the heading you want to modify and select the heading element within the editor. Change the font color of a heading element using the Font property within a General tab. -Click on the color picker or input field next to the font color option. +Still need help? If you're looking for something else, please feel free to make recommendations or ask us questions; we’re ready to assist you. -Set the desired color of the heading using one of these methods: + - [Ask the Baserow community](https://community.baserow.io) + - [Contact support](/contact) for questions about Baserow or help with your account. -- **Hexadecimal color code:** Enter a six-digit code preceded by a hashtag (#), like #FF0000 for red. -- **RGB value:** Specify the red, green, and blue values (0-255) separated by commas, like RGB (255, 0, 0) for red. -- **Opacity:** Adjust the transparency of the chosen color using a value between 0 (fully transparent) and 1 (fully opaque). -Use a visual color picker tool to interactively choose a desired color. + [1]: https://baserow.io/user-docs/data-sources + [2]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/6e762454-eeaf-4e7f-bdb0-36a1977cb154/Text%20element%20-%20app%20builder.png + [3]: https://www.markdownguide.org/basic-syntax/ + [4]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/e4a60717-5ab7-40e8-9f1b-335e943bee74/text%20-%20Styling%20and%20Theme%20Overrides.png",application builder,baserow_user_docs,https://baserow.io/user-docs/application-builder-text-element +178,Link element,application-builder-link-element,Link element in Baserow Application Builder,"# Link element -Alternatively, you can inherit the default styles defined in the [theme settings](/user-docs/application-settings#theme) for a cohesive look. +The Link element is the primary tool for navigation. It can appear as a simple text hyperlink or a call-to-action button. You use it to connect different parts of your application, drill down into row details, or direct users to external resources. -![Font color][6] +This guide covers how to create navigation links to internal pages or external websites. ---- +## Overview -Still need help? If you're looking for something else, please feel free to make recommendations or ask us questions—we’re ready to assist you. +The Application Builder allows you to bind the text to data sources. This enables the text to update dynamically based on user input or application logic. - - [Ask the Baserow community](https://community.baserow.io) - - [Contact support](/contact) for questions about Baserow or help with your account +* **Navigation:** Can point to an **Internal Page** (inside your app) or a **Custom URL** (external website). +* **Dynamic Context:** When linking to internal pages, you can pass data (like a Row ID) to load specific content. +* **Visual Variants:** You can style the element as a standard text **Link** or a prominent **Button**. +* **Styling:** Colors, typography, and interaction states (Hover/Active) are managed via **Theme Overrides**. - [1]: /user-docs/pages-in-the-application-builder - [2]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/9b4430ab-5f8d-4261-acc1-72b100196f42/Untitled.png - [3]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/39b1b36c-e6a0-41fd-a0e9-f54eb92de64b/Untitled%201.png - [4]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/4175c5f4-4bf8-4e56-b09a-2ea6cb4b1c0b/Untitled%202.png - [5]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/a9550551-377a-4864-8926-ea52a7a94f65/Untitled%203.png - [6]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/7f313e5b-4142-4775-9a2a-f80377b609c4/Untitled%204.png",,baserow_user_docs,https://baserow.io/user-docs/application-builder-heading-element -177,Text element,application-builder-text-element,Text element in Baserow Application Builder,"# Application builder - Text element +![Link element][1] -The text element is a versatile component that allows you to display information within the application. It provides a clean and simple way to present content. +## Add a link -In this section, we'll guide you through the process of setting up a text element and explain each configuration option in detail. +1. Open the **Elements** panel (click the `+` icon). +2. Select **Link**. +3. Drag and drop the element onto your canvas. -## Overview +## Configuration -The Application Builders allow you to bind the text element to [data sources](/user-docs/data-sources). This enables the text to update dynamically based on user input or application logic. By binding the text element, any changes to the data source will be reflected in the displayed text, creating a responsive and dynamic user experience. +Click the element to open the **General** properties tab. -For the text element, you can configure the element properties and [style](/user-docs/element-style) from the element settings. +### 1. Text +Enter the label user will click on. +* **Static:** Type ""Read More"" or ""View Details"". +* **Dynamic:** Click the **Connect Data** icon to bind this to a data source (e.g., displaying `{{ Company Name }}`). -![Text element][1] +### 2. Navigate to (Destination) +Choose where the user goes when they click. -## Add and configure text elements +#### Option A: Page (Internal) +Select an existing page from your application. +* **Parameters:** If the destination page has a [Path Parameter][2] (e.g., `/product/:id`), a new field will appear asking you to map the value. + * *Use Case (Row Details):* To link to a specific product's detail page, select the destination page, then map the `:id` parameter to `Current Record > Record ID`. -To add a text element, access the [elements panel](/user-docs/elements-overview) and select **Text**. +#### Option B: Custom URL (External) +Select **Custom URL** to link outside your app. +* **URL:** Enter the full address (e.g., `https://google.com` or `mailto:support@example.com`). -Once added, place the text wherever you want it on the [page][2]. Don't worry if it's not perfectly positioned initially; you can always move it later. +### 3. Open in +Control browser behavior. +* **Same tab:** Replaces the current page. Best for internal navigation. +* **New tab:** Opens a new browser window. Best for external links to keep users on your app. -Learn more about how to [add and remove an element from a page](/user-docs/add-and-remove-elements). +### 4. Variant +Choose the fundamental look of the element. +* **Link:** Appears as text (usually underlined or colored). Best for inline navigation or secondary actions. +* **Button:** Appears as a colored box. Best for primary calls-to-action (CTAs). -![Add and configure table elements][3] +## Styling and Theme Overrides -Once you've placed the text element on the editor, you can customize it to make it function and look the way you want. This involves settings related to text style. +By default, the link inherits styles from your [Global Theme](/user-docs/application-settings). To customize a specific link (e.g., changing its color or alignment), use a **Theme Override**. -## Text element horizontal alignment +1. In the General tab, click the **Style/Settings icon** (slider icon) next to the configuration fields. +2. This opens the **Styling Menu** where you can override: -You can horizontally align a text element using the horizontal alignment property. This will align the element on the page. +### Typography & Alignment + * **Font Family, Size, Weight** + * **Alignment:** (Left, Center, Right). *Note: This controls the text alignment within the element container.* + * Border size, radius, padding for button variant. -This property offers several options to achieve the desired layout: +### Colors and States +Links and buttons have interactive states. You can define different colors for each state to provide visual feedback: +* **Default:** How the link looks normally. +* **Hover:** How it looks when the mouse cursor is over it. +* **Active:** How it looks at the moment it is clicked. -- **Left:** Aligns the text to the left edge of the container. This is the default alignment for most text elements. -- **Right:** Aligns the text to the right edge of the container. -- **Center:** Positions the text horizontally in the center of the container. +![Link - Styling and Theme Overrides][3] -![Horizontal alignment][4] +> **Note:** To change the width of a Button variant (e.g., make it **Full Width**), switch to the **Style Tab** (Layout settings) and adjust the Width property. -## Text format +## Frequently asked questions (FAQ) -You can choose between the Plain text or Markdown format. +### How do I create a ""Back"" button? +Add a Link element, select **Page** as the destination, and choose the previous page in your hierarchy. Currently, there is no automatic ""Browser Back"" function, so you must define the destination explicitly. -Plain text is basic and universal, while Markdown offers more advanced formatting options. +### Can I pass data between pages? +Yes. If you need to pass data (like a User ID or Category), ensure the destination page has a **Path Parameter** set up in its [Page Settings][2]. The Link element will then allow you to map data to that parameter. -Markdown offers more advanced options for formatting text, like headings, bold, italics, and bulleted lists. +### Why is my button width not changing? +The **Variant** setting only changes the visual style (Text vs. Box). To change the actual size/width of the container, click the **Style** tab at the top of the properties panel and change **Width** to ""Full Width"" or ""Normal"". -You can use [Markdown](https://www.markdownguide.org/basic-syntax/) syntax in the text element, allowing you to format text with titles, paragraphs, links, images, etc. Once you're familiar with Markdown syntax, you can add structure to the text. +## Related content -![Text format markdown][5] + * [Page Settings & Parameters][2] + * [Button Element](/user-docs/application-builder-button-element) + * [Element Styling (Layout vs. Theme)](/user-docs/element-style) --- -Still need help? If you're looking for something else, please feel free to make recommendations or ask us questions—we’re ready to assist you. + +Still need help? If you're looking for something else, please feel free to make recommendations or ask us questions; we’re ready to assist you. - [Ask the Baserow community](https://community.baserow.io) - - [Contact support](/contact) for questions about Baserow or help with your account + - [Contact support](/contact) for questions about Baserow or help with your account. - [1]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/c52267ef-d70e-4b2a-92cd-a95e865c9a3e/Untitled.png + + [1]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/85d1d730-be9f-485b-b2bd-1a405446a1be/link%20element%20-%20app%20builder.png [2]: /user-docs/pages-in-the-application-builder - [3]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/e3ed62f7-e34d-4ff4-9183-c691d3ceaff9/Untitled%201.png - [4]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/d5fb0061-19dc-474a-a0c9-7a4923a4281f/Untitled%202.png - [5]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/160b2c77-327e-432c-a015-b809374005c2/Untitled%203.png",,baserow_user_docs,https://baserow.io/user-docs/application-builder-text-element -178,Link element,application-builder-link-element,Link element in Baserow Application Builder,"# Application Builder - Link element + [3]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/ca2fe27a-336d-4576-bc7f-705e35e1ef99/Link%20-%20Styling%20and%20Theme%20Overrides.png",application builder,baserow_user_docs,https://baserow.io/user-docs/application-builder-link-element +179,Image element,application-builder-image-element,Baserow Application Builder image element,"# Application Builder - Image element -Links are essential for smooth navigation and user interactions in an app. They help users move between pages, start actions, or reach outside websites. +The Image element allows you to embed visuals in your application. You can upload a fixed file (like a logo) or bind the source to a URL from your database (like a product photo that changes for each row). -We'll explore how to set up links well so they can connect to pages inside the app and to external sites. +This guide covers how to display static assets or dynamic images linked to your database records. ## Overview -For the link element, you can configure the element properties, and [style](/user-docs/element-style) from the element settings. +* **Source Options:** You can **Upload** a file directly or use a **URL** (link). +* **Dynamic Binding:** By using the URL option, you can connect the image to a Data Source to display different images for different database records. +* **Accessibility:** You can define **Alt Text** for screen readers and SEO. +* **Styling:** Visual properties like Size, Alignment, Aspect Ratio (Constraints), and Border Radius are managed via **Theme Overrides**. -The Application Builder allows you to bind the text to [data sources](/user-docs/data-sources). This enables the text to update dynamically based on user input or application logic. +![Baserow Application Builder - Image element][1] -## Add and configure link elements +## Add an image -To add a link element, access the [elements panel](/user-docs/elements-overview) and select **Link**. +1. Open the **Elements** panel (click the `+` icon). +2. Select **Image**. +3. Drag and drop the element onto your canvas. -Once added, place the link wherever you want it on the [page][1]. Don't worry if it's not perfectly positioned initially; you can always move it later. +## Configuration -Learn more about how to [add and remove an element from a page](/user-docs/add-and-remove-elements). +Click the image element to open the **General** properties tab. You will configure the *content* here. -![Add and configure table elements][2] +### 1. Image Source +Choose how Baserow finds the image. -Now, you'll customize the link's behavior and appearance by configuring its properties. This includes options for linking to specific actions within the application. +* **Upload:** Click the **Upload** button to select a file from your computer. Use this for static assets like banners, logos, or icons that never change. +* **URL:** Select this to link to an image hosted online. + * **Static URL:** Paste a direct link (e.g., `https://example.com/logo.png`). + * **Dynamic URL:** Click the **Connect Data** icon to bind this field to a [Data Source](/user-docs/data-sources). + * *Use Case:* If you have a table of employees with a ""Headshot URL"" field, bind this property to that field to show the correct face for each employee profile. -## Linking to pages +### 2. Alt Text +Enter a short description of the image. This is displayed if the image fails to load and is read aloud by screen readers. +* **Best Practice:** Describe the content (e.g., ""Red running shoe"") rather than the function (e.g., ""Image""). +* **Dynamic:** You can also bind this to a data source (e.g., `{{ Product_Name }}`) to automatically generate relevant alt text for every record. -The Application Builder allows you to create links that, when clicked, take users to a designated page within your application. +## Styling and Theme Overrides -You can control whether the linked page opens in the same browser tab (replacing the current content) or in a new tab altogether. Choosing the right option depends on your application's structure and user flow. Here's a quick guideline: +To customize the dimensions, alignment, and fit of the image, you must use a **Theme Override**. -- **Open in same tab**: When you click a link within a page, the new content replaces the current one in the same browser tab. This keeps your browsing experience focused on a single window. -- **Open in new tab**: Clicking a link within the page opens the linked content in a new browser tab. This allows you to keep the original content accessible while viewing the new one separately. +1. In the General tab, click the **Style/Settings icon** (slider icon) next to the configuration fields. +2. This opens the **Image Styling** menu where you can configure: -![Baserow Link Open in...][3] +### Dimensions (Size) +Control the physical size of the image container. +* **Max Width (%):** Sets the width relative to the parent container (0-100%). +* **Max Height (px):** Sets a fixed maximum height in pixels (5-3000px). -## Navigate to a page +### Constraints (Object Fit) +Determines how the image scales to fit inside the dimensions you set above. -This feature lets you link to a different page in the application. When you navigate to a page, you're moving to that place in the application where you can view content or do things. +| Constraint | Behavior | Best For | +| :--- | :--- | :--- | +| **Extend to max width** | Stretches the image to fill the available width while maintaining aspect ratio. | Hero images, full-width banners. | +| **Contain** | Scales the image so it fits entirely within the container without cropping. | Product photos, logos where seeing the whole image matters. | +| **Cover** | Fills the entire container, potentially cropping the edges of the image to fill the space. | Backgrounds, avatars, grid thumbnails. | -### Navigate to a path +> **Note:** Some constraints depend on dimensions. For example, **Cover** requires a specific height to work effectively, while **Contain** is unavailable if height is limited. -Navigating to a path is about moving to a specific location within the application, which is identified by its path in the application's structure. +### Appearance +* **Alignment:** Position the image to the **Left**, **Center**, or **Right** of its container. +* **Border Radius:** Round the corners of the image. Set to a high number (e.g., 50% or 100px) to create circular avatar images. -You need to first add a parameter via `:parameter` . [Path parameters](/user-docs/application-builder-page-settings#path-parameters) can be used to load data, depending on the provided parameter dynamically. +![Image Styling and Theme Overrides][2] -The content will be generated automatically on that page depending on which specific detail the user has navigated to the detail page from. +## Frequently asked questions (FAQ) -You can also do the same for a link type in the field configuration on a table element. +### Which image formats are supported? +You can display standard web formats including JPEG, PNG, GIF, WEBP, and SVG. -### Navigate to a custom URL +### Why is my image distorted? +If you force both a specific Width AND a specific Height that doesn't match the original aspect ratio, the image may skew. Use the **Constraints** setting (Cover or Contain) to handle aspect ratios gracefully. -Navigating to a custom URL means going to a web address that is specifically designed or created for a particular purpose, often outside of the standard paths predefined by the application. +### Can I link an image to a page? +The Image element itself is purely for display. To make an image clickable, you should wrap it in a Link container or use a Card element (if available), or check if the **Events** tab allows an ""On Click"" -> ""Open Page"" action for the image. -To link to an external website, input the URL into the link field using the following format `https://www.example.com`. +## Related content -### View row details + * [Connecting Data Sources](/user-docs/data-sources) + * [Element Styling (Layout vs. Theme)](/user-docs/element-style) + * [Using the Table Element](/user-docs/application-builder-table-element) -You can link to an internal page and set it to open a separate page within your Baserow application. For example, when the user clicks on a button, you want to display the row details. +--- -1. **Map row ID:** Ensure the row ID field in the [data source](/user-docs/data-sources) configuration is properly mapped. This will be used to identify the specific row when displaying details. - - > Determine the [data source](/user-docs/data-sources) that contains the information you want to show. This should be the same [data source](/user-docs/data-sources) that provides the details on the current page, or it could be a different one. - > -2. **Create a link:** - - Navigate to the element settings for the button or link you want to use to access the details page. - - Open the **General** tab. - - Locate the **Navigate to** dropdown menu. - - Select the dynamic page that displays the detailed information for a specific row. -![Link row to a details page][4] +Still need help? If you're looking for something else, please feel free to make recommendations or ask us questions; we’re ready to assist you. -That’s it. Now when the user clicks on a particular button, they will be taken to the page with the specific details. + - [Ask the Baserow community](https://community.baserow.io) + - [Contact support](/contact) for questions about Baserow or help with your account. -That’s it. Now when the user clicks on a particular button, they will be taken to the page with the specific details. -## Link variant + [1]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/40bfda1d-80cf-4a59-a790-ef3f62e0f451/image%20element.png + [2]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/3704a752-5515-4573-bcb9-077eef611fbc/Image%20-%20Styling%20and%20Theme%20Overrides.png",application builder,baserow_user_docs,https://baserow.io/user-docs/application-builder-image-element +180,Data input element,application-builder-text-input-element,Data input element in Baserow App Builder,"# Application Builder - Data Input element -A link variant refers to the style or type of link used. +The Data Input element is the standard text field for your application. Whether you are building a contact form, a search bar, or a login screen, this element handles the user entry. It supports various input types and validation logic. -When you select the Button variant, you can consider the design and user experience goals by choosing the button width. +This guide covers how to collect text, emails, passwords, or multiline descriptions from your users. -When it comes to the appearance of buttons, you have two options: ""auto"" and ""full width."" +## Overview -- **Auto width:** This means the button will adjust its width automatically based on the content inside. If the button text is short, the button will be narrower; if it's longer, the button will widen accordingly. -- **Full width:** Choosing this option means the button will stretch across the entire width of its container, regardless of the text inside. It provides a more expansive look and can be visually impactful. +The Data Input element allows users to enter textual information. This is important for collecting various data types, including names, email addresses, URLs, and phone numbers. Text inputs are commonly used together with the form element to create user input forms. -![Link to a details page][5] +* **Versatility:** Can be configured as a single-line text field, a password field, an email field, or a multi-line text area. +* **Validation:** Built-in options to mark fields as ""Required"" or enforce formats (like Email). +* **Dynamic Pre-fill:** You can bind the default value to a Data Source to pre-fill the input (e.g., ""Edit Profile""). +* **Styling:** Visual properties for both the **Label** and the **Input Box** are managed via **Theme Overrides**. -## Link horizontal alignment +![single-line input field][1] -You can horizontally align a link element using the horizontal alignment property. This will align the link on the page. +## Add a data input -- **Center:** Aligns the link text in the middle of its container horizontally. -- **Left:** Aligns the link text to the left edge of its container. -- **Right:** Aligns the link text to the right edge of its container. +1. Open the **Elements** panel (click the `+` icon). +2. Select **Data Input**. +3. Drag and drop the element onto your canvas (typically inside a [Form Container][2] if used for submission). -## Link button color +## Configuration -The color of a button refers to the visual appearance of the button itself. You can easily modify the link color in the Application Builder. +Click the element to open the **General** properties tab. -Navigate to the page where you can edit the button you want to modify and select the link element within the editor. Change the color using the Button color property in a General tab. +### 1. Content & Logic +* **Label:** The descriptive text displayed above the field (e.g., ""Email Address""). +* **Placeholder:** Grey hint text shown inside the box when empty (e.g., ""Enter your email...""). +* **Value:** Text that appears automatically when the page loads. + * *Dynamic:* Click the **Connect Data** icon to bind this to a [Data Source](/user-docs/data-sources). This is perfect for ""Edit"" forms where you want to show the existing data first. -Click on the color picker or input field next to the color option. +### 2. Input Type +Define how the browser should treat the data. + * **Text:** Standard alphanumeric input. + * **Password:** Masks the characters for security. -Set the desired color of the heading using one of these methods: +Passwords are sensitive data. Please be careful how you handle and store passwords in your application. -- **Hexadecimal color code:** Enter a six-digit code preceded by a hashtag (#), like #FF0000 for red. -- **RGB value:** Specify the red, green, and blue values (0-255) separated by commas, like rgb(255, 0, 0) for red. -- **Opacity:** Adjust the transparency of the chosen color using a value between 0 (fully transparent) and 1 (fully opaque). +### 3. Behavior & Validation + * **Required:** If checked, the user cannot submit the form (or trigger a linked action) without filling this field. + * **Multiline:** Switches the element from a single-line bar to a taller text box (Text Area). + * *Rows:* If Multiline is checked, you can define the default height (number of rows). + * **Email:** Validates for `@` symbol and domain. Adds mobile keyboard support for emails. -Use a visual color picker tool to interactively choose a desired color. +Even if you limit the number of displayed lines, users can still enter more text than what’s visible. The scrollbar functionality will allow them to scroll and view/edit the entire content. -Alternatively, you can inherit the default styles defined in the [theme settings](/user-docs/application-settings#theme) for a cohesive look. +Users can type or paste text within the text area, and they can navigate and edit the text using keyboard shortcuts and mouse clicks. -![Link button color][6] +![Add and configure table elements][4] +## Styling and Theme Overrides - [1]: /user-docs/pages-in-the-application-builder - [2]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/23b2f00d-e440-4629-9392-04e3d1b6fa7e/Untitled.png - [3]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/06d32e8c-84dc-485e-a2ed-85497374accc/Untitled%201.png - [4]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/7b858a00-baa9-437a-8e8c-3f3de8434434/Untitled%202.png - [5]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/b75c9700-e59c-4e5e-97ad-8c2434f908c4/Untitled%203.png - [6]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/9ecea232-7a25-4d26-943c-4d4d32294ce7/Untitled%204.png",,baserow_user_docs,https://baserow.io/user-docs/application-builder-link-element -179,Image element,application-builder-image-element,Baserow Application Builder image element,"# Application Builder - Image element +By default, the input inherits the styles from your [Global Theme](/user-docs/application-settings). To customize a specific input (e.g., to make a ""Danger"" input red), use **Theme Overrides**. -Adding visual elements can greatly enhance user experience. In this section, we'll walk you through setting up a display image element and explain each configuration option in detail. +1. In the General tab, look for the **Style/Settings icon** (slider icon) next to the **Label** or **Input** sections. +2. Clicking this opens the specific styling menu for that part of the element. -## Overview +### Styling the Label +* **Typography:** Font family, weight, size, and color. +* **Alignment:** Left, Center, or Right align the label text. -For the image element, you can configure the element properties and [style](/user-docs/element-style) from the element settings. +### Styling the Input Box +* **Text:** Font size and color of the text *inside* the box. +* **Background:** Fill color of the input field. +* **Border:** Color, width, and radius (rounding) of the box edges. +* **Padding:** Spacing inside the box. -Setting up a display image element involves a few key configurations: -- **Image file:** Choose the image you want to display via upload or URL. -- **Alt text:** This is a brief description of the image. -- **Alignment:** Position the image where you want it on the page – left, right, or center. -- **Image size:** Adjust the width and height of the image in pixels. You can choose specific dimensions or use a percentage to scale the image proportionally. +![Data input - Styling and Theme Overrides][3] -![Baserow Application Builder - Image element][1] +## Frequently asked questions (FAQ) -## Add and configure image elements +### Can I save the input data? +Yes. To save data, you usually place the Data Input inside a **Form Container** or use a **Button** with a ""Create/Update Row"" action. In the action settings, you map the Input's value to a specific database column. -To add an image element, access the [elements panel](/user-docs/elements-overview) and select **Image**. +### How do I limit the character count? +Currently, there is no strict ""Max Length"" setting in the frontend element. -Once added, place the image wherever you want it on the [page][2]. Don't worry if it's not perfectly positioned initially; you can always move it later. -Learn more about how to [add and remove an element from a page](/user-docs/add-and-remove-elements). -![Add and configure table elements][3] +## Related content -Now that you've added the image, it's time to customize its appearance and functionality. This is where image properties come in, including those related to [style](/user-docs/element-style). + * [Form Element][2] + * [Connecting Data Sources](/user-docs/data-sources) + * [Element Styling (Layout vs. Theme)](/user-docs/element-style) -## Image file +--- -When you want to add an image to a page, you have two options: uploading it directly from your device, or providing the web address (URL) where the image is stored online. -### Upload from device +Still need help? If you're looking for something else, please feel free to make recommendations or ask us questions; we’re ready to assist you. -Uploading works best for pictures you have or manage yourself. It's when you pick an image file from your computer, phone, or another device. + - [Ask the Baserow community](https://community.baserow.io) + - [Contact support](/contact) for questions about Baserow or help with your account. -To upload an image directly from your device, click the **Upload** button. This will open a file explorer window on your device. -Locate the desired image file and select it. The Application Builder will then upload the image and make it available for use on the page. + [1]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/cb67f9e5-4ab3-4c88-b4f9-4dba8df4dca8/Untitled.png + [2]: /user-docs/application-builder-form-element + [3]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/4300e9be-d4a4-4426-803e-29f36a38b926/multiline%20input%20-%20Styling%20and%20Theme%20Overrides.png + [4]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/0a0ea05b-9f3a-4d31-a52d-f7bc285021c0/Untitled%202.png",application builder,baserow_user_docs,https://baserow.io/user-docs/application-builder-text-input-element +181,Columns element,application-builder-columns-element,Columns element in Baserow Application Builder,"# Columns element -### By URL +The Columns element is a structural container that allows you to break the default vertical stacking of the page. You can split a row into up to 6 distinct columns, controlling the spacing and alignment of the content inside. -If the image you want to use is stored online, you can link to it instead of uploading it. Linking is preferable when the image is hosted elsewhere. +This page covers how to create multi-column layouts to organize elements side-by-side. -- Click **URL**. -- Paste the complete web address (URL) of the image you want to display in this field. -- Ensure the image is publicly accessible online. The Application Builder will attempt to fetch the image from the provided URL and display it on the page. +## Overview -The Application Builder allows you to bind the URL to [data sources](/user-docs/data-sources). This enables the URL to update dynamically based on user input or application logic. This is helpful for referencing existing images stored elsewhere. +A column container serves as a foundational element in organizing your application. It allows you to arrange content for an intuitive user experience. With column containers, you gain the flexibility to structure and customize your layout. -## Image alt text + * **Structure:** Splits the page width into 1, 2, 3, 4, 5, or 6 equal sections. + * **Container:** You drag other elements (Text, Images, Forms) *inside* the column slots. + * **Alignment:** You can vertically align content (e.g., center a button next to a tall text block). + * **Responsiveness:** Columns automatically stack vertically on smaller mobile screens for readability. -Image alt text, which is short for ""alternative text,"" is important for accessibility and search engine optimization (SEO). +![Baserow column Layout][2] -An image alt text provides a textual description of the image for users who may not be able to see it. The alt text is displayed if the image fails to load properly in a web browser and read aloud by screen readers for the visually impaired, ensuring everyone can understand the image’s purpose. +## Add columns -The Application Builder allows you to bind the text to [data sources](/user-docs/data-sources). This enables the text to update dynamically based on user input or application logic. +1. Open the **Elements** panel (click the `+` icon). +2. Select **Columns**. +3. Drag and drop the element onto your canvas. +4. You will see empty slots (dashed boxes) appear on the canvas. -## Image element horizontal alignment +## Add content to columns -Horizontal alignment refers to placing an image to the left, center, or right side of its parent element, which can be the entire page or a specific section. +Once the structure is in place, you must populate it. -- **Left:** The image will be positioned flush against the left edge of its container. -- **Center:** The image will be horizontally centered within its container. -- **Right:** The image will be positioned flush against the right edge of its container. +1. Add a new element (or select an existing one). +2. Drag it into one of the empty column slots. +3. You can stack multiple elements inside a single column slot (e.g., a Heading above a Text block within the left column). -![Horizontal alignment][4] +## Configuration -## Control image size +Click the Columns element to open the **General** properties tab. -This helps you manage how images are displayed on the page. By setting the image size appropriately, you can ensure they fit seamlessly without appearing stretched or distorted. +### 1. Column Layout +Choose the number of vertical sections. +* **Range:** Select between **1** and **6** columns. +* **Behavior:** Columns are equal width. For example, selecting ""2"" creates a 50%/50% split. Selecting ""3"" creates a 33%/33%/33% split. -There are two options to control image size: +### 2. Space between columns (Gap) +Define the horizontal whitespace between each column slot. +* **Input:** Enter a value in pixels (e.g., `20`). +* **Limit:** Maximum value is 2000. +* **Use Case:** Increase this value to prevent dense text blocks from looking cluttered. -- **Max width (percentage):** This determines the image's maximum width relative to its container. This ensures images don't overflow their container horizontally. -The percentage value must be between 0 and 100, to reflect the desired portion of the container's width the image should occupy. Percentages provide flexible layouts that adapt to different screen sizes. -- **Max height (pixels):** This defines the image's maximum height in pixels. This prevents large images from disrupting the layout. -The value must be between 5 and 3000 pixels to constrain the image's vertical dimension. Pixels provide a fixed value for specific height requirements. +![Space between columns][3] -## Image constraint +### 3. Vertical alignment +Control how elements sit relative to each other when column heights differ. This is critical when placing a small element (like a button) next to a tall element (like a paragraph). -Constraining images ensures a consistent visual experience and prevents unintended layout issues. +| Option | Behavior | Use Case | +| :--- | :--- | :--- | +| **Top** | Content aligns to the top edge. | Standard text layouts. | +| **Middle** | Content centers vertically. | Centering a button next to a tall image. | +| **Bottom** | Content aligns to the bottom edge. | Aligning footer links or ""Read More"" buttons. | -For image element, here are the constraints: +![Column Vertical alignment][4] -- **Extend to max width**: This means the image will stretch horizontally as much as it can without distorting its height. It fills the available space from left to right. -- **Contain**: Contain means the image will fit entirely within its container. This option is unavailable while limiting the height of the image. If the image has a maximum height set, the image won't be able to adhere to it while containing. -- **Cover**: Cover means the image will expand or shrink to cover the entire container, maintaining its aspect ratio. This may result in parts of the image being cropped if it doesn't perfectly fit the container's dimensions. This option is unavailable when the max height is not set. +## Frequently asked questions (FAQ) +### What happens on mobile devices? +To ensure your application is mobile-friendly, Baserow automatically ""stacks"" the columns. On a phone screen, Column 1 will appear at the top, followed by Column 2 beneath it, and so on. - [1]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/dd35bc3f-f94a-494d-a55d-4f2c0c4c45bf/Untitled.png - [2]: /user-docs/pages-in-the-application-builder - [3]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/910666b3-25bb-48d3-80d6-dcfce5681e58/Untitled%201.png - [4]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/354c98bb-cdac-4796-9134-aaf1e73859df/Untitled%202.png",,baserow_user_docs,https://baserow.io/user-docs/application-builder-image-element -180,Text input element,application-builder-text-input-element,Text input element in Baserow App Builder,"# Application Builder - Text input element +### Can I change the background color of a column? +The Columns element itself is purely structural and does not have visual styling properties like backgrounds or borders. To add a background, you should style the specific elements *inside* the column (e.g., add a Container or styled Group if available, or style the individual text/image elements). -The text input element allows users to enter text data in your application. +### Can I nest columns? +Currently, you cannot place a Columns element inside another Columns element. To achieve complex grids, try using a **Table** or **Repeat** element depending on your data needs. -This section will guide you through setting up a text input element and explain each configuration option in detail. +## Related content -![single-line input field][1] + * [Add and remove elements][147] + * [Overview of Elements][146] + * [Managing Page Layouts][143] -## Overview +--- -The text input element allows users to enter textual information. This is important for collecting various data types, including names, email addresses, URLs, and phone numbers. Text inputs are commonly used together with the [form element][2] to create user input forms. -For the text input element, you can configure the element properties, and [customize its appearance](/user-docs/element-style) from the element settings. +Still need help? If you're looking for something else, please feel free to make recommendations or ask us questions; we’re ready to assist you. -## Add and configure text input elements + - [Ask the Baserow community](https://community.baserow.io) + - [Contact support](/contact) for questions about Baserow or help with your account. -To add a text input element, access the [elements panel](/user-docs/elements-overview) and select **Text input**. + [1]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/e338a239-e015-4e96-bcec-f2c8da4cfc31/Untitled.png + [2]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/82e294ec-5047-4f59-aa27-dc8c0906d67d/Untitled%201.png + [3]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/85360cc8-c0ba-40af-ae0a-a3e482d8fb3e/Untitled%202.png + [4]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/754cf382-747d-4cf9-bec9-2b57ae6589a6/Untitled%203.png + [143]: /user-docs/pages-in-the-application-builder + [146]: /user-docs/elements-overview + [147]: /user-docs/add-and-remove-elements",application builder,baserow_user_docs,https://baserow.io/user-docs/application-builder-columns-element +182,Button element,application-builder-button-element,Button element in Baserow Application Builder,"# Application Builder - Button element -Once added, place the text input wherever you want it on the [page](/user-docs/pages-in-the-application-builder). Don't worry if it's not perfectly positioned initially; you can always move it later. +The Button element is the primary ""Call to Action"" (CTA) in your application. Unlike a simple [Link element][1], buttons are designed to trigger complex workflows; such as creating database rows, sending emails, or executing API requests. -Learn more about how to [add and remove an element from a page](/user-docs/add-and-remove-elements). +This page covers how to trigger actions, navigate pages, or submit forms with interactive buttons. -![Add and configure table elements][3] +## Overview -Now, you'll configure the text input's properties to make it function and look the way you want. +The Button element is like a trigger that lets users do something when they click it. It’s a way to make things happen with a simple click, making actions easy and quick for users. -## Text input properties + * **Function:** Triggers **Events** (actions) when clicked. + * **Content:** The label can be static text (""Submit"") or dynamic data (""Edit `{{ Item Name }}`""). + * **Styling:** Colors, typography, and borders are managed via **Theme Overrides**. + * **Layout:** Width (Auto vs. Full) and Alignment are managed in the **Style Tab**. -The text input element has the following common settings: +![Button element][2] -- **Label:** A descriptive label displayed above the field. -- **Default value:** The text pre-filled in the field when the application loads. -- **Placeholder:** Hint text displayed within the field when empty. -- **Required:** Determines if the user must enter a value. -- **Multiline:** Enables users to enter text across multiple lines. You can configure the number of lines displayed initially. +## Add a button -The Application Builder allows you to bind the text to [data sources](/user-docs/data-sources). This enables the text to update dynamically based on user input or application logic. +1. Open the **Elements** panel (click the `+` icon). +2. Select **Button**. +3. Drag and drop the element onto your canvas. -By customizing these style properties, you can create text inputs that match the look and feel of the page. +## Configuration -## Multiline text input +Click the button to open the **General** properties tab. -A multiline input field is used to create a field where users can input multiple lines of text. It differs from a single-line input field by allowing users to enter and edit text across multiple lines. +### 1. Button Text +Enter the label displayed on the button. +* **Static:** Type a standard label like ""Submit"", ""Read More"", or ""Contact Us"". +* **Dynamic:** Click the **Connect Data** icon to bind the text to a [Data Source][3]. + * *Use Case:* ""Update `{{ User Name }}`"" – ensures the user knows exactly which record they are acting upon. -Multiline inputs are commonly used for various purposes, such as user comments, message inputs, or any form field that requires the user to constrain text to paragraphs or longer descriptions. +### 2. Events (Actions) +The most important part of a button is what it does. +1. Click the **Events** tab in the properties panel. +2. Add an **On Click** trigger. +3. Add actions such as: + * **Open Page:** Navigate to another screen. + * **Create/Update Row:** Save data to your database. + * **Show Notification:** Display a success message. + * **Send Email:** Trigger an SMTP email. -![Multiline input][4] +> **Tip:** You can stack multiple actions. For example, *Create Row* -> *Send Email* -> *Show Notification*. [Learn more about configuring events][4]. -Note: Even if you limit the number of displayed lines, users can still enter more text than what's visible. The scrollbar functionality will allow them to scroll and view/edit the entire content. +![Button Styling and Theme Overrides][7] -Users can type or paste text within the text area, and they can navigate and edit the text using keyboard shortcuts and mouse clicks. +## Styling and Theme Overrides +By default, the button inherits the ""Primary Button"" style defined in your [Global Theme][5]. To customize a specific button (e.g., creating a red ""Delete"" button), use **Theme Overrides**. - [1]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/cb67f9e5-4ab3-4c88-b4f9-4dba8df4dca8/Untitled.png - [2]: /user-docs/application-builder-form-element - [3]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/e832d8d9-5673-4ddc-8be4-f01b0edeef39/Untitled%201.png - [4]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/0a0ea05b-9f3a-4d31-a52d-f7bc285021c0/Untitled%202.png",,baserow_user_docs,https://baserow.io/user-docs/application-builder-text-input-element -181,Columns element,application-builder-columns-element,Columns element in Baserow Application Builder,"# Application Builder - Columns element +1. In the General tab, click the **Style/Settings icon** (slider icon) next to the **Button Text** field. +2. This opens the **Theme Overrides** menu where you can configure: -To make interfaces easy to use, you need a good layout plan. Using columns helps you arrange your page content well. +### Visual Properties +* **Typography:** Font family, weight, and size. +* **Color:** Background color and text color. +* **Border:** Width, color, and radius (rounding). +* **Padding:** Internal spacing between the text and the button edge. -Let's go over how to set up column containers. This will help your interface match your data just right. +### Button States +Buttons change appearance based on user interaction. You can style each state independently: +* **Default:** The normal appearance. +* **Hover:** When the mouse cursor is over the button. +* **Active:** The moment the button is clicked. -## Overview +## Layout and alignment -A column container serves as a foundational element in organizing your application. It allows you to arrange content for an intuitive user experience. With column containers, you gain the flexibility to structure and customize your layout. +To control how the button sits on the page (width and position), switch to the **Style** tab at the top of the properties panel. -For the column element, you can configure the element properties and [style](/user-docs/element-style) from the element settings. +### Width +* **Auto:** The button width shrinks to fit the text content. Good for standard actions. +* **Full Width:** The button stretches to fill the entire container. Good for mobile layouts or major calls to action. -## Add and configure column elements +### Alignment +* **Left / Center / Right:** Controls where the button sits within its container (if width is set to ""Auto""). -To add a column element, access the [elements panel](/user-docs/elements-overview) and select **Columns**. +## Frequently asked questions (FAQ) -Once added, place the container wherever you want it on the [page](/user-docs/pages-in-the-application-builder). Don't worry if it's not perfectly positioned initially; you can always move it later. +### Can I put an icon in a button? +Currently, the Button element is text-only. To create an icon button, consider using a standard **Image** element and checking if it supports ""On Click"" events, or place a small image next to a text link. -Learn more about how to [add and remove an element from a page](/user-docs/add-and-remove-elements). +### How do I disable a button? +You cannot strictly ""disable"" a button based on logic (e.g., ""disable if form is empty"") in the current version. However, you can use **Element Visibility** to hide the button entirely if certain conditions (like User Role) are not met. -![Add and configure table elements][1] +### Why is my button stretching? +Check the **Style Tab**. If **Width** is set to ""Full Width,"" it will expand. Set it to ""Auto"" and check the **Alignment** settings to fix it. -After defining the column layout for your application, you can customize its appearance and behavior by configuring the container's properties. +## Related content -## Column layout + * [Configuring Events (Workflows)][4] + * [Link Element vs. Button Element][1] + * [Element Styling (Layout vs. Theme)][8] -The layout option varies from 1-6 columns and allows you to determine the desired arrangement of elements within your interface. +--- -By specifying a number between 1 and 6, you define the structure and presentation of the page. This impacts how data is organized and displayed, influencing usability and visual appeal. -![Baserow column Layout][2] +Still need help? If you're looking for something else, please feel free to make recommendations or ask us questions; we’re ready to assist you. -## Space between columns + - [Ask the Baserow community](https://community.baserow.io) + - [Contact support](/contact) for questions about Baserow or help with your account. -The space between columns is adjustable, ensuring optimal spacing and visual clarity within your interface. -This field's value must be less than or equal to 2000. + [1]: /user-docs/application-builder-link-element + [2]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/b446aa87-7b31-4b84-b4e3-54df3082f690/button%20element.png + [3]: /user-docs/data-sources + [4]: /user-docs/element-events + [5]: /user-docs/application-settings + [6]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/a1669635-fa04-4ac9-854b-e67e6808a1fb/Untitled%203.png + [7]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/ff8afcea-f0a2-4cf4-8bfe-85668a516153/Button%20-%20Styling%20and%20Theme%20Overrides.png + [8]: /user-docs/element-style",application builder,baserow_user_docs,https://baserow.io/user-docs/application-builder-button-element +183,Table element,application-builder-table-element,Table element in Baserow Application Builder,"# Application Builder - Table element -![Space between columns][3] +The Table element is the core component for displaying collections of data. It connects to a ""List Rows"" data source and renders the results as a grid. You can define exactly which columns to show, map them to dynamic data, and add buttons to drill down into record details. -## Column vertical alignment +This page covers how to display a list of database records in a structured, customizable grid. -The vertical alignment for your column container element can align content at the top, middle, or bottom of the container. +## Overview -- **Top alignment:** Selecting top alignment positions the content of the column container element at the top of the container. This is useful for situations where you want content to start from the top edge of the container. -- **Middle alignment:** Opting for middle alignment centers the content vertically within the column container element. It evenly distributes content, ensuring a balanced presentation. -- **Bottom alignment:** Choosing bottom alignment aligns the content at the bottom of the container element. This is for scenarios where you want content to end at the bottom edge of the container. + * **Data Source:** Requires a Data Source set to **""List multiple rows""**. + * **Fields:** You manually define the columns (fields) for the table. You can map text, tags, booleans, and links. + * **Interactivity:** Supports pagination (""Show More""), filtering, sorting, and searching for end-users. + * **Navigation:** You can add **Link Fields** to route users to a ""Details Page"" for a specific row. + * **Styling:** Visuals like borders, header colors, and fonts are managed via **Theme Overrides**. -![Column Vertical alignment][4] +![Table element][1] +## Add a table - [1]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/e338a239-e015-4e96-bcec-f2c8da4cfc31/Untitled.png - [2]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/82e294ec-5047-4f59-aa27-dc8c0906d67d/Untitled%201.png - [3]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/85360cc8-c0ba-40af-ae0a-a3e482d8fb3e/Untitled%202.png - [4]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/754cf382-747d-4cf9-bec9-2b57ae6589a6/Untitled%203.png",,baserow_user_docs,https://baserow.io/user-docs/application-builder-columns-element -182,Button element,application-builder-button-element,Button element in Baserow Application Builder,"# Application Builder - Button element +1. Open the **Elements** panel (click the `+` icon). +2. Select **Table**. +3. Drag and drop the element onto your canvas. -Buttons are key for interaction in the Baserow Application Builder. They let users start actions. +## Configuration -We'll show you how to set up a button and go over each setting option carefully. +Click the table element to open the **General** properties tab. -## Overview +### 1. Data Source +Select the source of your data. +* **Requirement:** The Data Source must use the **List multiple rows** service type. If you don't see your source listed, check that it is configured correctly in the Data tab. +* **Items per page:** Define how many rows appear initially (max 100). A **""Show More""** button will automatically appear if there are more records to load. -The Button element is like a trigger that lets users do something when they click it. It's a way to make things happen with a simple click, making actions easy and quick for users. +### 2. Table Fields (Columns) +Unlike a database view, the Table element starts empty. You must add fields to define what columns appear. -For the button element, you can configure the element properties, [style](/user-docs/element-style), and [events](/user-docs/element-events) from the element settings. +**To add a column:** +1. Click **+ Add field**. +2. **Name:** Enter the column header text (e.g., ""Customer Name""). +3. **Type:** Select the input type (Text, Link, Boolean, or Tags). +4. **Value:** Bind this to your data. Click the **Connect Data** icon and select the corresponding field from your Data Source (e.g., `Current Row > Name`). -The Application Builder allows you to bind the text to [data sources](/user-docs/data-sources). This enables the text to update dynamically based on user input or application logic. +![Add a new field][2] -## Add and configure button elements +**Field Types:** + * **Text:** Displays plain text or dynamic strings. + * **Link:** Creates a button or hyperlink. Essential for navigation. + * **Boolean:** Displays a True/False value (often rendered as a checkmark or status). + * **Tags:** Displays colored badges/labels. + * Button + * Image + * Rating -To add a button element, access the [elements panel](/user-docs/elements-overview) and select **Button**. +### 3. Orientation +Control how the table adapts to different screens. +* **Horizontal:** Standard table layout. Fields appear in a row left-to-right. +* **Vertical:** Stacked layout. Fields appear on top of each other for each record (card style). Best for mobile views. -Once added, place the button wherever you want it on the [page](/user-docs/pages-in-the-application-builder). Don't worry if it's not perfectly positioned initially; you can always move it later. +![Sort fields][3] -Learn more about how to [add and remove an element from a page](/user-docs/add-and-remove-elements). +## Master-Detail workflow: Linking to a details page -![Add and configure button elements][1] +A common pattern is to show a list of items (Table), and let the user click one to see more info (Details Page). -Now, you'll configure the button's properties to make it function and look the way you want. This involves settings related to [button events](/user-docs/element-events), like what happens when it's clicked, and its [style](/user-docs/element-style). +**Step 1: Prepare the Destination Page** +Ensure you have a separate page set up with a [Path Parameter][4] (e.g., `/project/:id`) to accept the Record ID. -## Button horizontal alignment +**Step 2: Add a Link Field** +1. In your Table element configuration, add a new field. +2. Set **Type** to **Link**. +3. Set **Link Text** (e.g., ""View Details""). +4. Set **Navigate to** to **Page**. +5. Select your destination page. +6. **Map the Parameter:** A field will appear for your page parameter (e.g., `:id`). Click the **Connect Data** icon and select `Current Row > Row ID`. -There are several ways to achieve horizontal alignment for your button element within the Application Builder. +Now, when a user clicks ""View Details"" on row #5, they will be sent to `/project/5`. -The horizontal alignment property controls how a button element is positioned within its container on the page. This helps you create a visually appealing and user-friendly page. +![Link row to a details page][5] -- **Left:** Aligns the button to the left edge of its container. -- **Center:** Aligns the button horizontally in the center of its container. -- **Right:** Aligns the button to the right edge of its container. +## Styling and Theme Overrides -![Button Horizontal alignment][2] +By default, the table inherits the basic table styles from your [Global Theme](/user-docs/application-settings). To customize the look (borders, header colors, fonts), use **Theme Overrides**. -## Button width +1. In the General tab, click the **Style/Settings icon** (slider icon) at the top of the configuration panel. +2. This opens the **Table Styling** menu with two tabs: -Consider your design and user experience goals when choosing the button width. +### Tab 1: Table Styling +Customize the structural lines and colors. +* **Borders:** Toggle borders for the outer table, headers, or cells. Set width and color. +* **Header:** Custom background color and text alignment for the top row. +* **Cells:** Padding and alignment for the data rows. +* **Separators:** Style the lines between rows. +* **Show More Button:** Customize the color and style of the pagination button. -When it comes to the appearance of buttons, you have two options: ""auto"" and ""full width."" +### Tab 2: Typography +Customize the fonts used inside the grid. +* **Body Font:** Set the font family, weight, and size for the data text. +* **Colors:** Define text colors for different states. -- **Auto width:** This means the button will adjust its width automatically based on the content inside. If your button text is short, the button will be narrower; if it's longer, the button will widen accordingly. -- **Full width:** Choosing this option means the button will stretch across the entire width of its container, regardless of the text inside. It provides a more expansive look and can be visually impactful. +## User interactions -![Button width][3] +You can allow external users to find data within your table. In the element properties panel, look for the **User Actions** section. -## Button color +* **Filter:** Enable specific fields to be filterable by the user. +* **Sort:** Allow users to click headers to reorder rows. +* **Search:** Enable a search bar to query specific text fields. -Before publishing the application button, consider styling the buttons. +![image: filter_order_and_search_for_published_applications][7] -You can easily modify the color in the Application Builder. +## Frequently asked questions (FAQ) -Learn more about [element style](/user-docs/element-style). +### Can I change the order of columns? +Yes. In the **Fields** configuration list, grab the handle (six dots) on the left of any field and drag it up or down to reorder the columns. -Navigate to the page where you can edit the heading you want to modify and select the heading element within the editor. Change the color of a heading element using the property within a General tab. +### Why is my table empty? +Check your **Data Source**. Ensure it is set to ""List multiple rows"" and that the source itself is returning data (check the filters on the Data Source). Also, ensure you have added at least one **Field** and mapped it to a value. -Click on the color picker or input field next to the color option. +### Can I hide the ""Show More"" button? +The button only appears if there are more records than the **Items per page** limit. To ""hide"" it, increase the limit (max 100), though it will still appear if you have >100 rows. -Set the desired color of the heading using one of these methods: +## Related content -- **Hexadecimal color code:** Enter a six-digit code preceded by a hashtag (#), like #FF0000 for red. -- **RGB value:** Specify the red, green, and blue values (0-255) separated by commas, like RGB (255, 0, 0) for red. -- **Opacity:** Adjust the transparency of the chosen color using a value between 0 (fully transparent) and 1 (fully opaque). + * [Connecting Data Sources](/user-docs/data-sources) + * [Creating a Details Page (Parameters)][4] + * [Styling Elements](/user-docs/element-style) -Use a visual color picker tool to interactively choose a desired color. +--- -Alternatively, you can inherit the default styles defined in the [theme settings](/user-docs/application-settings#theme) for a cohesive look. -![Button color][4] +Still need help? If you're looking for something else, please feel free to make recommendations or ask us questions; we’re ready to assist you. -## Button events + - [Ask the Baserow community](https://community.baserow.io) + - [Contact support](/contact) for questions about Baserow or help with your account. -On the right side of the page, you'll find the **Events** tab. It has a dropdown menu where you can choose what the button will do when someone clicks on it. -A button element can perform the following actions: + [1]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/00577615-558a-4a18-9586-17fefd08e031/Table%20element.png + [2]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/55181490-ee96-4b27-832c-f8408df90631/Untitled%203.png + [3]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/fa3d40c5-ac29-4b34-835c-005aa7240ce0/Untitled%2011.png + [4]: /user-docs/application-builder-page-settings + [5]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/fd769e3e-d098-4157-95c7-5338cbd9b6c2/Untitled%207.png + [6]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/c4821c54-88e8-4003-8de5-c2de03937b25/Untitled%2010.png + [7]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/1b4b3701-6795-404b-86ef-2cfa24391ec0/filter_order_and_search_for_published_applications.webp",application builder,baserow_user_docs,https://baserow.io/user-docs/application-builder-table-element +184,Form element,application-builder-form-element,Form element in Baserow Application Builder,"# Form element -- **Show notification**: This means it can display a message or alert to the user. -- **Open page**: Clicking the button can take the user to another page or website. -- **Create row**: It can add a new row of data. The action requires an integration to be used. -- **Update row**: This action allows modifying or editing existing data in a row. The action requires an integration to be used. +The Form element acts as a **container** that groups input fields together. It includes a built-in ""Submit"" button that triggers actions (like creating a row) only when all required fields inside the container are valid. -Learn more about [element events](/user-docs/element-events). +This page covers how to collect user data, validate inputs, and submit information to your database. +## Overview - [1]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/fd21d4ec-5872-4d92-afba-d2d994adbbf5/Untitled.png - [2]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/7bcd5bd2-6850-40ac-9da9-a1bc1fd9e438/Untitled%201.png - [3]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/aadce328-9630-440e-8bf5-f47459ab979d/Untitled%202.png - [4]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/a1669635-fa04-4ac9-854b-e67e6808a1fb/Untitled%203.png",,baserow_user_docs,https://baserow.io/user-docs/application-builder-button-element -183,Table element,application-builder-table-element,Table element in Baserow Application Builder,"# Application Builder - Table element +* **Container Logic:** You drag [Data Input][155], [Checkbox][161], or [Dropdown][160] elements *inside* the Form element to build your form. +* **Submit Action:** The form handles validation automatically. The ""On Submit"" event only fires if all required fields are filled correctly. +* **Workflow:** Typically used with the **Create Row** action to save data to a table. +* **Styling:** The look of the Submit button (Colors, Hover States, Padding) is managed via **Theme Overrides**. -The Table element works exactly like the grid view. It displays the data from the data source in a table layout. +![Form element][1] -In this section, we'll guide you through the process of setting up a table element and explaining each configuration option in detail. +## Add a form -## Overview +1. Open the **Elements** panel (click the `+` icon). +2. Select **Form**. +3. Drag and drop the element onto your canvas. +4. *Result:* You will see a container with a default ""Submit"" button. -For the table element, you can configure the element properties and [style](/user-docs/element-style) from the element settings. +## Build your form (Adding inputs) -To get started, you need to connect a pre-configured [data source](/user-docs/data-sources) to fetch data from data sources and display it on the table. +The Form element is empty by default (except for the button). To make it functional, you must add input fields. -As soon as you add a table element to the page, the element settings will become available and you can start setting it up. +1. Select an input element from the library (e.g., **Data Input** or **Checkbox**). +2. Drag it **inside** the Form container box. +3. Repeat for as many fields as you need. -![Baserow Application Builder table element][1] +> **Tip:** Use [Column elements][156] inside the Form container to create multi-column layouts (e.g., First Name and Last Name side-by-side). -## Add and configure table elements +## Configuration -To add a table element, access the [elements panel](/user-docs/elements-overview) and select **Table**. +Click the Form element (the outer container) to open the **General** properties tab. -Once added, place the table wherever you want it on the [page](/user-docs/pages-in-the-application-builder). Don't worry if it's not perfectly positioned initially; you can always move it later. +### 1. Submit Button Text +Define the label on the button. +* **Static:** Type standard text like ""Register"", ""Send Message"", or ""Save"". +* **Dynamic:** Click the **Connect Data** icon to bind the text to a [Data Source][144] (e.g., ""Join `{{ Event_Name }}`""). -Learn more about how to [add and remove an element from a page](/user-docs/add-and-remove-elements). +### 2. Form Logic +* **Reset to default values after submission:** + * **Enabled:** After the user clicks submit and the action succeeds, all inputs inside the form are cleared or reset to their default values. Perfect for ""Contact Us"" forms. + * **Disabled:** The data remains in the fields after submission. Perfect for ""Edit Profile"" forms where the user might want to make further changes. -![Add and configure table elements][2] +## Events (Workflows) -Now, you'll configure the table's properties to make it function and look the way you want. This involves settings related to table style. +The primary power of the form is the **On Submit** event. -## Table data source +1. Click the **Events** tab in the properties panel. +2. Add the **On Submit** trigger. +3. Add an action, typically **Create Row**. +4. **Map Fields:** In the action settings, map your database columns to the specific Input Elements inside your form. + * *Example:* Map the *Email* column to the *Data Input - Email* element. -> To list rows in the table, set the [data source](/user-docs/data-sources) service type as *List rows*. Learn more about data sources and how to configure them. -> -After adding a table element to the page from **Elements**, you need to select the [data source](/user-docs/data-sources) which you want to import your data from. This is done from the **General** tab of the element settings. -![Data source][3] +## Styling and Theme Overrides +To customize the **Submit Button's** appearance, you must use a **Theme Override**. -As soon as you select a **Data source**, you'll see the Fields configuration appear below. +1. In the General tab, click the **Style/Settings icon** (slider icon) next to the **Submit Button** configuration. +2. This opens the **Button Styling** menu where you can override: -## Table items per page +### Visual Properties +* **Typography:** Font family, weight, and size. +* **Alignment:** Position the button (Left, Center, Right, Full Width). +* **Color:** Background and text color. +* **Border & Radius:** Style the edges and rounding. +* **Padding:** Control the size of the button. -You can choose how many items appear in the list by default. The field must be an integer and must be less than or equal to 100. +### Interactive States +You can define distinct styles for user interactions: +* **Default:** Normal appearance. +* **Hover:** When the mouse is over the button. +* **Active:** When the button is being clicked. -If there are more items to display than the defined number, a **Show More** button will be added at the end of the list, allowing the user to expand the list and view the additional items. -## Table fields -This is where you configure how the data will be mapped to a table to specify how exactly you want to display your data. +## Frequently asked questions (FAQ) -You can configure some additional parameters for the table element and map the element fields to the data source to specify what data needs to be displayed and how. +### Can I have two forms on one page? +Yes. Each Form element is independent. Clicking ""Submit"" on Form A will not trigger Form B, even if they are on the same page. -For each field, you can add a Name, Type, Value, and Link text. You can also configure the position of the fields. +### How do I validate the data? +Validation is handled by the Input elements themselves. If you mark a [Data Input][155] as ""Required"" or set it to ""Email"" type, the Form will automatically block submission and show an error if the user makes a mistake. -### Add a new field +### Can I hide the Submit button? +No, the Submit button is integral to the Form element. If you need a custom button layout (e.g., a button *outside* the form), you might need to use individual input elements and a standalone Button element with specific workflows, though the Form element is recommended for easiest data handling. -You can add new fields using the **Add field** button. After adding the field, you need to specify its Name, Type, Value, and Link text. +## Related content -Fields can have the following settings: + * [Data Input Element][155] + * [Configuring Events (Create Row)][149] + * [Element Styling][148] -- Name – a title for the column -- Type -- Value -- Link text -- Navigate to -- Parameter +--- -![Add a new field][4] -### Field name +Still need help? If you're looking for something else, please feel free to make recommendations or ask us questions; we’re ready to assist you. -Here you set the title of the field. The name of the field can be modified as needed. + - [Ask the Baserow community](https://community.baserow.io) + - [Contact support](/contact) for questions about Baserow or help with your account. -### Field type + [1]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/2496bc41-709f-4530-bf3b-84fe5c31e922/Untitled.png + [2]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/0fe1dc92-f78e-4945-908b-df6850671e85/Untitled%201.png + [3]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/b6f3848b-538f-4261-9cb5-bf92dc33d03f/Untitled%202.png + [4]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/33255c0e-ca8e-4350-9768-fb3bc70ef82d/Untitled%203.png + [144]: /user-docs/data-sources + [148]: /user-docs/element-style + [149]: /user-docs/element-events + [155]: /user-docs/application-builder-text-input-element + [156]: /user-docs/application-builder-columns-element + [160]: /user-docs/application-builder-dropdown-element + [161]: /user-docs/application-builder-checkbox-element",application builder,baserow_user_docs,https://baserow.io/user-docs/application-builder-form-element +185,Choice element,application-builder-dropdown-element,Choice element in Baserow Application Builder,"# Application Builder - Choice element -Choosing the appropriate field type ensures your table data is stored and displayed effectively. The table element supports the following field types: +The Choice element (formerly the Dropdown element) is a flexible input tool. It allows you to present a predefined list of options to your users. Depending on your configuration, this can look like a compact dropdown menu, a list of radio buttons for single selection, or a group of checkboxes for multiple selections. - - **Text**: This is the most versatile type, suitable for any textual content, including names, descriptions, or short paragraphs. - - **Link**: Use this type to create clickable links within your table cells. When clicked, these links navigate users to external websites or internal pages within your application. - - **Boolean**: This type represents a true/false value. It's ideal for capturing binary data like ""Active/Inactive"" or ""Yes/No"" flags. - - **Tags**: This type allows users to assign labels or categories to table cells. Tags are commonly used for filtering, sorting, or grouping data based on these labels. +This page covers how to allow users to select one or multiple options via dropdowns, radio buttons, or checkboxes. -### Field value +![Choice element for Application Builder][1] -If the Text type is selected, you can also set the value of the field. +## Overview -You can enter a specific value for the selected field so that the rows display that value. +Choice elements are versatile components used for various purposes, including creating navigation menus and selecting options from a predefined list. -You can enter static text here. However, if you've connected to a [data source](/user-docs/data-sources), all the fields from the data source will also become available for your choice. + * **Versatility:** Can be configured as a **Dropdown**, **Radio List** (Single Select), or **Checkbox List** (Multi-select). + * **Options:** You can define static options manually *or* pull dynamic options from a [Data Source][2]. + * **Validation:** Supports ""Required"" fields and default values. + * **Styling:** Visual properties (Colors, Borders, Padding) are managed via **Theme Overrides**. -![Enter value][5] +> The [Record Selector element][3] enhances how users link and select related rows from other tables, making it especially useful for handling large datasets. Learn more about how to [use the Record Selector element][3]. -### Field link text +## Add a choice element -If the Link type is selected, you can also set the link text of the field. +1. Open the **Elements** panel (click the `+` icon). +2. Select **Choice**. +3. Drag and drop the element onto your canvas (typically inside a [Form Container][4]). -Similar to the text value, you can enter static text here. However, if you've connected to a [data source,](/user-docs/data-sources) all the fields from the data source will also become available for your choice. +## Configuration -![Enter Link text][6] +Click the element to open the **General** properties tab. -### Navigate to +### 1. Content & Logic +* **Label:** The descriptive text displayed above the field (e.g., ""Select Department""). +* **Placeholder:** Hint text displayed inside a dropdown when empty (e.g., ""Choose one...""). +* **Value:** The option that is selected automatically when the page loads. -If the Link type is selected, you can also link to an internal page or custom URL through a button. +### 2. Defining Options +You must define what users can pick. You have two methods: -![Navigate to][7] +**Method A: Static Options (Manual)** +Use this for fixed lists that rarely change (e.g., ""Yes/No"" or ""High/Medium/Low""). +1. Click **+ Add option**. +2. **Name:** The text the user sees (e.g., ""High Priority""). +3. **Value:** The data saved to the database (e.g., `high_priority`). -### Link row to a details page +**Method B: Dynamic Options (Data Source)** +Use this to populate the list from your database (e.g., a list of Active Employees). +1. Click the **Data Source** dropdown under ""Options source"". +2. Select a connected Data Source. +3. **Name Field:** Select the column to display to the user (e.g., `Employee Name`). +4. **Value Field:** Select the column to save (e.g., `Employee ID`). -If the Link type is selected, you can link to an internal page through a button and set it to open a separate page within the Baserow application to view the details of a row. +### 3. Selection & Display Behavior +Control how the user interacts with the options. -> In the detail page, you should link to the same data source that the table to which you want to connect the details is linked to. -> +* **Allow multiple values:** + * **Off (Single Select):** User can pick only one option. + * **On (Multi-select):** User can pick several options. -For example, let’s say you have a list of projects in a table. When the user clicks on the link associated with a specific project, you want to display this project’s details. +* **Display:** + * **Dropdown:** Options are hidden inside a collapsible menu. Best for long lists to save space. + * **List (Radio/Checkbox):** Options are displayed as an open list on the page. + * *If Single Select:* Displays as **Radio Buttons**. + * *If Multi-select:* Displays as **Checkboxes**. -You need to first add a parameter via `:parameter` . Path parameters can be used to load data, depending on the provided parameter dynamically. Then map the row ID in the data source configuration. +### 4. Validation +* **Required:** If checked, the user cannot submit the form without making a selection. -![Link row to a details page][8] +## Styling and Theme Overrides -To link to a dynamic page, go to the element settings and open the General tab. From the **Navigate to** dropdown menu, set the detail page to which the table items are supposed to be linked. +By default, the element inherits styles from your [Global Theme](/user-docs/application-settings). To customize a specific element (e.g., to make the dropdown background blue), use **Theme Overrides**. -![ink to a dynamic page][9] +1. In the General tab, look for the **Style/Settings icon** (slider icon) next to the **Label** or **Input** sections. +2. Clicking this opens the specific styling menu. -That’s it. Now when the user clicks on a particular link associated with a row, they will be taken to the page with the details of the row. +### Styling the Label +* **Typography:** Font family, weight, size, and color. +* **Alignment:** Left, Center, or Right. -The content for each row will be generated automatically on that page depending on which row the user has navigated to the detail page from. +### Styling the Input (Box/List) +* **Background:** Color of the dropdown box or option background. +* **Border:** Color, width, and radius. +* **Padding:** Spacing inside the selection box. +* **Typography:** Font settings for the option text. -You can also do the same for a Button element. +## Frequently asked questions (FAQ) -### Delete a field +### When should I use the Record Selector instead? +The **Choice** element is best for simple lists or when you want to display Radio/Checkboxes. If you are dealing with very large datasets (thousands of rows) where a user needs to search and link complex records, the [Record Selector][7] is often a better choice for performance. -![Delete a field][10] +### Can I pre-select multiple options? +Yes. In the **Default Value** field, you can select multiple items if ""Allow multiple values"" is enabled. -## Button color +### How do I save the selection? +Like other input elements, the Choice element does not save data automatically. It must be placed inside a **Form Container** or used with a **Button** action (Create/Update Row) to map the selected `Value` to a specific table column. -You can easily modify the color in the Application Builder. +## Related content -The color of a button refers to the visual appearance of the button itself. + * [Record Selector Element][7] + * [Connecting Data Sources][2] + * [Element Styling (Layout vs. Theme)][8] -You can configure the color of the **Show more** button to show more items in the list. +--- -Navigate to the page where you can edit the heading you want to modify and select the heading element within the editor. Change the color of a heading element using the property within a General tab. -Click on the color picker or input field next to the color option. +Still need help? If you're looking for something else, please feel free to make recommendations or ask us questions; we’re ready to assist you. -Set the desired color of the heading using one of these methods: + - [Ask the Baserow community](https://community.baserow.io) + - [Contact support](/contact) for questions about Baserow or help with your account. -- **Hexadecimal color code:** Enter a six-digit code preceded by a hashtag (#), like #FF0000 for red. -- **RGB value:** Specify the red, green, and blue values (0-255) separated by commas, like RGB (255, 0, 0) for red. -- **Opacity:** Adjust the transparency of the chosen color using a value between 0 (fully transparent) and 1 (fully opaque). -Use a visual color picker tool to interactively choose a desired color. + [1]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/079cc165-afe0-4dc7-b34b-acce64de4b68/Choice%20dropdown%20element.png + [2]: /user-docs/data-sources + [3]: https://baserow.io/user-docs/application-builder-record-selector-element + [4]: /user-docs/application-builder-form-element + [5]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/cb156340-d793-49aa-8cc7-c3cf224da962/Untitled%201.png + [6]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/8edd4b78-a01b-4918-917f-4c286f7c92d6/Untitled.png + [7]: /user-docs/application-builder-record-selector-element + [8]: /user-docs/element-style",application builder,baserow_user_docs,https://baserow.io/user-docs/application-builder-dropdown-element +186,Checkbox element,application-builder-checkbox-element,Checkbox element in Baserow Application Builder,"# Checkbox element -Alternatively, you can inherit the default styles defined in the [theme settings](/user-docs/application-settings#theme) for a cohesive look. +The Checkbox element is a binary input tool. It is commonly used for ""Yes/No"" questions, agreeing to terms and conditions, or toggling a boolean state. Unlike the [Choice element][160] (which handles multiple options), the Checkbox handles a single on/off state. -![Button color][11] +This page covers how to collect True/False input or consent from users. -## Sort fields +## Overview -You can manually reorder the fields with the drag-and-drop functionality. +The Checkbox element is commonly used to give a choice between two options, typically ‘true’ or ‘false.’ When you check the box, it means ‘true,’ and when you leave it unchecked, it means ‘false.’ It’s a way to select one or more options from a list by clicking on them. -![Sort fields][12] +* **Function:** Captures a single `True` (Checked) or `False` (Unchecked) value. +* **Database Mapping:** Best paired with a [Boolean field][38] in your database. +* **Dynamic Binding:** You can control the default state using data from your database (e.g., showing a task as already ""Completed""). +* **Styling:** The visual appearance of the box and the label font are managed via **Theme Overrides**. -## Table element orientation +![Application Builder - Checkbox element][1] -This setting controls how rows in the table element are arranged on the screen. You can define how the fields are displayed for different device types. This allows you to customize the layout for optimal viewing on various screen sizes. +## Add a checkbox -You can choose between two orientations: +1. Open the **Elements** panel (click the `+` icon). +2. Select **Checkbox**. +3. Drag and drop the element onto your canvas (typically inside a [Form Container][159]). - - Vertical: Fields will be stacked on top of each other. - - Horizontal: Fields will be displayed in rows from left to right. +## Configuration -## User actions +Click the element to open the **General** properties tab. -External users can filter, sort, and search within [published applications][13], creating a more interactive and user-friendly experience. +### 1. Option name +This is the text displayed next to the checkbox. +* **Text:** Enter a clear description (e.g., ""I agree to the Terms of Service"" or ""Subscribe to newsletter""). +* **Position:** The label usually appears to the right of the box. -For the table element, you can specify which fields to make filterable, sortable, and searchable for your external users. +### 2. Default Value +Determine the initial state of the checkbox when the page loads. +* **Unchecked (False):** The box starts empty. +* **Checked (True):** The box starts with a tick. +* **Dynamic:** Click the **Connect Data** icon to bind this to a [Data Source][144]. + * *Use Case:* When building an ""Edit Profile"" form, bind this to the user's existing `Email_Opt_In` boolean field. If they are already opted in, the box will load as checked. -To add filtering, ordering, and searching capabilities, click on the table element and navigate to the right sidebar. There, you’ll see checkboxes to enable Filter, Sort, and Search for specific fields. +### 3. Required +Toggle this validation setting. +* **Checked:** The user *must* check the box to submit the form. (Common for ""Terms and Conditions""). +* **Unchecked:** The user can leave the box empty. -![image: filter_order_and_search_for_published_applications][14] +## Styling and Theme Overrides ---- +By default, the checkbox inherits styles from your [Global Theme][140]. To customize a specific checkbox (e.g., to make the tick green or the label bold), use **Theme Overrides**. -Still need help? If you're looking for something else, please feel free to make recommendations or ask us questions—we’re ready to assist you. +1. In the General tab, look for the **Style/Settings icon** (slider icon) next to the **Label** or **Checkbox** configuration fields. +2. Clicking this opens the specific styling menu for that part of the element. - - [Ask the Baserow community](https://community.baserow.io) - - [Contact support](/contact) for questions about Baserow or help with your account +### Styling the Checkbox (The Square) +* **Colors:** Define the background color when unchecked vs. checked, and the color of the tick mark. +* **Border:** Set the width, color, and radius (rounding) of the box edges. +* **Size:** Adjust the physical size of the clickable area. +### Styling the Label (The Text) +* **Typography:** Font family, weight, size, and color. +* **Padding:** Adjust the space between the checkbox and the text. - [1]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/f6b10d57-f288-46d9-b83e-ffdaf2c8fb79/Untitled.png - [2]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/56fa9be7-100e-4b96-bf0e-c32000d89de2/Untitled%201.png - [3]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/69f11760-4aee-4803-8a08-dc3196d65a73/Untitled%202.png - [4]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/55181490-ee96-4b27-832c-f8408df90631/Untitled%203.png - [5]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/71fac19d-55d7-414e-9263-67e52c8222bf/Untitled%204.png - [6]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/8beb966d-9e74-4631-aef9-31ea1ba5c698/Untitled%205.png - [7]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/883b4278-e114-43aa-a9dc-9b85e0ece9c2/Untitled%206.png - [8]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/fd769e3e-d098-4157-95c7-5338cbd9b6c2/Untitled%207.png - [9]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/09d9e959-8e99-4a42-aa8c-bdf04e64eb0a/Untitled%208.png - [10]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/92c43c91-7342-4dd7-9fa4-8a3dd0353009/Untitled%209.png - [11]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/c4821c54-88e8-4003-8de5-c2de03937b25/Untitled%2010.png - [12]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/fa3d40c5-ac29-4b34-835c-005aa7240ce0/Untitled%2011.png - [13]: /user-docs/preview-and-publish-application - [14]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/1b4b3701-6795-404b-86ef-2cfa24391ec0/filter_order_and_search_for_published_applications.webp",,baserow_user_docs,https://baserow.io/user-docs/application-builder-table-element -184,Form element,application-builder-form-element,Form element in Baserow Application Builder,"# Application Builder - Form element +## Frequently asked questions (FAQ) -Using the Form element, you can collect information from your users which you can store in a database, and configure events on submit. +### How do I create a list of checkboxes? +If you need users to select multiple options from a list (e.g., ""Select your interests: Coding, Design, Marketing""), use the **[Choice Element][160]** configured to ""Multiple Select"" mode, or use the **[Checkbox Element][161]** inside a **[Repeat Element][164]**. -In this section, we'll guide you through the process of setting up a form element and explain each configuration option in detail. +### Can I save the checkbox state? +Yes. Place the checkbox inside a **Form Container**. When configuring the form's submission action (e.g., ""Create Row""), map the Checkbox element's value to a **Boolean** field in your table. -## Overview +### Does ""Required"" mean it must be True? +Yes. For a checkbox, ""Required"" validation means the user must check the box (value = True) to proceed. If you want a field that must be answered but can be No, consider using a **Choice** element with ""Yes/No"" options. -For the form element, you can configure the element properties, [style](/user-docs/element-style), and [events](/user-docs/element-events) from the element settings. +## Related content -The form layout is customizable and you can have any number of fields, specifying the type of each field. + * [Form Element][159] + * [Boolean Field][38] + * [Element Styling (Layout vs. Theme)][148] -For example, if you have a job board, each job detail page can contain a form through which visitors can apply for that job. +--- -The Application Builder allows you to bind the text to [data sources](/user-docs/data-sources). This enables the text to update dynamically based on user input or application logic. -![Form element][1] +Still need help? If you're looking for something else, please feel free to make recommendations or ask us questions; we’re ready to assist you. -## Add and configure form elements + - [Ask the Baserow community](https://community.baserow.io) + - [Contact support](/contact) for questions about Baserow or help with your account. -To add a form element, access the [elements panel](/user-docs/elements-overview) and select **Form**. + [1]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/9b1b27c2-05c0-4cec-b863-afea7b4ee2b9/Untitled.png + [2]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/4281431d-3a77-448c-b627-1a7426a088b6/Untitled%201.png + [38]: /user-docs/boolean-field + [140]: /user-docs/application-settings + [144]: /user-docs/data-sources + [148]: /user-docs/element-style + [159]: /user-docs/application-builder-form-element + [160]: /user-docs/application-builder-dropdown-element + [161]: /user-docs/application-builder-checkbox-element + [164]: /user-docs/application-builder-repeat-element",application builder,baserow_user_docs,https://baserow.io/user-docs/application-builder-checkbox-element +187,iFrame element,application-builder-iframe-element,iFrame element in Baserow Application Builder,"# Application Builder - IFrame element -Once added, place the form wherever you want it on the [page](/user-docs/pages-in-the-application-builder). Don't worry if it's not perfectly positioned initially; you can always move it later. +The IFrame (Inline Frame) element creates a window within your application that displays content from another source. It is the primary tool for integrating external services (like Google Forms, YouTube videos, or BI dashboards) without requiring users to leave your page. -Learn more about how to [add and remove an element from a page](/user-docs/add-and-remove-elements). +This page covers how to embed external websites, maps, videos, or custom HTML snippets into your application. -![Add and configure table elements][2] +## Overview -Now, you'll configure the form's properties to make it function and look the way you want. This involves settings related to form events (like what happens when it's clicked) and its style. +An IFrame is an HTML element used to embed another document within the current HTML document. It allows you to display content from another source without the need for the user to navigate away from the current page. -## Form element types +* **Two Modes:** + * **URL:** Point to a specific web address (e.g., `https://example.com`). + * **Embed (HTML):** Paste raw code snippets (e.g., a Twitter/X timeline widget). +* **Dynamic Content:** You can bind the URL or HTML to a Data Source to display different content for each record. +* **Limitations:** Not all websites allow themselves to be embedded. If a site refuses to connect, it is a security setting on *their* end, not Baserow's. -Now, let's see what element types are available: +![Baserow form embed][2] -- **Text input** - an input field that requires the user to type in the value. -- **Dropdown** – a dropdown where users can choose from one of the predefined list of options. For the dropdown element, you will add all the available options**.** -- **Checkbox** - a checkbox field with a yes/no value. +## Add an IFrame -## Form submit button +1. Open the **Elements** panel (click the `+` icon). +2. Select **IFrame**. +3. Drag and drop the element onto your canvas. -In the General tab, you customize the submit button and button color. +## Configuration -You can enter static text as the *Submit button* value. However, if you've connected to a [data source](/user-docs/data-sources), all the fields from the data source will also become available for your choice. +Click the IFrame element to open the **General** properties tab. -![Submit button][3] +### 1. Source Type +Choose how you want to provide the content. -## Form button color +* **URL:** Use this to display a full webpage. +* **Embed:** Use this to render raw HTML code. -You can easily modify the color in the Application Builder. -Navigate to the page where you can edit the heading you want to modify and select the heading element within the editor. Change the color of a heading element using the property within a General tab. +### 2. Content (URL or HTML) +Depending on the Source Type selected above, configure the content: -Click on the color picker or input field next to the color option. +#### If using URL +Enter the direct link to the resource. +* **Static:** Type a fixed URL (e.g., `https://www.wikipedia.org`). +* **Dynamic:** Click the **Connect Data** icon to bind this to a [Data Source](/user-docs/data-sources). + * *Use Case:* In a ""Company Directory"" app, bind the IFrame to a `{{ Company Website }}` field. As users browse different companies, the IFrame updates to show that specific company's site. -Set the desired color of the heading using one of these methods: +> **Security Warning:** The URL must use **HTTPS**. Most modern browsers block insecure HTTP content from loading inside secure applications. -- **Hexadecimal color code:** Enter a six-digit code preceded by a hashtag (#), like #FF0000 for red. -- **RGB value:** Specify the red, green, and blue values (0-255) separated by commas, like RGB (255, 0, 0) for red. -- **Opacity:** Adjust the transparency of the chosen color using a value between 0 (fully transparent) and 1 (fully opaque). +#### If using Embed +Paste the raw HTML code provided by the third-party tool. +* **Static:** Paste a widget code (e.g., a Calendly booking widget). +* **Dynamic:** Bind to a text field containing HTML code from your database. -Use a visual color picker tool to interactively choose a desired color. +![Baserow get your embed code][3] -Alternatively, you can inherit the default styles defined in the [theme settings](/user-docs/application-settings#theme) for a cohesive look. +### 3. Height +Define how tall the IFrame window should be. +* **Input:** Enter a numeric value in pixels (e.g., `600`). +* **Limit:** Maximum height is 2000px. +* **Note:** The width is automatically determined by the container (e.g., the Column or Page width). -![Button color][4] +## Troubleshooting common issues -## Form events +### ""Refused to connect"" or Blank White Box +If you enter a valid URL (like `google.com`) but the IFrame is blank or shows an error icon, the target website is blocking the connection. +* **Cause:** Many major sites (Google, Facebook, Stripe) send an `X-Frame-Options: DENY` header to prevent security risks like clickjacking. +* **Solution:** You cannot force these sites to load. You must use a link intended for embedding (e.g., use `youtube.com/embed/VIDEO_ID` instead of `youtube.com/watch?v=VIDEO_ID`). -In the Events tab, you can set where it's going to be sent. To set the form's destination, you have the following options: +### Content is cut off +If the external content requires scrolling, increase the **Height** property in the element settings. The IFrame does not automatically resize to fit the external content. -- **Show notification**: This means it can display a message or alert to the user. -- **Open page**: Clicking the button can take the user to another page or website. -- **Create row**: It can add a new row of data. The action requires an integration to be used. -- **Update row**: This action allows modifying or editing existing data in a row. The action requires an integration to be used. +## Frequently asked questions (FAQ) -Learn more about [element events](/user-docs/element-events). +### Can I run JavaScript in the Embed mode? +For security reasons, `script` tags and complex JavaScript execution may be restricted within the IFrame to prevent Cross-Site Scripting (XSS) attacks. Simple HTML/CSS widgets work best. +### How do I change the width? +The IFrame automatically fills the width of its parent container. To make it narrower, place the IFrame element inside a **Column** element and adjust the column layout. - [1]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/2496bc41-709f-4530-bf3b-84fe5c31e922/Untitled.png - [2]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/0fe1dc92-f78e-4945-908b-df6850671e85/Untitled%201.png - [3]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/b6f3848b-538f-4261-9cb5-bf92dc33d03f/Untitled%202.png - [4]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/33255c0e-ca8e-4350-9768-fb3bc70ef82d/Untitled%203.png",,baserow_user_docs,https://baserow.io/user-docs/application-builder-form-element -185,Choice element,application-builder-dropdown-element,Choice element in Baserow Application Builder,"# Application Builder - Choice element +## Related content -> We renamed the Dropdown element to the Choice element to better represent its new functionality, which includes displaying a list of radio buttons or checkboxes. + * [Connecting Data Sources](/user-docs/data-sources) + * [Using the Columns Element][156] + * [Text Input Element (for URLs)][155] -The choice element lets users pick from a list of choices. It looks like a list that comes down when you click on it. +--- -In this section, we'll guide you through the process of setting up a choice element and explain each configuration option in detail. -![Choice element for Application Builder][1] +Still need help? If you're looking for something else, please feel free to make recommendations or ask us questions; we’re ready to assist you. -## Overview + - [Ask the Baserow community](https://community.baserow.io) + - [Contact support](/contact) for questions about Baserow or help with your account. -Choice elements are versatile components used for various purposes, including creating navigation menus and selecting options from a predefined list. They offer a user-friendly way to present choices without overwhelming the interface. + [1]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/e823613f-fb69-49b3-a8ee-866ded4c2d0b/Untitled.png + [2]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/5bb39ca3-6ca6-40ae-897f-2c387a25d507/Untitled%201.png + [3]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/cf1bd718-7203-480a-8f1c-f82c007b9dd4/Untitled%202.png + [155]: /user-docs/application-builder-text-input-element + [156]: /user-docs/application-builder-columns-element",application builder,baserow_user_docs,https://baserow.io/user-docs/application-builder-iframe-element +188,Login element,application-builder-login-element,Login element in Baserow Application Builder,"# Application Builder - Login element -> The Record Selector element enhances how users link and select related rows from other tables, making it especially useful for handling large datasets. Learn more about how to use the [Record Selector element][2]. +The Login element provides a pre-built form (Email/Password) that connects to your User Source. It handles the security, validation, and session creation required to log users into your application. -You can configure the element properties and [style](/user-docs/element-style) from the element settings. +This page covers how to securely authenticate users and grant access to private pages and data. -The Application Builder allows you to bind the text to [data sources](/user-docs/data-sources). This enables the text to update dynamically based on user input or application logic. You can also populate the options with data retrieved from data sources. This offers a dynamic way to manage options. +## Overview -![Baserow choice element][3] +The login element is an important component for securing your application. -## Add and configure choice elements + * **Prerequisite:** You must have a configured [User Source][1] (connected to a Users table) before this element will work. + * **Function:** Authenticates the user. If successful, it triggers an ""On Success"" event (usually a redirect). + * **Visibility:** Once logged in, the user's role determines which pages and elements they can see. + * **Styling:** You can style the **Inputs** and the **Button** independently using **Theme Overrides**. -To add a choice element, access the [elements panel](/user-docs/elements-overview) and select **Choice**. +![login element](https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/e55f2618-ae71-4f52-b321-a7912069b0fd/Untitled%202.png) -Once added, place the choice element wherever you want it on the [page](/user-docs/pages-in-the-application-builder). Don't worry if it's not perfectly positioned initially; you can always move it later. +## Add a login form -Learn more about how to [add and remove an element from a page](/user-docs/add-and-remove-elements). +1. Open the **Elements** panel (click the `+` icon). +2. Select **Login**. +3. Drag and drop the element onto your canvas. + * *Tip:* It is best practice to place this on a dedicated ""Login"" page that is visible to **All Visitors**. -![Add and configure table elements][4] +## Configuration -Now, you'll configure the choice element's properties to make it function and look the way you want. This involves settings related to choice element style. +Click the element to open the **General** properties tab. -## Choice element configuration +### 1. User Source (Required) +You must tell the element which database to check credentials against. +* Select an existing **[User Source][1]** from the dropdown. +* If the dropdown is empty, go to Application **Settings > User Sources** to create one or add a new source from the dropdown menu. -Choice elements are frequently used for navigation menus, or selecting options from a list. Users can choose from one of the predefined list of options you add. +![Choose a user source to use the login element][2] -The choice element has the following settings: +### 2. Button Label +Customize the text displayed on the submission button. +* *Default:* ""Log in"" +* *Custom:* ""Sign In,"" ""Access Portal,"" etc. - - **Label:** This is a clear and concise text displayed above the choice element. It acts as a description of the choice. - - **Default value:** This pre-selects an option from the list. If no default value is chosen, the first option is usually selected by default. - - **Placeholder:** This provides a hint to the user about what they should select when the choice is empty. - - **Required:** When enabled, users must choose an option from the list before continuing. This ensures they provide a necessary selection. - - **Allow multiple values**: This option determines whether the element allows users to select multiple choices. - - **Display**: This option controls how the element is displayed. You can choose between two options: - - **Dropdown**: This option presents a dropdown menu where users can select one or more options. - - **Checkboxes**: This option displays a list of checkboxes, allowing users to select multiple options by clicking on individual checkboxes. - - **Options:** These are the individual choices presented within the menu. Each option has two properties: - - **Value:** An internal identifier associated with the option. This value is typically used to store the user's selection in your application. - - **Name:** The text displayed to the user when presenting the options in the menu. Choose clear and concise names that accurately reflect the corresponding value. +### 3. Events (On Success) +By default, the login form simply validates the user. You must define what happens *next*. +1. Click the **Events** tab. +2. You will see the **On Success** trigger. +3. Add an action, typically **Open Page**. +4. Select the destination (e.g., ""Home"" or ""Dashboard""). + * *Note:* If you don't set this, the user will remain on the login page even after successfully logging in. -You can populate the options with data retrieved from data sources. +## Styling and Theme Overrides +The Login element consists of two distinct parts: the **Input Fields** and the **Submit Button**. You can customize them separately via **Theme Overrides**. - [1]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/99a6fa85-1562-485a-a9d5-1b8833a9501e/Choice%20element%20for%20Application%20Builder%20.png - [2]: /user-docs/application-builder-record-selector-element - [3]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/8edd4b78-a01b-4918-917f-4c286f7c92d6/Untitled.png - [4]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/cb156340-d793-49aa-8cc7-c3cf224da962/Untitled%201.png",,baserow_user_docs,https://baserow.io/user-docs/application-builder-dropdown-element -186,Checkbox element,application-builder-checkbox-element,Checkbox element in Baserow Application Builder,"# Application Builder - Checkbox element +1. In the General tab, look for the **Style/Settings icon** (slider icon) next to the configuration sections. -The checkbox is a handy tool for users to quickly say yes with a tick or no by leaving it blank. +### Styling User Source (Inputs) +Click the override icon next to the **User Source** field to style the text boxes: +* **Label:** Font, color, and size of the ""Email"" and ""Password"" labels. +* **Input:** Background color, border radius, and text padding of the entry boxes. -Let's go over how to set up a checkbox and talk about each setting option. +### Styling Login Button +Click the override icon next to the **Login button label** field to style the button: +* **States:** Configure distinct looks for **Default**, **Hover**, and **Active** states. +* **Visuals:** Change the button color, border, and alignment (Left/Center/Right/Full Width). -## Overview +## Post-login behavior -A checkbox is a little box you can check or leave empty. For the checkbox element, you can configure the element properties and [style](/user-docs/element-style) from the element settings. +Once a user logs in, the application state changes. +1. **Page Visibility:** The user can now access pages set to ""Logged-in visitors only."" +2. **Data Security:** Any data restricted to the user's role becomes available. +3. **Logout:** To allow users to leave, you must add a separate **Button element** elsewhere in your app (e.g., in the header) and configure its Event to **Log out**. -It's commonly used to give a choice between two options, typically 'true' or 'false.' When you check the box, it means 'true,' and when you leave it unchecked, it means 'false.' It's a way to select one or more options from a list by clicking on them. +![Baserow logout button will end the user's session ][3] -Learn more about the [boolean field in the Baserow table](/user-docs/boolean-field). +## Troubleshooting -The Application Builder allows you to bind the text to [data sources](/user-docs/data-sources). This enables the text to update dynamically based on user input or application logic. +### Login works, but the user is not redirected +This usually happens if the user does not have permission to view the destination page. +* **Check Page Visibility:** Go to the destination page (e.g., ""Dashboard"") and check its **Visibility** settings. Ensure the user's role is included in the ""Allow roles"" list. +* **Check User Source:** Ensure the User Source has the **Role field** correctly mapped to your database. -![Application Builder - Checkbox element][1] +### ""Invalid Credentials"" error + * Ensure the email and password in your database match exactly. + * Remember that passwords in Baserow are hashed. You cannot simply type a plain text password into the database cell; you must create the user via a Sign-Up form or use the admin tools to set the password. -## Add and configure checkbox elements +Learn more about the [password field type][4]. -To add a checkbox element, access the [elements panel](/user-docs/elements-overview) and select **Checkbox**. +## Related content -Once added, place the checkbox wherever you want it on the [page](/user-docs/pages-in-the-application-builder). Don't worry if it's not perfectly positioned initially; you can always move it later. + * [Configure User Sources][1] + * [Page Visibility Settings][5] + * [Events and Workflows][6] -Learn more about how to [add and remove an element from a page](/user-docs/add-and-remove-elements). +--- -![Add and configure table elements][2] -Now, you'll configure the checkbox's properties to make it function and look the way you want. This involves settings related to checkbox style. +Still need help? If you're looking for something else, please feel free to make recommendations or ask us questions; we’re ready to assist you. -## Configure checkbox element + - [Ask the Baserow community](https://community.baserow.io) + - [Contact support](/contact) for questions about Baserow or help with your account. -When setting it up, you'll have some configuration options, like deciding what each choice represents or customizing its appearance. It's like tailoring the checkbox to fit your specific needs. -The checkbox element has the following common settings: + [1]: /user-docs/user-sources + [2]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/c2d28ec6-d16b-49a4-9232-426d5b05c704/Untitled%201.png + [3]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/8a1906dc-375a-4811-a222-6954cd75a7fe/logout.png + [4]: https://baserow.io/user-docs/password-field + [5]: /user-docs/application-builder-element-visibility + [6]: /user-docs/element-events",application builder,baserow_user_docs,https://baserow.io/user-docs/application-builder-login-element +189,Element visibility,application-builder-element-visibility,Baserow Application Builder element visibility,"# Configure element visibility (Permissions) -- **Label**: The label provides context for what the checkbox represents. This is the text that appears next to the checkbox, indicating what the checkbox is for. It should be clear and concise, explaining the purpose of selecting or deselecting the checkbox. -- **Default value**: The default value sets the initial state of the checkbox. This refers to the state of the checkbox when the form loads. For a yes/no checkbox, the default value could be either ""Yes"" (checked) or ""No"" (unchecked), depending on the context and the expected user behavior. -- **Required**: This option determines whether the user must interact with the checkbox before submitting the form. If the checkbox is required, the user must check yes before proceeding. If it's not required, the user can choose to leave the checkbox unchecked. +While [Page Visibility][1] controls access to an entire screen, **Element Visibility** allows you to show or hide individual components on that screen. This is essential for creating dynamic interfaces; for example, showing a ""Login"" button to guests but a ""Log Out"" button to authenticated users. +This guide covers how to control who sees specific buttons, tables, and content based on login status and user roles. - [1]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/9b1b27c2-05c0-4cec-b863-afea7b4ee2b9/Untitled.png - [2]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/4281431d-3a77-448c-b627-1a7426a088b6/Untitled%201.png",,baserow_user_docs,https://baserow.io/user-docs/application-builder-checkbox-element -187,iFrame element,application-builder-iframe-element,iFrame element in Baserow Application Builder,"# Application Builder - iFrame element +## Overview -The iFrame element allows you to insert custom snippets of code anywhere on your page. +The visibility tab allows you to define which user groups can view specific elements in your application according to a [user’s authentication status][2]. -In this section, we will guide you through setting up an IFrame element and explain each configuration option in detail. +* **Granularity:** You can set visibility rules for every single element on your canvas. +* **Login Status:** You can show elements to everyone, only [logged-in users][3], or only logged-out visitors. +* **Role-Based Access:** If a user is logged in, you can further restrict access based on their assigned role (e.g., ""Admin"" vs. ""Viewer""). +* **Security:** Hiding a data element (like a Table) automatically secures the backend API. Unauthorized users cannot fetch the data even if they try to bypass the UI. -## Overview +> Learn more about [page visibility][4] to control which user groups can see specific pages in your application. -An IFrame is an HTML element used to embed another document within the current HTML document. It allows you to display content from another source without the need for the user to navigate away from the current page. +![Baserow Visibility tab and its location on the right-side panel][5] -For the IFrame element, you can configure the element properties and [style](/user-docs/element-style) from the element settings. +## Configure basic visibility -The Application Builder allows you to bind the text to [data sources](/user-docs/data-sources). This enables the text to update dynamically based on user input or application logic. +To define who can see an element: -## Add and configure IFrame elements +1. Select the element on your canvas (e.g., a Button or Text block). +2. Click the **Visibility** tab in the properties panel on the right. +3. Select one of the three visibility levels: -To add an IFrame element, access the [elements panel](/user-docs/elements-overview) and select **IFrame**. +| Option | Description | Use Case | +| :--- | :--- | :--- | +| **All visitors** | Visible to everyone. | Public headers, generic information. | +| **Logged-in visitors** | Visible only to authenticated users. | ""My Profile"" buttons, private data tables. | +| **Logged-out visitors** | Visible only to guests. | ""Sign Up"" or ""Login"" buttons. | -Once added, place the IFrame wherever you want it on the [page](/user-docs/pages-in-the-application-builder). Don't worry if it's not perfectly positioned initially; you can always move it later. +## Configure role-based access (RBAC) -Learn more about how to [add and remove an element from a page](/user-docs/add-and-remove-elements). +For more advanced control, you can restrict elements to specific user roles (e.g., allowing only ""Managers"" to see a ""Delete"" button). -![Add and configure table elements][1] +![Visibility roles for Application Builder ][6] -Now, you'll configure the IFrame's properties to make it function and look the way you want. This involves settings related to the IFrame style. +### Step 1: Define roles in your database +Before configuring the frontend, your user data must have roles defined. +1. Go to your **Users Table** in the database. +2. Create a [**Single Select** field][7] named ""Role"". +3. Add options for your roles (e.g., `Admin`, `Editor`, `Viewer`). +4. Assign a role to each user row. -That's it. Now the IFrame will appear. +### Step 2: Connect roles to User Source +1. In the Application Builder, go to **Settings** → **[User Sources][8]**. +2. Edit your user source. +3. Locate the **Role field** setting and map it to the ""Role"" column you created in Step 1. -## IFrame source type +### Step 3: Apply rules to elements +1. Select the element you want to restrict. +2. Go to the **Visibility** tab. +3. Select **Logged-in visitors**. +4. Under the Roles section, choose your logic: + * **All roles:** Visible to any logged-in user. + * **Allow roles:** Only the selected roles (e.g., `Admin`) can see this. + * **Disallow roles:** Everyone *except* the selected roles can see this. -Using IFrames can be handy for integrating content from other sources into your application seamlessly. +## Security and API enforcement -You can set the source as a URL or an Embed. +> We’re constantly enhancing the security features of the Application Builder. -- **URL:** Input the link to the external resource to be embedded. Ensure that you have control over, or trust the URL entered. -- **Embed:** Input the raw HTML content to be embedded. +Baserow ensures that ""Hidden"" means ""Secure."" Visibility settings are enforced on the backend, not just the frontend (CSS). -For example, if you wanted to embed a map or form on a page, you could use an IFrame to display the map without redirecting the user to an external website. + * **Data Security:** If you hide a Table element from a specific role, Baserow's API will **not** return that table's data to the user's browser. Even if a user inspects the network traffic, the data is not there. + * **Action Security:** If you hide a ""Update Row"" button from a user, the API endpoint to update that row is blocked for that user session. -![Baserow form embed][2] +In a nutshell, securing the API is inherent to how you design your application; by controlling what data is available to users, you define what the API exposes. -## IFrame content +## Frequently asked questions (FAQ) -You can enter static text here. However, if you've connected to a [data source](/user-docs/data-sources), all the fields from the data source will also become available. +### What happens if a user has no role assigned? +If you use ""Allow roles"" logic (e.g., Allow `Admin`), a user with *no* role will **not** see the element. If you use ""Disallow roles"" logic (e.g., Disallow `Viewer`), a user with *no* role **will** see the element (because they are not a Viewer). -First, you need to get your embed code. +### Can I hide a container to hide all its children? +Yes. If you place multiple elements inside a **Column** or **Container** element and hide the parent, all child elements inside it inherit that visibility rule. This is much faster than configuring every single button individually. -To pass dynamic data, create a corresponding field in the pre-configured [data source](/user-docs/data-sources). You can create a [text field](/user-docs/single-line-text-field) in the table and add the embed code or create a [URL field](/user-docs/url-field) with the external link. +### Does this work with page visibility? +Yes. Page Visibility acts as the ""Gate"" to the URL. Element Visibility acts as the ""Filter"" for the content on that page. If a user cannot access the Page, they definitely cannot see the Elements. -Next, add an IFrame element to the page and link the field from the data source. +## Related content -![Baserow get your embed code. ][3] +* [Configure User Sources][8] +* [Page Visibility Settings][1] +* [Overview of Elements][9] -## IFrame height (px) +--- -The height of an IFrame element within an application can be specified in pixels (px). The maximum allowed height is 2000px. -Here's an example of setting the iframe height to 500px: +Still need help? If you're looking for something else, please feel free to make recommendations or ask us questions; we’re ready to assist you. -```json - -``` - - - [1]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/e823613f-fb69-49b3-a8ee-866ded4c2d0b/Untitled.png - [2]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/5bb39ca3-6ca6-40ae-897f-2c387a25d507/Untitled%201.png - [3]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/cf1bd718-7203-480a-8f1c-f82c007b9dd4/Untitled%202.png",,baserow_user_docs,https://baserow.io/user-docs/application-builder-iframe-element -188,Login element,application-builder-login-element,Login element in Baserow Application Builder,"# Application Builder - Login element - -The login element allows users to access your application with secure credentials. - -Let's look at how to set up login functionality in your application using the login element. This element lets you build and handle user accounts right inside the Application Builder. - -## Overview - -The login element is an important component for securing your application. To enable user authentication, you'll need to choose a [user source](/user-docs/user-sources). This provides information needed to recognize and validate users attempting to log in. - -## Add and configure login elements - -To add a login element, access the [elements panel](/user-docs/elements-overview) and select **Login**. - -Once added, place the element wherever you want it on the [page](/user-docs/pages-in-the-application-builder). Don't worry if it's not perfectly positioned at first; you can always move it later. - -Learn more about how to [add and remove an element from a page](/user-docs/add-and-remove-elements). - -![Add and configure login elements][1] - -Now, you'll customize the login's behavior and appearance by configuring its properties. This involves settings related to login style. - -## Choose a user source to use the login element - -To configure the login element, you'll first need to choose the [source of your user data](/user-docs/user-sources). This determines where the application will verify user credentials during the login process.  - -The user source is essentially how your application identifies and verifies users. It establishes the foundation for secure login functionality. - -Adding a user source allows the application to recognize and authenticate users effectively. By adding a user source, you define how users will authenticate themselves within the application. This typically involves configuring settings related to username and password verification. - -Learn more about [how to add or configure a user source](/user-docs/user-sources). - -![Choose a user source to use the login element][2] - -Once you've chosen the user source, you can establish a secure connection and enable user authentication using the chosen method. - -## Using email and password for login - -Baserow's [password field type](/user-docs/password-field) provides a secure way to manage user credentials within your application. This functionality allows you to: - -- Set unique passwords for each user record. -- Store passwords securely within your Baserow database. -- Create new rows containing password data. - -By configuring a user source that leverages email and password login, users can authenticate themselves using their existing credentials. This streamlines the login process and enhances application security. - -Learn more about the [password field type](/user-docs/password-field). - -![Using email and password for login][3] - -## What happens after login? - -Once a user successfully logs in with a valid username and password, you can set the visibility of each element in your application according to a user's authentication status. - -![What happens after login in Baserow?][4] - -You can add a [logout action][5]. This can be set up as a [button element][6], with the 'On click' event configured to trigger a ‘Logout’. - -Clicking the logout button will end the user's session and return them to the login screen. - -![Baserow logout button will end the user's session ][7] - -## Troubleshooting - -**When a user successfully logs in, the next action after login is not initiated** - -The user may not have permissions to view the page that needs to be opened after login in. - -When you configure Page Visibility on a page, but the visitor doesn't have permissions to view that page, they will be automatically redirected to the login page. - -To fix this: - - - In the Page Editor, go to the page that the ""Open a Page"" action is pointing to. - - - Click Page Settings -> Visibility - - - Make sure that the visibility settings are correct. - - If the visitor's role is not included, make sure the visibility rules allow the visitor's role in the ""Allow roles..."" list. - - If the ""Allow roles"" doesn't have any roles to select, it means the user hasn't configured the User Roles correctly. - - - To configure User Roles, go to Application settings -> Users. - - Make sure a User Source exists - - Make sure the Role field is selected and that it is pointing to the correct database field. - - - Check that the page uses a valid data source. - - [1]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/4561f5d6-e1f6-4d3c-8103-7e23db516c6c/Untitled.png - [2]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/c2d28ec6-d16b-49a4-9232-426d5b05c704/Untitled%201.png - [3]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/e55f2618-ae71-4f52-b321-a7912069b0fd/Untitled%202.png - [4]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/6979e3f0-c9f5-4ae5-9dc7-52e1b6bbfb8f/Untitled%203.png - [5]: /user-docs/element-events - [6]: /user-docs/application-builder-button-element - [7]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/8a1906dc-375a-4811-a222-6954cd75a7fe/logout.png",,baserow_user_docs,https://baserow.io/user-docs/application-builder-login-element -189,Element visibility,application-builder-element-visibility,Baserow Application Builder element visibility,"# Control element visibility in the Application Builder - -The visibility tab provides a powerful tool to control access to information and functionalities within your application. - -This article explains how to control who sees specific [elements][1] within your application using the visibility tab in the Application Builder. - -> Learn more about [page visibility][2] to control which user groups can see specific pages in your application. - -## Overview - -The visibility tab allows you to define which user groups can view specific elements in your application according to a [user's authentication status][3]. This is helpful for situations where you want to: - -- Restrict access to certain content for logged-in users only. -- Provide special information for visitors who haven't signed up yet. -- Control what everyone, regardless of login status, can see. - -You can set a different visibility level for each element in your application. This allows you to create a customized user experience based on login status. - -![Baserow Visibility tab and its location on the right-side panel][4] - -## How to use the visibility tab - -1. **Select an Element:** Click on the element in your application that you want to control visibility for. This could be a button, text, image, or any other element. -2. **Open the visibility tab:** Look for the right-side panel within the Application Builder. There should be a tab labeled ""Visibility"". -3. **Choose visibility level:** Within the Visibility tab, you'll see three options: - - **All visitors:** This option makes the element visible to everyone who visits your application, regardless of their login status. - - **[Logged-in visitors][5]:** This option restricts the element to users who have successfully logged in to your application. - - **Logged-out visitors:** This option makes the element visible only to users who haven't logged in yet. -4. **Set visibility:** Click on the desired option to define who can see the selected element. - -## Set visibility roles for logged-in visitors - -![Visibility roles for Application Builder ][6] - -Visibility roles provide a granular level of control over user access. By defining roles within your application and assigning them to users, you can determine which elements each user can see. - -Before using visibility roles, you'll need to establish the different user roles within your application. - - 1. [Create a field in the table][7] to store the assigned role for each user based on their permissions. - 2. In the Application Builder, navigate to your [User Source][3] settings. There, you'll define a role field. This field will be used to map user data in your [User Source][3] to the roles you established. or use the default role to assign appropriate roles to each user. - 3. Navigate to the element's Visibility tab within the editor. This allows you to define visibility based on user roles: - - - **All roles**: This option makes the element visible to all users, regardless of their role. - - **Allow roles**: Choose this option to define which specific user roles can see the element. Only users assigned to the selected roles will have access. - - **Disallow roles**: This option hides the element from users assigned to the selected roles. All other users will still see the element. - -This empowers you to create custom user experiences tailored to different user types. - -## Note: Visibility security - -> We're constantly enhancing the security features of the Application Builder. - -Elements that are hidden on the frontend are also secured on the backend so that data from those elements are never returned by the underlying API if the authenticated user does not have access. - -Here's how the data security works: - -1. If you create an element (like a table or form) that shows certain data from a data source, and make that element visible to a specific user role, then users with that role can access only that specific data they can see. -2. Similarly, if you create an action button that lets users update certain fields, they can only update exactly what that button is configured to change - nothing more. - -For example: - -- If you make a table element visible to the ""Sales"" role that shows customer names and order totals, users with the Sales role can only see those two pieces of information and nothing more and the API will only return the necessary data. -- If you add an ""Update Status"" button visible to ""Support"" role, they can only update the status field, even if they can see other fields. - -In a nutshell, securing the API is inherent to how you design your application — by controlling what data is available to users, you define what the API exposes. - ---- - -Still need help? If you're looking for something else, please feel free to make recommendations or ask us questions—we’re ready to assist you. - - -   [Ask the Baserow community](https://community.baserow.io) - -   [Contact support](/contact) for questions about Baserow or help with your account. + - [Ask the Baserow community](https://community.baserow.io) + - [Contact support](/contact) for questions about Baserow or help with your account. - [1]: /user-docs/elements-overview - [2]: /user-docs/application-builder-page-settings#page-visibility - [3]: /user-docs/user-sources - [4]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/d809d3da-d097-446a-be89-ce891dfd8fe8/Baserow%20Visibility%20tab%20and%20its%20location%20on%20the%20right-side%20panel.png - [5]: /user-docs/application-builder-login-element + [1]: /user-docs/application-builder-page-settings#page-visibility + [2]: https://baserow.io/user-docs/user-sources + [3]: https://baserow.io/user-docs/application-builder-login-element + [4]: https://baserow.io/user-docs/application-builder-page-settings#page-visibility + [5]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/6c5e7e06-445a-456f-8dce-922ef5fae492/Visibility%20condition.png [6]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/d4df6672-9e82-4c87-ba65-0bf8d8b516a5/Visibility%20roles%20for%20Application%20Builder%20.png - [7]: /user-docs/adding-a-field",,baserow_user_docs,https://baserow.io/user-docs/application-builder-element-visibility + [7]: https://baserow.io/user-docs/adding-a-field + [8]: /user-docs/user-sources + [9]: /user-docs/elements-overview",application builder,baserow_user_docs,https://baserow.io/user-docs/application-builder-element-visibility 190,Paste data into cells,paste-data-into-baserow-table,Paste data into Baserow table cells,"# Paste data into table cells Baserow handles row creation automatically during paste operations; no configuration needed, just paste your data and watch new rows appear. @@ -23896,9 +24205,11 @@ Yes. You can generate all AI values in one go by clicking the **Generate all AI [13]: https://baserow.io/blog/use-baserow-ai-analyze-documents",field,baserow_user_docs,https://baserow.io/user-docs/ai-field 192,Repeat element,application-builder-repeat-element,Repeat element in Baserow Application Builder,"# Application Builder - Repeat element -A repeat element allows you to show a list within a [page](/user-docs/pages-in-the-application-builder) by repeating the elements for each result in the data source. It allows you to repeat a set layout for each item in a container, making it easier to manage large amounts of similar data. -In this section, we’ll set up a repeat element, the configuration options, and how it generally works. + +The Repeat element (often called a ""Repeater"" or ""Collection"") is a container that iterates through a list of data. You design the layout for a single item (the template), and the element automatically repeats that layout for every row in your Data Source. + +This page covers how to display dynamic lists, grids, or nested collections from your database. @@ -23906,90 +24217,113 @@ In this section, we’ll set up a repeat element, the configuration options, and Repeat elements allow you to display lists or collections of data. Instead of manually creating multiple instances of similar elements, repeat elements can dynamically generate as many instances as needed based on the data source. -Using repeat elements ensures consistency across the application. When you update the design or functionality of a repeat element, the changes are automatically applied to all instances. This makes maintenance easier and reduces the risk of inconsistencies. - -You can customize the [element properties and style of the element](/user-docs/element-style) using the element settings panel on the right side of the screen. - -## Hierarchy using the Repeat element +* **Dynamic Content:** Connects to a ""List multiple rows"" [Data Source][1] to display records. +* **Layouts:** Supports **Vertical** lists (rows) or **Horizontal** grids (cards). +* **Nesting:** You can place a Repeat element *inside* another Repeat element to show related data (e.g., ""Employees"" inside ""Departments""). +* **Styling:** Layout spacing is configured in the General tab, while button styles (Show More, Filters) are managed via **Theme Overrides**. -You can create two types of hierarchies using the Repeat element: +![Baserow app builder repeat element][2] - - **Root-level collection element**: This type of element is associated with a multiple-row [data source][1] but does not have a multiple-valued property. It allows you to create a primary collection that can iterate over data rows. - - **Nested collection element**: This type of element does not require a [data source][1] but instead uses a multiple-valued property. It is embedded within another collection element, allowing you to display a hierarchy, such as a list of departments with each containing a nested list of employees or categories with nested products for an e-commerce store. +## Add a repeat element -![Hierarchy using the Repeat element][2] +1. Open the **Elements** panel (click the `+` icon). +2. Select **Repeat**. +3. Drag and drop the element onto your canvas. +4. **Important:** You must now drag *other* elements (like Headings or Text) **inside** the Repeat box to define what each item looks like. -## Add and configure repeat elements +## Configuration -To add a repeat element, click the `+` icon and select **Repeat**. +Click the element to open the **General** properties tab. -Place the element wherever you want it on the [page][3]. Don't worry if it's not perfectly positioned initially; you can always move it later. +### 1. Data Source +Select where the list comes from. +* **Root-level collection:** Select a [Data Source][1] (service type: *List multiple rows*). +* **Nested collection:** If placed inside another Repeat element, you can select a **Link to Table** field to show related records. -Learn more about how to [add and remove an element from a page](/user-docs/add-and-remove-elements). +### 2. Layout & Orientation +Control how the items are arranged. +* **Orientation:** + * **Vertical:** Stacks items on top of each other (standard list). + * **Horizontal:** Places items side-by-side (grid/cards). +* **Items per row:** (Horizontal only) Define how many items appear per row on Desktop, Tablet, and Mobile. +* **Space between repetitions:** Define the gap (in pixels) between each item in the list. -Now, you can configure the element's properties to make it function and look the way you want. This involves settings related to repeat style and [adding child elements][4] to the repeat element. +### 3. Pagination +Control how much data loads at once. +* **Items per page:** Set the limit (max 100) for the initial load. +* **Show more label:** Customize the text of the button that loads the next batch (e.g., ""Load more products""). -![Baserow app builder repeat element][5] +### 4. User Actions (Filters & Search) +Allow public users to interact with the list. +* **Filter / Sort / Search:** Check the boxes to enable these features. +* **Fields:** Select which specific columns (e.g., ""Name"", ""Price"") the user is allowed to filter or search by. -## Repeat data source +![image: filter_order_and_search_for_published_applications][4] -> To list rows in the repeat list, set the [data source](/user-docs/data-sources) service type as *List multiple rows*. Learn more about data sources and how to configure them. -> - -After adding a repeat element to the page from **Elements**, you need to select the [data source](/user-docs/data-sources) which you want to import your data from. This is done from the **General** tab of the element settings. +### 5. Developer Tools +* **Temporarily disable repetitions:** Check this box to hide all items except the first one. This is useful during design time so you can focus on editing the template without seeing 50 copies of it on your canvas. -![Repeat element screenshot][6] +## Styling and Theme Overrides -## Repeat items per page +While the Repeater layout is structural, it generates interactive buttons (the ""Show More"" button and ""User Action"" buttons). You can style these using **Theme Overrides**. -You can choose how many items appear in the list by default. The field must be an integer and must be less than or equal to 100. +1. In the General tab, look for the **Style/Settings icon** (slider icon) next to **Show more label** or **User actions**. +2. Clicking this opens the specific styling menu for those buttons. -If there are more items to display than the defined number, a **Show more** button will be added at the end of the list, allowing the user to expand the list and view the additional items. +### Styling the ""Show More"" Button +* **Typography:** Font family, weight, size. +* **Colors & States:** Define the Background and Text color for **Default**, **Hover**, and **Active** states. +* **Spacing:** Adjust padding to control button size. -## Repeat element orientation +### Styling User Action Buttons +* **Button Style:** Customize the ""Filter"" and ""Sort"" buttons that appear above the list. +* **States:** Set distinct colors for when a filter is active vs. inactive. -This setting controls how repeated elements are arranged on the screen. You can choose between two orientations: +## Advanced: Hierarchy and Nesting -* **Vertical:** Elements will be stacked on top of each other. -* **Horizontal:** Elements will be displayed in rows from left to right. +The Repeat element is powerful because it supports nesting. -When choosing horizontal orientation, you can further define the number of elements displayed per row for different device types. This allows you to customize the layout for optimal viewing on various screen sizes. +* **Example:** A ""Department"" directory. +* **Structure:** + 1. **Outer Repeater:** Connected to ""Departments"" data source. Contains a Heading (`{{ Department Name }}`). + 2. **Inner Repeater:** Placed inside the outer one. Connected to the ""Employees"" link field. Contains a Text element (`{{ Employee Name }}`). +* **Result:** The app displays a list of departments, and under each department, a list of its specific employees. -**Properties of items per row:** +![Hierarchy using the Repeat element][6] -* **Per device type:** You can set a different number of items per row for each device type (e.g., mobile, tablet, desktop). -* **Integer value:** The number of items must be a whole number (1, 2, 3, etc.). -* **Range:** The number of items must be between 1 and 10 (inclusive). - -![Repeat element horizontal screenshot][7] +## Frequently asked questions (FAQ) -## User actions +### Why is my Repeat element empty? +Ensure you have selected a valid **Data Source** and that the data source actually returns rows. Also, check that you have placed elements (like Text or Images) *inside* the Repeat container and mapped them to fields. -External users can filter, sort, and search within [published applications][8], creating a more interactive and user-friendly experience. +### Can I limit the number of items? +Yes. Use the **Items per page** setting. If you want a hard limit (e.g., ""Top 3 items"") without a ""Show More"" button, you might need to configure the limit on the **Data Source** filters instead. -For the repeat element, you can specify which fields to make filterable, sortable, and searchable for your external users. +### How do I make a grid layout? +Set **Orientation** to ""Horizontal"" and set **Items per row** to 3 (or your desired number). This creates a card-style grid. -To add filtering, ordering, and searching capabilities, click on the repeat element and navigate to the right sidebar. There, you’ll see checkboxes to enable Filter, Sort, and Search for specific fields. +## Related content -![image: filter_order_and_search_for_published_applications][9] + * [Connecting Data Sources][1] + * [Using the Table Element](/user-docs/application-builder-table-element) + * [Element Styling (Layout vs. Theme)][7] --- -Still need help? If you're looking for something else, please feel free to make recommendations or ask us questions—we’re ready to assist you. + +Still need help? If you're looking for something else, please feel free to make recommendations or ask us questions; we’re ready to assist you. - [Ask the Baserow community](https://community.baserow.io) - - [Contact support](/contact) for questions about Baserow or help with your account + - [Contact support](/contact) for questions about Baserow or help with your account. [1]: /user-docs/data-sources - [2]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/6479b9ca-d9ce-4523-a2b2-cedada4ca1ba/nested_collection_elements.webp - [3]: /user-docs/pages-in-the-application-builder - [4]: /user-docs/add-and-remove-elements - [5]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/0ee7d1fb-09f2-45c5-a6db-f58326dfb831/Application%20Builder%20-%20Repeat%20element%20.png - [6]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/0f4b9ce7-c200-4e69-ba10-cf92bc4bef49/Repeat%20element.png - [7]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/4f372355-aae8-4044-b3cc-1097d0c2ee44/Repeat%20element%20-%20vertical.png - [8]: /user-docs/preview-and-publish-application - [9]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/40f9ce8a-e81b-4740-a3b0-b7bbc635a37d/filter_order_and_search_for_published_applications.webp",,baserow_user_docs,https://baserow.io/user-docs/application-builder-repeat-element + [2]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/b1c076bc-5b66-4bd8-b48f-2ded5a8906aa/Repeat%20element.png + [3]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/4f372355-aae8-4044-b3cc-1097d0c2ee44/Repeat%20element%20-%20vertical.png + [4]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/40f9ce8a-e81b-4740-a3b0-b7bbc635a37d/filter_order_and_search_for_published_applications.webp + [5]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/0f4b9ce7-c200-4e69-ba10-cf92bc4bef49/Repeat%20element.png + [6]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/6479b9ca-d9ce-4523-a2b2-cedada4ca1ba/nested_collection_elements.webp + [7]: /user-docs/element-style",application builder,baserow_user_docs,https://baserow.io/user-docs/application-builder-repeat-element 193,AI formula generator,generate-formulas-with-baserow-ai,Generate formulas with AI in Baserow,"# Generate formulas with Baserow AI Baserow's AI formula generator converts plain English descriptions into working formulas instantly. Describe what you want to calculate, and AI creates the formula syntax for you. @@ -24336,7 +24670,7 @@ Still need help? If you're looking for something else, please feel free to make [3]: https://baserow.io/user-docs/webhooks [4]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/d2d504cf-47cc-4cd9-8e99-faa469ee235c/iNCOMING%20webhook.png [5]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/fa28096e-3f32-4ec2-856e-e2dcb007ae0a/test%20webhook%20suretriggers.png - [6]: https://baserow.io/user-docs/personal-api-tokens",,baserow_user_docs,https://baserow.io/user-docs/suretriggers-integration + [6]: https://baserow.io/user-docs/personal-api-tokens",integrations,baserow_user_docs,https://baserow.io/user-docs/suretriggers-integration 227,Timeline view,guide-to-timeline-view,Timeline view in Baserow tables,"# Timeline view guide @@ -24857,72 +25191,112 @@ Still need help? If you're looking for something else, please feel free to make [13]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/1338865f-6680-4868-b330-4f2b6307c85b/screenshot_to_identify_a_synced_field.webp",,baserow_user_docs,https://baserow.io/user-docs/data-sync-in-baserow 260,Record selector element,application-builder-record-selector-element,Record selector element in Baserow Application Builder,"# Application Builder - Record selector element -The Record Selector simplifies linking and selecting rows from related tables. This is useful when selecting any of the thousands of rows from a related table to populate a link row field. +The Record Selector is a specialized input element designed to handle relationships. Unlike a simple dropdown menu, it allows users to search, paginate, and select records from a connected Data Source. It is the frontend equivalent of the ""Link to Table"" field. -In this section, we'll provide detailed instructions and best practices for using the Record Selector element in the Application Builder. - -![Baserow Record selector element][1] +This page covers how to select and link rows from related tables, optimized for large datasets. ## Overview The Record Selector enables you to populate a link row field by selecting from rows in another table. This is ideal for scenarios where dynamic data connections are needed, such as assigning roles, linking tasks, or selecting categories from a related dataset. -The Record Selector handles thousands of rows using dynamic data loading, ensuring fast performance even for large datasets. +* **Use Case:** Best for selecting items from a large database (e.g., assigning a ""Customer"" to an ""Order""). +* **Performance:** Supports pagination (""Items per page"") to handle thousands of rows without slowing down the app. +* **Search:** You can configure which fields (e.g., Name, Email) are searchable by the user. +* **Styling:** Colors, fonts, and borders are managed via **Theme Overrides**. -You can configure the element properties and [style](/user-docs/element-style) from the element settings. +![Baserow Record selector element][1] + +## Add a record selector -The Application Builder allows you to bind the text to [data sources](/user-docs/data-sources). This allows the text to update dynamically based on user input or application logic. You can also populate the options with data retrieved from data sources. This offers a dynamic way to manage options. +1. Open the **Elements** panel (click the `+` icon). +2. Select **Record selector**. +3. Drag and drop the element onto your canvas (typically inside a [Form Container][2]). -## Add the record selector to an application +## Configuration -To add a record selector element, access the [elements panel](/user-docs/elements-overview) and select **Record selector**. +Click the element to open the **General** properties tab. -Once added, place the record selector element wherever you want it on the [page](/user-docs/pages-in-the-application-builder). Don't worry if it's not perfectly positioned initially; you can always move it later. +### 1. Data Connection (Source) +To populate the list of options, you must connect a Data Source. -Learn more about how to [add and remove an element from a page](/user-docs/add-and-remove-elements). +* **Select records from:** Choose a [Data Source][3] (List Rows type) that contains the options. + * *Example:* If you are building a ""Create Order"" form, connect this to a ""List Customers"" data source. +* **Items per page:** Define how many results appear in the dropdown at once (5–100). + * *Note:* Users can click ""Load More"" or use the search bar to find items not currently visible. -Then, you can configure the record selector element's properties to make it function and look the way you want. This involves settings related to the record selector element style. +### 2. Content & Logic +* **Label:** The descriptive text displayed above the field (e.g., ""Assign Customer""). +* **Placeholder:** Hint text displayed when no selection is made (e.g., ""Search for a customer...""). +* **Value:** Automatically select a record when the page loads. + * *Dynamic:* You can bind this to a page parameter or user profile (e.g., auto-select the current logged-in user). -## Configure the Record Selector settings +### 3. Behavior & Validation +* **Allow multiple values:** If checked, the user can select multiple records (e.g., tagging multiple ""Categories""). If unchecked, it acts as a single-select. +* **Required:** Prevents form submission until a selection is made. -Once the element is added, configure its properties in the settings panel. You can populate the options with data retrieved from data sources. The dropdown dynamically reflects changes in the linked table, ensuring real-time updates. +![Configure the Baserow Record Selector Settings][4] -The following settings are available: +## Search configuration (User Actions) - - **Select records from**: Choose the [data sources](/user-docs/data-sources) you want to pull records from. Example: Job roles or Departments. - - **Items per page**: Define how many rows are displayed in the selector view. Example: 20. This number must be greater than or equal to 5 but less than or equal to 100. - - **Option name suffix**: Add a suffix to distinguish rows when displayed in the dropdown. Example: combining multiple data fields like ""Company - [Value]"". - - **Label**: Enter a clear label for the field. Example: ""Select one or more roles"". - - **Placeholder**: Add placeholder text to guide users on what to do. Example: ""Make a selection"". - - **Default value**: Pre-define the field’s initial value using a specific data source or default logic. Example: Default to the first row. If you don’t define a default value, the field will remain empty until the user selects a row. - - **Allow multiple values**: Toggle this option if you want users to select more than one row. - - **Required**: Check this box if the field must be completed before submission. - - **User actions**: You can configure search functionality for external users by selecting relevant properties under the **User actions** section. +Because this element connects to your database, you must explicitly define which fields the user can search. -## User actions +1. In the properties panel, look for the **User Actions** or **Search** section. +2. Check the boxes next to the fields you want to be searchable. + * *Example:* For a User selector, you might check `Name` and `Email` so users can find people by either identifier. -For each element, you can specify which fields to make searchable for external users to give end-users more control to search within the [published application][3]. +![Record selector element - User actions][5] -To add searching capabilities, click on the element and navigate to the right sidebar. There, you’ll see checkboxes to enable Search for specific fields. +## Styling and Theme Overrides -![Record selector element - User actions][2] +By default, the selector inherits the input styles from your [Global Theme](/user-docs/application-settings). To customize it (e.g., change the label font or input border), use **Theme Overrides**. -This creates a more interactive and user-friendly experience. +1. In the General tab, look for the **Style/Settings icon** (slider icon) next to the **Data source* section. +2. Clicking this opens the specific styling menu. -![Configure the Baserow Record Selector Settings][4] +### Styling the Label +* **Typography:** Font family, weight, size, and color. +* **Alignment:** Left, Center, or Right align the label text. + +### Styling the Input Box +* **Text:** Font size and color of the selected item text. +* **Background:** Fill color of the selector box. +* **Border:** Color, width, and radius. +* **Padding:** Internal spacing. + +## Frequently asked questions (FAQ) + +### What is the difference between this and a ""Choice"" element? +* **Record Selector:** Connects to a *Data Source* (Database rows). Supports search, pagination, and dynamic updates. Use this for relationships. +* **Choice (Dropdown):** Uses a static list of options (Option A, Option B) or simple mapped values. Use this for status fields or simple categories. + +### Can I filter the options shown? +Yes. You filter the options at the **Data Source** level. If you want the selector to only show ""Active"" customers, apply a filter to the connected Data Source (Status = Active). The Record Selector will strictly respect that filter. + +### How do I save the selection? +The Record Selector only *collects* the ID(s) of the selected rows. To save this relationship, you must place the element inside a Form or map its value to a **Create/Update Row** button action. + +## Related content + + * [Connecting Data Sources][3] + * [Using the Choice (Dropdown) Element][6] + * [Element Styling (Layout vs. Theme)][7] --- -Still need help? If you're looking for something else, please feel free to make recommendations or ask us questions—we’re ready to assist you. + +Still need help? If you're looking for something else, please feel free to make recommendations or ask us questions; we’re ready to assist you. - [Ask the Baserow community](https://community.baserow.io) - - [Contact support](/contact) for questions about Baserow or help with your account + - [Contact support](/contact) for questions about Baserow or help with your account. - [1]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/9349ad70-8f47-4366-b169-c7faed797819/record_selector_element.webp - [2]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/0c1c59c5-06db-4212-b52a-ef7003823a2a/record_selector_element_user_actions.webp - [3]: /user-docs/preview-and-publish-application - [4]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/868ff5a8-d4f3-4184-b945-1e8a7f289349/Screenshot%202024-12-10%20at%2013.17.39.png",,baserow_user_docs,https://baserow.io/user-docs/application-builder-record-selector-element + [1]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/3d051cbf-9386-445a-a312-930a95e7daba/Record%20selector%20element.png + [2]: /user-docs/application-builder-form-element + [3]: /user-docs/data-sources + [4]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/868ff5a8-d4f3-4184-b945-1e8a7f289349/Screenshot%202024-12-10%20at%2013.17.39.png + [5]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/0c1c59c5-06db-4212-b52a-ef7003823a2a/record_selector_element_user_actions.webp + [6]: /user-docs/application-builder-dropdown-element + [7]: /user-docs/element-style",application builder,baserow_user_docs,https://baserow.io/user-docs/application-builder-record-selector-element 261,Export a workspace,export-workspaces,Export Baserow workspace data,"# Export workspace data from Baserow Export complete workspaces or select specific databases, applications, and dashboards to create portable ZIP backups. Export structure-only or include all data for migrations, backups, or sharing with other Baserow instances. @@ -25265,136 +25639,175 @@ Still need help? If you're looking for something else, please feel free to make [7]: /user-docs/delete-a-workspace [8]: /user-docs/create-a-table-via-import [9]: /user-docs/import-data-into-an-existing-table",workspace,baserow_user_docs,https://baserow.io/user-docs/import-workspaces -293,Date-time picker element,application-builder-date-time-picker-element,Date-time picker element in Baserow Application Builder,"# Application Builder - Date-time picker element +293,Date-time picker element,application-builder-date-time-picker-element,Date-time picker element in Baserow Application Builder,"# Date-time picker element -The date-time picker element offers a user-friendly way to input dates and times in your application. Rather than typing dates manually as text strings, users can select dates from a calendar interface. You can configure the element to show or hide the time field and set the date and time format. +The Date-time picker provides an intuitive way for users to select dates without worrying about format errors (e.g., DD/MM vs MM/DD). It is essential for scheduling forms, birthday inputs, or filtering records by date range. -![Image: Baserow date_time_picker][1] +This page covers how to collect dates, times, or timestamps with a user-friendly calendar interface. ## Overview -The date-time picker enables you to select specific dates and times for your records using this intuitive date-time picker. Ideal for scheduling events, tracking deadlines, recording timestamps, and more. +Rather than typing dates manually as text strings, users can select dates from a calendar interface. You can configure the element to show or hide the time field and set the date and time format. -You can configure the element properties, and [customize its appearance](/user-docs/element-style) from the element settings. +* **Functionality:** Opens a calendar popup for date selection and a scrollable list for time selection. +* **Formats:** Supports US (MM/DD), European (DD/MM), and ISO (YYYY-MM-DD) formats. +* **Time Support:** You can toggle time selection on or off and choose between 12-hour (AM/PM) or 24-hour clocks. +* **Styling:** Visuals for the Label and Input Box are managed via **Theme Overrides**. -## Add the date-time picker to an application -To add a date-time picker element, access the elements panel and select the date-time picker. -Once added, place the date-time picker element wherever you want it on the page. Don’t worry if it’s not perfectly positioned initially; you can always move it later. +## Add a date-time picker -[Learn more about how to add and remove an element from a page.](/user-docs/add-and-remove-elements) +1. Open the **Elements** panel (click the `+` icon). +2. Select **Date-time picker**. +3. Drag and drop the element onto your canvas (typically inside a [Form Container][159]). -Then, you can configure the date-time picker element’s properties to make it function and look the way you want. This involves settings related to the date-time picker element style. +## Configuration -## Configuring the date-time picker element +Click the element to open the **General** properties tab. -The date-time picker element can be configured with the following options +### 1. Content & Logic +* **Label:** The descriptive text displayed above the field (e.g., ""Appointment Date""). +* **Default Value:** A date that appears automatically when the page loads. + * *Dynamic:* Click the **Connect Data** icon to bind this to a [Data Source][144]. Useful for ""Reschedule"" forms where you want to show the current appointment time. +* **Required:** If checked, the user cannot submit the form without selecting a value. -- **Label:** Enter a descriptive label for the date-time picker field. This label will be displayed to users. -- **Default value:** Optionally, set a default date-time value that will be pre-populated in the field. -- **Required:** Check this box to make the field mandatory for users to fill. -- **Date format:** Select the preferred date format from the dropdown list. Options include: - - **European (25/04/2024)**: Day/Month/Year format - - **US (04/25/2024)**: Month/Day/Year format - - **ISO (2024-04-25)**: Year-Month-Day format -- **Include time:** Check this box if you want users to be able to select a specific time in addition to the date. +### 2. Formatting +Control how the date and time look to the user. -The Application Builder allows you to bind the label or default value to [data sources](/user-docs/data-sources). This allows the date to update dynamically based on user input or application logic. +* **Date Format:** + * **European:** 25/04/2024 (DD/MM/YYYY) + * **US:** 04/25/2024 (MM/DD/YYYY) + * **ISO:** 2024-04-25 (YYYY-MM-DD) +* **Include Time:** Check this box to allow time selection. +* **Time Format:** (Visible only if ""Include Time"" is checked) + * **12 Hour:** 01:30 PM + * **24 Hour:** 13:30 -By customizing these style properties, you can create inputs that match the look and feel of the page. +## Styling and Theme Overrides ---- +By default, the picker inherits styles from your [Global Theme][140]. To customize the specific look of this element, use **Theme Overrides**. -Still need help? If you're looking for something else, please feel free to make recommendations or ask us questions—we’re ready to assist you. +1. In the General tab, look for the **Style/Settings icon** (slider icon) next to the **Label** or **Input** sections. +2. Clicking this opens the styling menu for that specific part. - - [Ask the Baserow community](https://community.baserow.io) - - [Contact support](/contact) for questions about Baserow or help with your account +### Styling the Label +* **Typography:** Font family, weight, size, and color. +* **Alignment:** Left, Center, or Right align the label text. +### Styling the Input Box +* **Text:** Font style for the date inside the box. +* **Background & Borders:** Fill color, border width, color, and radius. +* **Calendar Icon:** The icon usually inherits the text color; check overrides for specific icon styling options if available. - [1]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/132f134d-6ec5-477f-b703-b26b01060dbf/date_time_picker.webp",,baserow_user_docs,https://baserow.io/user-docs/application-builder-date-time-picker-element -294,Multi-page container,application-builder-multi-page-container,Multi-page container in Baserow Application Builder,"# Application Builder - Multi-page header and footer elements +## Frequently asked questions (FAQ) -Multi-page header and footer elements allow you to create reusable containers that can be applied across multiple pages of your application. +### How is the date saved? +Regardless of the *display* format (US/EU), Baserow typically sends the date to the backend in standard ISO format (YYYY-MM-DD) or UTC timestamps to ensure data consistency. -This streamlines your workflow by allowing you to define a consistent structure once and apply it throughout your application. This not only saves time but also ensures design consistency. +### Can I restrict the date range? +Currently, the date picker allows selection of any valid date. To restrict range (e.g., ""Future dates only""), you would typically handle this validation on the backend or via form submission logic. -![Image: Baserow multipage header and footer][1] +### Why don't I see the time options? +Ensure the **Include Time** checkbox is selected in the element configuration. Without this, the element functions as a strict Date picker. -## Overview +## Related content + + * [Form Element][159] + * [Connecting Data Sources][144] + * [Element Styling (Layout vs. Theme)][148] + +--- -The header and footer elements are perfect for common page elements like navigation bars, branding, or footer information. -With the reusable design, you can apply the same header or footer to multiple pages, update it once, and changes reflect across all linked pages. +Still need help? If you're looking for something else, please feel free to make recommendations or ask us questions; we’re ready to assist you. + + - [Ask the Baserow community](https://community.baserow.io) + - [Contact support](/contact) for questions about Baserow or help with your account. -You can configure styling and layout using the element settings. For further details, refer to the [element style customization guide][2]. + [140]: /user-docs/application-settings + [144]: /user-docs/data-sources + [148]: /user-docs/element-style + [159]: /user-docs/application-builder-form-element",application builder,baserow_user_docs,https://baserow.io/user-docs/application-builder-date-time-picker-element +294,Multi-page container,application-builder-multi-page-container,Multi-page container in Baserow Application Builder,"# Multi-page header and footer -## Add a multi-page container +The **Multi-page Header** and **Multi-page Footer** are special container elements designed for consistency. Unlike standard elements that exist only on the specific page where you add them, these global elements allow you to design a structure once and broadcast it to every page in your app. -To add the multi-page header or multi-page footer to a [page][3]: +This page covers how to create global navigation bars and footers that update across your entire application. -1. Open the elements panel from the page editor interface. -2. Scroll through the list or search for: - - **Multi-page header** to add a reusable header: Add navigation menus, logos, or search bars to all pages. - - **Multi-page footer** to add a reusable footer: Include contact information, links to privacy policies, or copyright text. -3. Select to add the desired element to the page. +## Overview + +The header and footer elements are perfect for common page elements like navigation bars, branding, or footer information. -## Configure the multi-page element +* **Write Once, Update Everywhere:** If you fix a typo in your footer, it updates on every page instantly. +* **Automatic Positioning:** Headers always snap to the very top; Footers always snap to the very bottom. +* **Container Logic:** These elements act as empty shells. You must place other elements (like [Columns][1], [Images][2], or [Menus][3]) *inside* them to add content. +* **Visibility Control:** You can choose to show these global elements on all pages, specific pages, or exclude them from certain pages (like a Login screen). -The header will automatically position itself at the top of the page. The footer will automatically position itself at the bottom of the page. +![Image: Baserow multipage header and footer][4] -Once you’ve added the header or footer, you can customize how and where it appears. +## Add a global element -### Display settings +1. Open the **Elements** panel. +2. Scroll to the **Layout** section (or search). +3. Select **Multi-page header** or **Multi-page footer**. +4. The element will automatically snap to the top or bottom of your canvas. -You have three options to configure visibility where headers or footers appear across your pages: +## Add content (Populating the container) -1. **On all pages** +By default, a new header or footer is an empty container. To make it useful, you must add content *inside* it. -The element appears on every page within your application. +1. **Drag and Drop:** Select standard elements (e.g., an [Image][2] for your logo, a [Menu][3] for navigation links). +2. **Place Inside:** Add them into the header/footer area until you see the drop zone highlight. +3. **Layout:** We highly recommend adding a **[Columns element][1]** inside the header first. This allows you to place your Logo on the left and your Menu on the right. -2. **Only on selected pages** +> **Styling Note:** These containers do not have their own ""Theme Overrides."" To style the look (e.g., change the font of the links or the size of the logo), you must configure the properties of the specific elements you placed inside. -You can choose specific pages where the element should appear. A list of all pages will be displayed for easy selection. For example, apply a footer only to specific content pages like About or Contact. +## Configure display settings (Visibility) -3. **Exclude selected pages** +The most powerful feature of multi-page elements is controlling *where* they appear. You might want a Header on all pages *except* your ""Sign Up"" page. -The element will appear on all pages except those you select. For example, exclude the header from a login page or a specialized landing page. +1. Click the Header or Footer element to open the **General** properties tab. +2. Locate the **Display** section. +3. Choose one of the following modes: + +| Option | Description | Use Case | +| :--- | :--- | :--- | +| **On all pages** | The element appears everywhere. | Standard navigation bars. | +| **Only on selected pages** | You manually check which pages show this element. | A specialized footer for ""Help Center"" pages only. | +| **Exclude selected pages** | Appears everywhere *except* the pages you check. | Hiding the main menu on ""Login"" or ""404 Error"" pages. | -### Steps to configure display settings +## Frequently asked questions (FAQ) -1. Click on the **Multi-page header** or **Multi-page footer** element in the editor. -2. Open the **General settings** panel. -3. Under the **Display** section, choose one of the [following display settings][4]: - - On all pages - - Only on selected pages - - Exclude selected pages -4. For selective options, use the displayed list to check/uncheck pages as needed. +### Can I have multiple headers? +You can technically add multiple header elements, but they will stack at the top. A better approach for different user roles (e.g., Admin vs. User) is to have **one** header containing elements with **[Visibility Roles](/user-docs/application-builder-element-visibility)** applied to them. -For more design control, combine multi-page headers/footers with elements like [columns][5] or [buttons][6] for dynamic layouts. +### How do I change the background color? +Since the Multi-page element is a structural container, it usually adopts the page background or a transparent state. To create a colored bar, add a **Container** or **Column** element *inside* the header, set its background color using the **Style Tab**, and ensure it is set to ""Full Width."" -You can [edit or update][7] the multi-page elements at any time; changes are reflected across all pages where they are applied. +### Can I move the header to the middle of the page? +No. Multi-page headers are hard-coded to remain at the top of the viewport, and footers at the bottom, to ensure consistent UX standards. ## Related content - -- [Customizing element style][2] -- [Managing page layouts and elements][7] + + * [Menu Element][3] + * [Columns Element (Layout)][1] + * [Element Visibility (Permissions)][5] --- -Still need help? If you're looking for something else, please feel free to make recommendations or ask us questions—we’re ready to assist you. + +Still need help? If you're looking for something else, please feel free to make recommendations or ask us questions; we’re ready to assist you. - [Ask the Baserow community](https://community.baserow.io) - - [Contact support](/contact) for questions about Baserow or help with your account + - [Contact support](/contact) for questions about Baserow or help with your account. - [1]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/c4f30052-7e54-4083-834f-9eef7429b5fb/multi_page_header_and_footer_elements.webp - [2]: /user-docs/element-style - [3]: /user-docs/pages-in-the-application-builder - [4]: #display-settings - [5]: /user-docs/application-builder-columns-element - [6]: /user-docs/application-builder-button-element - [7]: /user-docs/elements-overview",,baserow_user_docs,https://baserow.io/user-docs/application-builder-multi-page-container + [1]: /user-docs/application-builder-columns-element + [2]: /user-docs/application-builder-image-element + [3]: /user-docs/menu-element + [4]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/78b5ef1e-b2a8-4370-a542-754c908cc71d/Multipage%20header.png + [5]: /user-docs/application-builder-element-visibility",application builder,baserow_user_docs,https://baserow.io/user-docs/application-builder-multi-page-container 326,Dashboards overview,dashboards-overview,Dashboards overview in Baserow,"# Baserow Dashboards overview Baserow Dashboards help you visualize and analyze your data through customizable widgets. Create visual representations of your metrics, monitor trends, and present information clearly to stakeholders. Dashboards automatically update when your data changes, ensuring you always see current information. @@ -25615,192 +26028,305 @@ Still need help? If you’re looking for something else, please feel free to mak [2]: https://baserow.io/user-docs/dashboards-overview [3]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/e73b6055-ba76-459b-a51c-ec25bbccf302/create.png [4]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/02403839-efc7-4bcb-935b-01b2e426d442/Add%20a%20Widget.png",dashboard,baserow_user_docs,https://baserow.io/user-docs/create-a-dashboard -328,Menu element,menu-element,Menu element in Baserow Application Builder,"## Overview of the Menu element +328,Menu element,menu-element,Menu element in Baserow Application Builder,"# Menu element + +The Menu element is a container for navigation links. Unlike individual [Link elements][153], the Menu element groups multiple items together, handling alignment, spacing, and layout (horizontal vs. vertical) automatically. It is essential for headers, footers, and sidebars. -The Menu element is a powerful navigation component in the Application Builder that helps users move through different parts of your application smoothly. It creates an organized structure that makes navigation intuitive and efficient. +This page covers how to create structured navigation bars or sidebars to help users move through your application. -You can configure the Menu element's properties, style, and functionality through the element settings. +## Overview -## Add and configure Menu elements +The Menu element helps users move through different parts of your application smoothly. It creates an organized structure that makes navigation intuitive and efficient. -Adding a Menu element to your application is straightforward through the elements panel. +* **Structure:** Organizes multiple navigation items in a list. +* **Orientation:** Can be displayed **Horizontally** (Top Bar) or **Vertically** (Sidebar). +* **Items:** Each item can link to an internal page or external URL. +* **Styling:** Visual properties for **Buttons** and **Links** are managed via **Theme Overrides**. -1. Open the **Elements** panel from the top bar -2. Select the **Menu** element -3. Configure menu items and styling through the properties panel +## Add a menu -### Menu Styling Options +1. Open the **Elements** panel (click the `+` icon). +2. Select **Menu**. +3. Drag and drop the element onto your canvas (typically into a [Column][156] or [Header/Footer][167] container). -The Menu element offers two main styling approaches: +## Configuration -- **Button style:** Creates a button-like menu that's prominent and easy to click. Ideal for primary navigation options. -- **Link style:** Provides a more subtle, text-based navigation approach. Perfect for secondary navigation or when space is limited.",,baserow_user_docs,https://baserow.io/user-docs/menu-element -359,Rating element,application-builder-rating-element,Rating element in Baserow Application Builder,"# Application Builder - Rating element +Click the menu element to open the **General** properties tab. -The Rating element allows users to display, collect, and view ratings in your application. Rating elements provide visual feedback through star, heart, or numeric representations, making them ideal for collecting user feedback or displaying evaluation scores. +### 1. Menu Items +This is where you define the links, buttons, separators, or spacers. +* **Add Item:** Click **+ Add menu item** to create a new link. +* **Label:** Enter the text displayed to the user (e.g., ""Home"", ""Profile"", ""Log Out""). +* **Destination:** Configure the link target: + * **Page:** Select an internal application page (and map parameters if needed). + * **Custom URL:** Enter an external web address. +* **Variant:** Choose if this specific item should look like a **Link** (text) or a **Button** (boxed). -In this section, we'll guide you through setting up different rating elements and explain each configuration option in detail. +> **Tip:** You can drag and drop items in the list to reorder them. -## Overview +### 2. Orientation +Define the layout flow of the menu. +* **Horizontal:** Items are placed side-by-side. Best for top navigation bars. +* **Vertical:** Items are stacked on top of each other. Best for sidebars or mobile drawers. + +### 3. Alignment +Control how the items sit within the container. +* **Left/Start:** Items group to the left. +* **Center:** Items group in the middle. +* **Right/End:** Items group to the right. + +## Styling and Theme Overrides + +To customize fonts, colors, and spacing, use **Theme Overrides**. + +1. In the General tab, click the **Style/Settings icon** (slider icon) next to the configuration fields. +2. This opens the **Menu Styling** modal. +3. You will see two specific tabs for customizing the different item variants: + +### The ""Link"" Tab +Customize items set to the **Link** variant. +* **Typography:** Font, size, and weight for text links. +* **States:** Define colors for **Default**, **Hover**, and **Active** (current page) states. + +### The ""Button"" Tab +Customize items set to the **Button** variant. +* **Background & Border:** Define the shape and color of the button box. +* **Typography:** Style the text inside the button. +* **States:** distinct styles for Hover and Active states. + +> **Why two tabs?** This allows you to mix styles in a single menu. For example, you can have ""Home"" and ""About"" look like simple **Links**, while ""Sign Up"" looks like a prominent **Button**. + +## Frequently asked questions (FAQ) + +### How do I indicate the current page? +The Menu element automatically detects the active page based on the URL. You can style this ""Active State"" in the **Theme Overrides** menu (e.g., making the current link bold or blue) so users know where they are. + +### Can I create a dropdown menu? +Currently, the Menu element supports a single level of links. To create a dropdown, you may need to use a container with visibility logic or check for updates to the Menu component. + +### Does the menu collapse on mobile? +The Menu element itself simply lists items. For a responsive mobile experience (like a hamburger menu), you typically place the Menu element inside a container that is hidden on Desktop and visible on Mobile (using [Visibility settings][150]). + +## Related content -Baserow offers three different rating element types to serve various purposes in your application: + * [Multi-page Header and Footer][167] + * [Link Element Configuration][153] + * [Element Styling (Layout vs. Theme)][148] -1. **Rating**: Shows a static rating value represented by stars, hearts, or numbers. This is useful for displaying pre-existing ratings from your data source. -2. **Rating Input**: Allows users to provide ratings that can be stored in your database. Perfect for collecting user feedback or reviews. -3. **Rating Column**: Integrates within table elements to display rating values alongside other data in a structured format. +--- + + +Still need help? If you're looking for something else, please feel free to make recommendations or ask us questions; we’re ready to assist you. + + - [Ask the Baserow community](https://community.baserow.io) + - [Contact support](/contact) for questions about Baserow or help with your account. + +[1]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/example_menu_screenshot.png +[148]: /user-docs/element-style +[150]: /user-docs/application-builder-element-visibility +[153]: /user-docs/application-builder-link-element +[156]: /user-docs/application-builder-columns-element +[167]: /user-docs/application-builder-multi-page-container",application builder,baserow_user_docs,https://baserow.io/user-docs/menu-element +359,Rating element,application-builder-rating-element,Rating element in Baserow Application Builder,"# Application Builder - Rating elements -For all rating element types, you can configure the element properties and [style](/user-docs/element-style) from the element settings panel. +Baserow provides two distinct elements for handling ratings. The **Rating** element is read-only and used to visualize data (e.g., ""4.5 out of 5 stars""). The **Rating Input** element is interactive and used in forms to collect feedback from users (e.g., ""Rate this product""). -## Add and configure rating elements +This page covers how to display evaluation scores or collect user feedback using visual symbols like stars and hearts. -To add a rating element, access the [elements panel](/user-docs/elements-overview) and select **Rating** or **Rating input**. +## Overview + +The Rating element is perfect for showing existing ratings from your data sources. It’s a read-only element that visualizes numeric values as ratings. -Once added, place the rating element wherever you want it on the [page](/user-docs/pages-in-the-application-builder). Don't worry if it's not perfectly positioned initially; you can always move it later. + * **Two Types:** Use **Rating** for display and **Rating Input** for data collection. + * **Visuals:** Customize symbols (Stars, Hearts, Thumbs) and colors. + * **Scale:** Define a maximum value (e.g., 5-star or 10-star scale). + * **Data Binding:** Bind the rating value to a Data Source to display dynamic scores from your database. -> Learn more about how to [add and remove an element from a page](/user-docs/add-and-remove-elements). +![rating element][1] -Now, you'll configure the rating element's properties to make it function and look the way you want. +## Add a rating element -## Rating element +1. Open the **Elements** panel (click the `+` icon). +2. Select either **Rating** (Display) or **Rating Input** (Form). +3. Drag and drop the element onto your canvas. -The Rating element is perfect for showing existing ratings from your data sources. It's a read-only element that visualizes numeric values as ratings. +## Configuration -### Configuration options +Click the element to open the **General** properties tab. Both element types share core visual settings, while the Input element has additional form options. - **Rating value**: Set the value to display. This can be: - - A static numeric value between 0 and 10 - - A field from a connected [data source](/user-docs/data-sources) -
- **Maximum rating**: Define the maximum possible rating value, which determines the number of rating symbols displayed (between 1 and 10). -
-**Style**: Select the visual representation of your ratings: - - Stars - - Hearts - - Thumbs up - - Flags - - Smiles -
- - **Symbol color**: Customize the color of filled symbols using: - - Hexadecimal color code - - RGB value - - Color picker - - Primary/Secondary/Border/Success/Warning/Error +### 1. Value & Scale +Define the data behind the rating. -## Rating Input element +* **Value (Display Only):** The number to visualize. + * *Static:* Enter a fixed number (e.g., `5`). + * *Dynamic:* Click the **Connect Data** icon to bind this to a [Data Source][2]. Use this to show a record's specific rating. +* **Default Value (Input Only):** The initial rating selected when the page loads (optional). +* **Maximum:** Set the scale of the rating (e.g., **5** for a standard review, **10** for a detailed score). -The Rating Input element allows users to provide ratings by clicking or tapping on rating symbols. This interactive element is perfect for forms and feedback collection. +### 2. Visual Style +Customize the look of the symbols. -### Configuration options +* **Style:** Choose the icon shape: + * Stars (Standard reviews) + * Hearts (Likes/Favorites) + * Thumbs Up (Approval) + * Smiles (Satisfaction/NPS) + * Flags (Priority) +* **Color:** Choose the active color for the filled symbols (e.g., Gold for stars, Red for hearts). -In addition to the display options available in the Display Rating element, the Rating Input includes these additional settings: +### 3. Form Logic (Input Only) +If you are using the **Rating Input** element, you will see these additional options: -- **Label**: Add descriptive text above the rating input to guide users. +* **Label:** Descriptive text displayed above the stars (e.g., ""How would you rate our service?""). +* **Required:** Forces the user to select a rating before submitting the form. -- **Default value**: Set a pre-selected rating that appears when the page loads. -- **Required**: When enabled, users must provide a rating before form submission. +## Use cases -## Rating Column in Table elements +### Displaying Reviews (Read-only) +Use the **Rating** element inside a **Repeat** or **Table** element. Bind the **Value** property to your database's ""Score"" field. This creates a dynamic list where every item shows its correct star rating. -When configuring a table element, you can display rating values as one of the column types. This integrates seamlessly with your tabular data. +### Collecting Feedback (Input) +Use the **Rating Input** element inside a **Form Container**. When the user submits the form, the selected value (1-5) is saved to your database number field. -### Configuration steps +## Usage in Table elements +In addition to the standalone elements, you can also display ratings directly inside a **Table** element. +1. Select your **Table** element. +2. Edit the columns. +3. Set the column type to **Rating**. +4. Configure the style (Stars/Hearts) and Maximum value to match your data. -1. Add a [table element](/user-docs/application-builder-table-element) to your page -2. In the table's field configuration, add a new field or edit an existing one -3. Set the field type to **Rating** -4. Configure the value mapping to the data source field containing rating values -5. Adjust the rating display settings to match your needs -
-## Working with Rating data -
-### Connecting to data sources +## Frequently asked questions (FAQ) -The Application Builder allows you to bind rating elements to [data sources](/user-docs/data-sources). This enables: +### Can I use partial ratings? +The **Rating** (Display) element visualizes decimals (e.g., 4.5 will show 4 full stars and 1 half star). The **Rating Input** currently accepts integers (whole numbers) for user selection. -- Display Rating elements to show values from your database -- Rating Input elements to store user ratings back to your database -- Dynamic rating displays that update when data changes +### What data type should I use in the database? +Ratings are numbers. Ensure your database field is set to **Number** (with decimals enabled if you want to store averages like 4.2). -### Using Rating elements with workflows +### Can I change the size of the stars? +The size is determined automatically by the layout. To adjust spacing or layout, check the **Style** tab for width and padding options. -Rating Input elements work particularly well with workflow actions: +## Related content -- Collect user feedback and store it in database tables -- Create satisfaction surveys with multiple rating inputs -- Update existing ratings based on user input + * [Using the Form Element][3] + * [Connecting Data Sources][2] + * [Table Element Configuration][4] --- -Still need help? If you're looking for something else, please feel free to make recommendations or ask us questions—we're ready to assist you. + +Still need help? If you're looking for something else, please feel free to make recommendations or ask us questions; we’re ready to assist you. - [Ask the Baserow community](https://community.baserow.io) - - [Contact support](/contact) for questions about Baserow or help with your account",,baserow_user_docs,https://baserow.io/user-docs/application-builder-rating-element + - [Contact support](/contact) for questions about Baserow or help with your account. + + + [1]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/c1fcc5b8-74a7-496b-836c-90c0b165ba94/Rating%20element.png + [2]: /user-docs/data-sources + [3]: /user-docs/application-builder-form-element + [4]: /user-docs/application-builder-table-element",application builder,baserow_user_docs,https://baserow.io/user-docs/application-builder-rating-element 360,File input element,application-builder-file-input-element,File input element in Baserow Application Builder,"# Application Builder - File Input element -The File Input element enables users to upload files directly through your application. This enterprise/advanced feature enhances data collection capabilities by allowing end users to submit files that can be stored in your Baserow tables. +The File Input element creates a drag-and-drop zone or selection button for file uploads. It is designed to work with **File** fields in your Baserow database, enabling workflows like submitting resumes, uploading receipts, or updating profile pictures. -In this section, we'll guide you through setting up a file input element and explain each configuration option in detail. +This page covers how to allow users to upload images, documents, and media to your database. ## Overview -For the file input element, you can configure the element properties and [style](/user-docs/element-style) from the element settings panel on the right side of the screen. +The File Input element is particularly useful when combined with workflow actions to create or update rows in your database, with the uploaded files stored in “File” type fields. -The File Input element is particularly useful when combined with workflow actions to create or update rows in your database, with the uploaded files stored in ""File"" type fields. +* **Function:** Captures files from the user's device. +* **Constraints:** You can limit uploads by **File Type** (e.g., `.pdf` only) and **Max Size**. +* **Storage:** Files are not saved immediately; they are staged until a **Button Action** (Create/Update Row) submits them to the database. +* **Styling:** Visuals for the input box and label are managed via **Theme Overrides**. -## Add and configure file input elements +## Add a file input -To add a file input element, access the [elements panel](/user-docs/elements-overview) and select **File Input**. +1. Open the **Elements** panel (click the `+` icon). +2. Select **File Input**. +3. Drag and drop the element onto your canvas. -Once added, place the file input wherever you want it on the [page](/user-docs/pages-in-the-application-builder). Don't worry if it's not perfectly positioned initially; you can always move it later. +## Configuration -> Learn more about how to [add and remove an element from a page](/user-docs/add-and-remove-elements). +Click the element to open the **General** properties tab. -Now, you'll customize the file input's behavior and appearance by configuring its properties. +### 1. General Settings +* **Label:** Descriptive text above the input (e.g., ""Upload Resume""). +* **Help text:** Instructions displayed inside the drop zone (e.g., ""PDFs only, max 5MB""). +* **Required:** If checked, the user cannot submit the associated form/action without uploading a file. +* **Multiple files:** Toggle this to allow uploading more than one file at a time. +* **Preview images:** If enabled, image files will show a thumbnail preview after selection instead of just the filename. -## File Input configuration options +### 2. Constraints (Validation) +Control what users are allowed to upload. +* **Allowed file types:** Define specific extensions (one per line). + * *Examples:* `jpg`, `png`, `pdf`. + * *Wildcards:* `image/*` (all images), `video/*` (all video), `audio/*` (all audio). + * *Empty:* Leave blank to allow **all** file types. +* **Maximum file size:** Set a limit in MB (e.g., `10`) to prevent large uploads. -### General settings +### 3. Initial Values (Pre-fill) +Use these settings when building an ""Edit"" form where a file might already exist. +* **Initial file URL:** Bind this to a [Data Source][144] (e.g., `Current Record > Profile Picture`) to show the existing file. +* **Initial file name:** Bind this to the filename from your data source so the user sees the correct name. - - **Label**: Add a descriptive text that appears above the file input to guide users on what files to upload. +## Styling and Theme Overrides - - **Help text**: Provide additional context or instructions inside the button to assist users with file selection. *Leaving this empty will result in the default text Drag and drop files here or click to select* +By default, the input inherits styles from your [Global Theme][140]. To customize it, use **Theme Overrides**. - - **Required**: When enabled, users must upload at least one file before form submission is allowed. +1. In the General tab, click the **Style/Settings icon** (slider icon) next to the **Label** or **Input** sections. +2. This opens the styling menu with two tabs: - - **Multiple files?**: Enable this to allow the user to upload more than one file. +### Input Tab (The Box) +* **Background:** Color of the drop zone. +* **Border:** Color, width, and radius of the upload box. +* **Padding:** Spacing inside the drop zone. - - **Initial file URL?**: Shows an example file underneath the input. +### Typography Tab (The Text) +* **Font:** Style the Label and Help Text (Family, Size, Weight, Color). +* **Alignment:** Left, Center, or Right align the text. - - **Initial file name?**: Gives the example file a title. +## How to save uploaded files -### File restrictions +The File Input element does not save files automatically. You must connect it to a workflow. - - **Allow multiple files**: Toggle this option to allow users to upload one or multiple files within a single file input element. +1. Add a **Button** element below the File Input. +2. Go to the Button's **Events** tab. +3. Add an action: **Create Row** or **Update Row**. +4. In the action settings, map your database's **File Field** to the **File Input** element's value. - - **Maximum files**: Define how many files users can upload (when multiple files are allowed). This setting helps prevent excessive uploads. +> **Note:** When the user clicks the button, the file is uploaded to Baserow storage and linked to the new/updated row. - - **Maximum file size**: Set the maximum allowed size for each uploaded file (in MB). This helps control storage usage and prevents extremely large file uploads. +## Frequently asked questions (FAQ) - - **Preview images?**: Will generate a small preview of the file if it is an image file format. +### Can I rename files upon upload? +No. The file is saved with its original name from the user's device. To rename it, you would need to use an external automation (e.g., Make/Zapier) triggered after the row is created. -## Working with uploaded files +### What happens if I verify ""Multiple files"" but the database field is single? +The database field configuration takes precedence. If your Baserow database field is not set to accept multiple files, only the first file might be saved, or the action may fail. Ensure your database structure matches your frontend constraints. -### Using with workflow actions +### Is there a storage limit? +Yes. The total storage depends on your Baserow plan limits. The ""Maximum file size"" setting in the element is a frontend restriction you set for your users, but the system-wide limit still applies. -The File Input element works seamlessly with workflow actions to store uploaded files in your database: +## Related content -1. **Create Row action**: Include the uploaded file in a new database record -2. **Update Row action**: Add or replace files in an existing record -3. **Form Submit action**: Process multiple inputs including file uploads + * [Button Element (for saving data)][157] + * [Connecting Data Sources][144] + * [File Field Type](/user-docs/file-field) --- -Still need help? If you're looking for something else, please feel free to make recommendations or ask us questions—we're ready to assist you. + +Still need help? If you're looking for something else, please feel free to make recommendations or ask us questions; we’re ready to assist you. - [Ask the Baserow community](https://community.baserow.io) - - [Contact support](/contact) for questions about Baserow or help with your account",,baserow_user_docs,https://baserow.io/user-docs/application-builder-file-input-element + - [Contact support](/contact) for questions about Baserow or help with your account. + + [140]: /user-docs/application-settings + [144]: /user-docs/data-sources + [157]: /user-docs/application-builder-button-element",application builder,baserow_user_docs,https://baserow.io/user-docs/application-builder-file-input-element 361,MCP Server,mcp-server,Baserow MCP Server overview,"# MCP Server in Baserow Baserow's Model Context Protocol (MCP) Server lets you connect Large Language Models like Claude, Cursor, or Windsurf directly to your workspace. You can create, read, update, and delete data using natural language prompts without writing code or API calls. @@ -26974,7 +27500,7 @@ Baserow integrates with multiple AI providers (OpenAI, Anthropic, Ollama, OpenRo Baserow's generative AI configuration lets you connect your preferred AI models directly to your workspace. -Baserow Self-Hosted users can configure at workspace-level for team-specific models, or instance-level for organization-wide settings. +Baserow Self-Hosted users can configure at the workspace-level for team-specific models, or instance-level for organization-wide settings. ![Baserow AI field configuration interface][2] @@ -26997,26 +27523,26 @@ For self-hosted Baserow, configure API keys globally using [environment variable ## Supported AI providers -| Provider | Required Configuration | Example Models | -|----------|----------------------|----------------| -| **OpenAI** | API key + Organization (optional) + Enabled Models | gpt-3.5-turbo, gpt-4 | -| **Anthropic** | API key + Enabled Models | claude-3-haiku-20240307, claude-opus-4-20250514 | -| **Ollama** | Host + Enabled Models | llama2, mistral | -| **Mistral** | API key + Enabled Models | mistral-large-latest, mistral-small-latest | -| **OpenRouter** | API key + Organization (optional) + Enabled Models | openai/gpt-4o,anthropic/claude-3-haiku | +| Provider | Required Configuration | +|----------|----------------------| +| **OpenAI** | API key + Organization (optional) + Enabled Models | +| **Anthropic** | API key + Enabled Models | +| **Ollama** | Host + Enabled Models | +| **Mistral** | API key + Enabled Models | +| **OpenRouter** | API key + Organization (optional) + Enabled Models ### OpenAI setup Provide: - [OpenAI API key][6] for authentication. Without an API key, users cannot select OpenAI models. - OpenAI organization name (optional) -- Comma-separated list of [OpenAI models][7] to enable (e.g., `gpt-3.5-turbo, gpt-4-turbo-preview`). Note that this only works if an OpenAI API key is set. If this variable is not provided, the user won’t be able to choose a model. +- Comma-separated list of [OpenAI models][7] to enable. Note that this only works if an OpenAI API key is set. If this variable is not provided, the user won’t be able to choose a model. ### Anthropic setup Provide: - [Anthropic API key][8] for authentication - - Comma-separated list of [Anthropic models][9] to enable (e.g., `claude-3-haiku-20240307, claude-opus-4-20250514`) + - Comma-separated list of [Anthropic models][9] to enable ### OpenRouter setup @@ -27024,13 +27550,13 @@ Provide: - [OpenRouter API key][10] for authentication. - Optionally provide an Open Router organization name that will be used when making an API connection. - - Provide a comma-separated list of [Open Router models][11] that you would like to enable in the instance (e.g. `openai/gpt-4o`, `anthropic/claude-3-haiku`). Note that this only works if an API key is set. If this variable is not provided, the user won’t be able to choose a model. + - Provide a comma-separated list of [Open Router models][11] that you would like to enable in the instance. Note that this only works if an API key is set. If this variable is not provided, the user won’t be able to choose a model. ### Ollama setup Provide: - [Ollama][12] host URL. Provide the hostname to your Ollama server. This typically runs locally on your own device. -- Comma-separated list of [Ollama models][13] to enable (e.g., `llama2`) +- Comma-separated list of [Ollama models][13] to enable Ollama enables local model hosting for enhanced data privacy. @@ -27038,7 +27564,7 @@ Ollama enables local model hosting for enhanced data privacy. Provide: - [Mistral API key][14] for authentication -- Comma-separated list of [Mistral models][15] to enable (e.g., `mistral-large-latest,mistral-small-latest`) +- Comma-separated list of [Mistral models][15] to enable ## Frequently asked questions @@ -27048,7 +27574,7 @@ Baserow integrates with multiple AI providers (OpenAI, Anthropic, Ollama, OpenRo ### Can I use multiple AI providers simultaneously? -Yes. Configure multiple providers at workspace or instance level. Users can select their preferred model when creating AI fields. +Yes. Configure multiple providers at the workspace or instance level. Users can select their preferred model when creating AI fields. ### Where do I find my API keys? @@ -27060,7 +27586,7 @@ Yes. Configure multiple providers at workspace or instance level. Users can sele ### What happens if I don't configure an API key for self-hosted Baserow? -AI fields will be disabled until you add at least one provider's API key at workspace or instance level. +AI fields will be disabled until you add at least one provider's API key at the workspace or instance level. ### Do workspace-level settings override instance settings? @@ -27898,12 +28424,12 @@ The workflow will only proceed to the nodes *after* the Iterator once all items ### Send a Slack message -This action sends a message directly to a Slack channel or specific user when your workflow runs. +This action sends a message directly to a Slack channel when your workflow runs. **Configuration:** 1. **Integration:** Select your ""Slack Bot"" integration. -2. **Channel:** Choose the channel name or User ID to receive the message. +2. **Channel:** Choose the channel name. 3. **Message:** Enter the text content. You can use dynamic data (e.g., `New lead: [Name]`) to personalize the message. How to set up the Slack Bot: @@ -28505,40 +29031,42 @@ Still need help? If you're looking for something else, please feel free to make [5]: https://baserow.io/user-docs/duration-field [6]: https://baserow.io/user-docs/date-and-time-fields [7]: https://baserow.io/user-docs/link-to-table-field",,baserow_user_docs,https://baserow.io/user-docs/date-dependency -591,AI assistant,ai-assistant,"Meet Kuma, your Baserow AI assistant","# Introduction to Kuma AI +591,Kuma AI assistant,ai-assistant,"Meet Kuma, your Baserow AI assistant","# Introduction to Kuma AI Meet Kuma, your AI assistant. Chat with Kuma to create databases, write formulas, and organize your data; all in plain language. This guide introduces Kuma, Baserow's built-in AI assistant. Learn how to access Kuma, what you can ask it, and how it can help you build and manage your databases faster. +Learn more: [AI assistant developer docs][1] + ## What is Kuma? Kuma is your Baserow AI assistant integrated directly into the Baserow interface. It helps you work faster without switching tools or digging through documentation. It’s not just a chatbot. Kuma understands the context of your workspace and can take action to **create or edit** tables, views, and fields on your behalf. -> If you self-host, you can also choose which [AI provider and model][1] it uses, so you stay in control of your data and privacy. +> If you self-host, you can also choose which [AI provider and model][2] it uses, so you stay in control of your data and privacy. -Learn more: [AI-assistant Configuration][2] +Learn more: [AI-assistant Configuration][3] -![Meet Kuma, your Baserow AI assistant][3] +![Meet Kuma, your Baserow AI assistant][4] ### How to access the AI assistant You can access Kuma from anywhere in your Baserow workspace. 1. Navigate to your workspace. - 2. Click the **Kuma AI** tab located in the side bar. + 2. Click the **Kuma AI** tab located in the sidebar. 3. This will open the Kuma chat panel. 4. Type your request in plain language and press Enter. ## What you can do with the AI assistant -Kuma is a powerful builder's assistant. You can ask it to perform a wide range of tasks, from [building new tables][4] to explaining complex features. +Kuma is a powerful builder's assistant. You can ask it to perform a wide range of tasks, from [building new tables][5] to explaining complex features. ### Build and edit databases -Instead of manually [creating tables][4] and [adding each field][5], you can describe what you need to Kuma. Kuma will build the database for you, complete with relevant fields, views, and relationships. +Instead of manually [creating tables][5] and [adding each field][6], you can describe what you need to Kuma. Kuma will build the database for you, complete with relevant fields, views, and relationships. **Examples:** * `""Create a project management table.""` @@ -28547,7 +29075,7 @@ Instead of manually [creating tables][4] and [adding each field][5], you can des ### Write formulas -Stop trying to remember complex syntax. Describe the calculation you need, and Kuma will write the formula for you. It can [create a new Formula field][6] or update an existing one instantly. +Stop trying to remember complex syntax. Describe the calculation you need, and Kuma will write the formula for you. It can [create a new Formula field][7] or update an existing one instantly. **Examples:** * `""Write a formula that calculates the number of days until the deadline.""` @@ -28556,7 +29084,7 @@ Stop trying to remember complex syntax. Describe the calculation you need, and K ### Organize and manage your data -Use plain language to quickly [manage your data views][7]. Kuma can [create new views][8] or modify your current view by adding filters, sorting rules, or grouping. +Use plain language to quickly [manage your data views][8]. Kuma can [create new views][9] or modify your current view by adding filters, sorting rules, or grouping. **Examples:** * `""Create a new Grid view in my 'Tasks' table called 'High Priority Tasks'.""` @@ -28589,7 +29117,7 @@ Baserow integrates with multiple AI providers (OpenAI, Anthropic, Ollama, OpenRo Kuma is configured to work on versions from Baserow v2.0. -Learn more about [Baserow AI-Assistant: Quick DevOps Setup][9]. This guide shows how to enable the AI-assistant in Baserow, configure the required environment variables, and turn on knowledge-base lookups via an embeddings server. +Learn more about [Baserow AI-Assistant: Quick DevOps Setup][10]. This guide shows how to enable the AI-assistant in Baserow, configure the required environment variables, and turn on knowledge-base lookups via an embeddings server. ## Frequently Asked Questions (FAQs) @@ -28601,7 +29129,7 @@ The **[AI field](/user-docs/ai-field)** operates *inside* a table to perform a s ### Can Kuma see my data? -To perform actions, Kuma needs to understand your workspace structure (like table and field names). As a self-hosted user, if you provide Baserow with an external LLM, our generative AI features will communicate with it and send it contextual data to help with the query. Alternatively, you can self-host your own model and provide the configurations to Baserow, so the data doesn’t leave your environment. By allowing you to [choose your own AI provider in the settings][1], you remain in control of which service handles your data and your privacy. +To perform actions, Kuma needs to understand your workspace structure (like table and field names). As a self-hosted user, if you provide Baserow with an external LLM, our generative AI features will communicate with it and send it contextual data to help with the query. Alternatively, you can self-host your own model and provide the configurations to Baserow, so the data doesn’t leave your environment. By allowing you to [choose your own AI provider in the settings][2], you remain in control of which service handles your data and your privacy. ### What if Kuma makes a mistake? @@ -28622,15 +29150,16 @@ Still need help? If you're looking for something else, please feel free to make - [Contact support](/contact) for questions about Baserow or help with your account - [1]: /user-docs/configure-generative-ai - [2]: /docs/installation%2Fconfiguration#ai-assistant-configuration - [3]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/c0754a24-8b55-4d73-9526-9662d3ef6fb3/Meet%20Kuma%2C%20your%20Baserow%20AI%20assistant.jpg - [4]: /user-docs/create-a-table - [5]: /user-docs/adding-a-field - [6]: /user-docs/formula-field-overview - [7]: /user-docs/view-customization - [8]: /user-docs/create-custom-views-of-your-data - [9]: /docs/installation/ai-assistant",workspace,baserow_user_docs,https://baserow.io/user-docs/ai-assistant + [1]: https://baserow.io/docs/installation%2Fai-assistant + [2]: /user-docs/configure-generative-ai + [3]: /docs/installation%2Fconfiguration#ai-assistant-configuration + [4]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/c0754a24-8b55-4d73-9526-9662d3ef6fb3/Meet%20Kuma%2C%20your%20Baserow%20AI%20assistant.jpg + [5]: /user-docs/create-a-table + [6]: /user-docs/adding-a-field + [7]: /user-docs/formula-field-overview + [8]: /user-docs/view-customization + [9]: /user-docs/create-custom-views-of-your-data + [10]: /docs/installation/ai-assistant",workspace,baserow_user_docs,https://baserow.io/user-docs/ai-assistant 623,Two-factor auth,two-factor-auth,Two-factor authentication in Baserow,"# Two-factor authentication (2FA) Two-factor authentication (2FA) adds an extra layer of security to your Baserow account. In addition to your password, you will need to provide a unique verification code generated by an authenticator app on your phone to log in. @@ -28709,6 +29238,1029 @@ Still need help? If you're looking for something else, please feel free to make - [Ask the Baserow community](https://community.baserow.io) - [Contact support](/contact) for questions about Baserow or help with your account",account management,baserow_user_docs,https://baserow.io/user-docs/two-factor-auth +656,Power BI integration,power-bi-integration,Baserow Power BI integration,"# Power BI integration + +Integrate Baserow with Power BI to visualize your database data. + +You’ve built a database in Baserow. Your data is organized, your workflows are automated, and everything is running smoothly. But now you need to visualize that data in Power BI dashboards. + +## Use Cases: When to connect Baserow to Power BI + + 1. **Executive dashboards**: High-level views of project status and resource allocation. If your team manages projects in Baserow, but executives want a visual dashboard, connect Baserow to Power BI to create real-time project status dashboards so leadership can get insights. + 2. **Multi-source data analysis**: If you have data in Baserow, Excel, and SQL Server that needs combined analysis, Power BI pulls from all three sources and creates unified reports. + 3. **Automated reporting:** Visualize pipeline value and conversion rates from your Baserow CRM. For cases where you currently export Baserow CSVs weekly to create reports, API connection with scheduled refresh eliminates manual exports. + 4. **Client-Facing Reports**: When you need to share polished visualizations with clients, embed Power BI reports in your website or send PDF reports. + 5. **Compliance and Audit Tracking**: When you track compliance data in Baserow and need audit-ready reports, Power BI generates standardized, timestamped reports from Baserow data. + 6. **Inventory Tracking:** Monitor stock levels and trigger visual alerts when supplies run low. + +![Baserow x PowerBI][1] + +This comprehensive guide will walk you through every step of connecting Baserow to Power BI using the Web API, from obtaining your API token to setting up automated refreshes. + +By the end, you’ll have: + +- A live connection between Baserow and Power BI +- Real-time data visualization of your Baserow databases +- Automated refresh schedules that keep your dashboards current +- A deep understanding of how the integration works (and its limitations) + +Let’s transform your Baserow data into compelling visual insights. + +## Integration Architecture + +Before diving into the setup, let’s understand what’s actually happening when you connect Baserow to Power BI. + +Baserow does not push data to Power BI. Power BI pulls data from Baserow. You control exactly what data comes through using URL parameters. + +``` +Baserow Database (the data source) + ↓ +REST API Endpoint (returns JSON) + ↓ +Power BI Web Connector (fetches data) + ↓ +Power Query Editor (transforms JSON to tables) + ↓ +Power BI Data Model (ready for visualization) +``` + +## Prerequisites + + - [**Baserow](https://baserow.io) Account** (Cloud or Self-Hosted)**:** Access to the workspace containing the target database. + - [**Power BI Desktop**](https://powerbi.microsoft.com/desktop/) (latest version recommended) + - Basic understanding of databases (tables, rows, columns, relationships) + - **Connection:** Web API (REST) + - **Auth:** Database Token (API Key) + +## Step-by-Step Implementation + +The Web connector requires manual configuration. You need to construct the correct API endpoint URLs, provide authentication headers, transform the JSON response structure, handle pagination for large datasets, and manage refresh credentials. + +### Step 1: Generate a Database token + +A Database token acts as a security key, proving permission to access specific Baserow databases and tables. + +To generate a database token: + +1. Log in to Baserow. Click your profile icon (top-right) +2. Navigate to **Settings → Database tokens**. +3. Click **+ Create token**. +4. Select the workspace and give your token a descriptive name: `Power BI Integration` +5. **Scope:** Choose which tables this token can access. Select only the tables you need to visualize in Power BI. +6. Set permissions: + - **Create rows:** Optional (only if pushing data from Power BI) + - **Read rows:** ✓ Required + - **Update rows:** Optional + - **Delete rows:** Usually not needed +7. Once created, a long string appears (e.g., `eyJ0eXAiOiJKV1QiLCJh...`). Click the “Copy” icon + +Learn more about [Baserow database tokens](https://baserow.io/user-docs/personal-api-tokens). + +> Database tokens can grant access to your Baserow data. If this token is compromised, your data is at risk. +> + +![Database tokens][2] + +### Step 2: Construct the Endpoint URL + +Power BI needs a specific address to call. To list rows in a table, a `GET` request has to be made to the table endpoint. + +The standard format is: + +```markdown +https://api.baserow.io/api/database/rows/table/[TABLE_ID]/ +``` + +Learn more about the [anatomy of a Baserow API Endpoint](https://baserow.io/user-docs/database-api). + +To get the Table ID, open your table in the browser. Look at the URL: `https://baserow.io/database/15/table/4567/519`, the Table ID is `4567`. + +Learn more about multiple ways to [find a Table ID](https://baserow.io/user-docs/database-and-table-id). + +**Query parameters:** + +The response is paginated, and by default, the first page is returned. The correct page can be fetched by providing the `page` and `size` query parameters. + +Add these parameters to your endpoint URL using `?parameter=value¶meter2=value2` format. + +- `user_field_names=true`: Returns ""Client Name"" instead of ""field_124"". +- `size=200`: This defines how many rows should be returned per page. The default is 100 + +We will cover pagination and field names in futher sections. + +The Final URL will look like this: + +```markdown +https://api.baserow.io/api/database/rows/table/4567/?user_field_names=true&size=200 +``` + +### Step 3: Connect Power BI to Baserow via Web Connector + +Now that we have the API token and endpoint URL, let’s connect Power BI. + +Power BI’s Web connector makes HTTP requests to Baserow’s API endpoints and retrieves the data. + +1. Open Power BI Desktop. Start with a blank report or open an existing one. +2. In the **Home** group of the Power BI Desktop ribbon, select the **Get data** button label or down arrow to open the **Common data sources** list. +3. **Select Web Connector:** In the Get Data dialog, search and select “Web” from the list. Click **Connect**. + + ![Power BI’s Web connector][3] + +4. A connection window appears. In “From Web” dialog, switch the radio button to **Advanced**. +5. **URL parts:** Paste your endpoint URL (`https://api.baserow.io/...`). Click OK. + + If you need to add more than one part, select **Add part** to add another URL fragment text box. As you enter each part of the URL, the complete URL used is displayed in the **URL preview** box. + +6. The default timeout for both `POST` and `GET` is 100 seconds. If this timeout for processing data is too short, you can use the optional **Command timeout in minutes** to extend the time the request remains connected to Baserow. +7. HTTP request header parameters: + - **Key**: Select `Authorization` from the dropdown (or type it). + - **Value**: Enter `Token [YOUR_DATABASE_TOKEN]` in the box next to it. + + > You must include the word ""Token"" followed by a space before the actual database token. + > +8. Click **OK**. + +![Power Query Editor][4] + +If authentication succeeds, Power Query Editor opens showing a preview of your data. + +> After the first connection, Power BI remembers your credentials. To edit them later, naviaget to: Home → Transform Data → Data source settings. Find your Baserow API URL in the list and click “Edit Permissions”. Under “Credentials,” click “Edit”. Update your API token if it changed. Click “OK”. +> + +### Step 4: Clean the Table + +After evaluating query, you will see the Power Query Editor. Power Query converts the JSON response from Baserow into tabular format that Power BI can visualize. + +> If Power Query shows the JSON as a record with fields, transform it into a proper table structure. +> + +Since Power BI auto-expanded the data, you need to tidy up the view: + +1. **Remove Unnecessary Columns:** Unless you are writing a script to loop through pages, the `count`, `next`, and `previous` columns are now duplicates. Right-click the `next` column header and select **Remove**. Do the same for `previous` and `count`. +2. **Rename Columns:** Your data columns may have the prefix `results.` (e.g., `results.id`). Double-click the header, type the new name and click **Enter**. Repeat for other fields to make them readable. +3. **Filter Rows**: If you only need specific rows, click the filter dropdown on any column header. Uncheck values you want to exclude or use “Text Filters” / “Number Filters” for complex conditions. + + We will cover API-level filtering in futher sections. + + +### Step 5: Set Correct Data Types + +Power Query auto-detects data types. **For each column, c**lick the data type icon in the column header to confirm the correct type. + +Go to the **Transform** ribbon and set the types for critical columns like dates and numbers to avoid calculation errors. + +**Common Conversions:** + +- **Text:** For names, descriptions, IDs stored as text +- **Whole Number:** For counts, ratings, integers +- **Decimal Number:** For prices, percentages, decimals +- **Date:** For date-only fields (no time). +- **Date/Time:** For timestamps. Power Query handles this automatically if you select “Date/Time” type. +- **True/False:** For boolean/checkbox fields + +We cover Baserow field types and Power BI translation below. + +![Set Correct Data Types][5] + +If Power BI outputs a JSON object, not a table. You must use Power Query to translate it. + +**Multiple Select Fields (comma-separated):** + +Single Select / Multiple Select comes in as a List or Record. For Multiple Select Fields, if the +API returns: `[""Option 1"", ""Option 2"", ""Option 3""]` + +- For **Single Select**: Click the Expand icon to get the `value`. +- For **Multi Select**: Power BI's **Power Query Editor** recognizes the nested list structure returned by the Baserow API. Click ""Expand to New Rows"" first, then expand to get the `value`. Power BI automatically creates a new row for every item in the list while duplicating the original main record's data. + +**File Fields (extract first URL)** + +Files/Images returns a list of file objects. In Power Query, add a Custom Column to extract just the URL of the first image: + +```objectivec +// Get first file URL= Table.AddColumn(Source, ""File_URL"", each + try [file_field]{0}[url] otherwise null) +``` + +You can track the applied steps under Query Settings and delete a step if needed. + +### Step 6: Build visuals with your Baserow data + +Once your data is transformed in Power Query: + +1. In the Power Query Editor, click **Close & Apply** (top left). +2. The window will close, and you will see a blank white report canvas. +3. On the far right Data pane, you will see your Baserow data ready for visualization. Power BI loads the transformed data into your data model. +4. Select or drag a field from the Data pane (like `count` or `status`) onto the report canvas. It will instantly turn into a chart. +5. **Building a Chart:** + - Click on a **Bar Chart** icon in the Visualizations pane. + - Drag your **Status** field into the ""X-Axis"". + - Drag your **ID** field into the ""Y-Axis"" and right-click it to select ""Count"". +6. **Result:** You now have a live chart showing how many projects you have in every status. Every time you hit ""Refresh,"" this bar chart updates to match Baserow. + +![Build visuals with your Baserow data][6] + +## Handling Pagination + +If your Baserow table has more than 100 rows (or your specified `size` parameter), the API returns results in pages. You need to fetch all pages. + +Baserow API returns maximum 200 rows per request. If you have 1,000 rows, you need 5 requests and need to consider performance. + +### Manual Pagination (Simple Method) + +For small datasets (<1,000 rows), you can simply increase the page size: + +``` +https://api.baserow.io/api/database/rows/table/12345/?user_field_names=true&size=200 +``` + +### Automatic Pagination (Advanced Method) + +If you have 1,000 rows, the standard method above only gets the first 200. You need a loop to automatically follow the `next` URL until all data is retrieved. + +Use Power Query **M Code** in the Advanced Editor to automatically fetch all pages. The M Code will connect to the JSON, loop through the pages, combine them into a list, and convert that list into the final table. + +1. In Power Query, go to **Home** → **Advanced Editor**. The code appears in a text editor +2. Replace the existing code with this script: + + ```markdown + let + // 1. CONFIGURATION + // Your Baserow URL (Ends with a slash) + // If using SaaS: ""https://api.baserow.io/"" + BaseUrl = ""https://api.baserow.io/"", + + // The path to your table. Replace [YOUR_TABLE_ID] with the actual ID (e.g., 5653) + // Note: Do not put query parameters (?size=200) here. + RelativePathStart = ""api/database/rows/table/[YOUR_TABLE_ID]/"", + + // Replace with your actual Token + Token = ""Token [YOUR_API_TOKEN]"", + + // 2. FETCH FUNCTION + // We use 'RelativePath' to satisfy Power BI Service security rules + FetchPage = (relPath) => + let + Source = Json.Document(Web.Contents(BaseUrl, [ + RelativePath = relPath, + Headers = [Authorization=Token] + ])), + results = Source[results], + nextURL = Source[next] + in + {results, nextURL}, + + // 3. RECURSIVE LOOP + FetchAllPages = (relPath) => + let + page = FetchPage(relPath), + results = page{0}, + nextURL = page{1}, + // Logic: If there is a next link, strip the BaseUrl to get just the relative path + newRelPath = if nextURL <> null then Text.Replace(nextURL, BaseUrl, """") else null, + nextResults = if newRelPath <> null then @FetchAllPages(newRelPath) else {} + in + List.Combine({results, nextResults}), + + // 4. EXECUTION + // We add the parameters here (size=200 ensures fewer requests) + SourceList = FetchAllPages(RelativePathStart & ""?user_field_names=true&size=200""), + + // 5. CONVERSION TO TABLE + #""Converted to Table"" = Table.FromList(SourceList, Splitter.SplitByNothing(), null, null, ExtraValues.Error), + FieldNames = List.Distinct(List.Combine(List.Transform(SourceList, each Record.FieldNames(_)))), + #""Expanded Column1"" = Table.ExpandRecordColumn(#""Converted to Table"", ""Column1"", FieldNames, FieldNames) + in + #""Expanded Column1"" + ``` + +3. Replace `[YOUR_TABLE_ID]` and `[YOUR_API_TOKEN]` with your actual data. + + > If you are self-hosting, ensure your `BaseURL` matches your self-hosted domain**.** + > +4. Click **Done** to save changes. + +![Automatic Pagination (Advanced Method)][7] + +After pasting this new M Code into the Advanced Editor, you will need to manually review the data types. Since the M Code dynamically expands all fields, it cannot guess the correct type (Text, Date, Number) for every column. + +**How It Works:** + +1. `FetchPage()` function gets one page and returns results + next URL +2. `FetchAllPages()` recursively calls itself until `next` is null +3. `List.Combine()` merges all pages into one list +4. It scans your data to find the column names automatically, so you never have to update the code when you add fields in Baserow. +5. Standard transformation converts to table + +> If you are working with a large dataset, you can set up incremental refresh so Power BI only downloads the new rows next time. +> + +## Sync data by setting up automated refreshes + +One of the most common questions is: *""If I change a row in Baserow right now, does it change in my Power BI chart immediately?""* + +The answer is: No, it is not instant. ****Power BI doesn’t auto-refresh while viewing. How to make data dynamic: + +1. **In Power BI Desktop:** You must manually click the **""Refresh""** button in the Home ribbon. When you click that, Power BI calls the Baserow API again, fetches the new data, and updates your charts. +2. **In Power BI Web Service:** You can set up a **Scheduled Refresh** (e.g., every morning at 9:00 AM or every hour). + + > Because this connection uses an API Token for security, the Power BI Service generally cannot refresh Web APIs that use custom headers automatically unless you install the **On-Premises Data Gateway**. + > + +![automated refreshes][8] + +## Query Folding + +Large datasets and complex transformations can slow down your reports. + +Web connectors don’t support query folding, so all transformations happen in Power BI. Do as much filtering/sorting at the API level using URL parameters. + +**Example of API-level filtering:** + +```objectivec +// Filter at API levelSource = Json.Document(Web.Contents( + ""https://api.baserow.io/api/database/rows/table/12345/"", [Query=[ + user_field_names=""true"", #""filter__Status__equal""=""Active"" ]] +)) +``` + +Power Query pushes transformations back to the data source (API) instead of processing locally. + +## Supported data types and structures + +### Baserow Field Types and Power BI Translation + +| Baserow field type | Power BI data type | Notes | +| --- | --- | --- | +| **Text** | Text | Direct mapping | +| **Long Text** | Text | Direct mapping | +| **Number** | Decimal Number / Whole Number | Choose based on decimal places | +| **Rating** | Whole Number | Stars become integers (1-5) | +| **Boolean** | True/False | Direct mapping | +| **Date** | Date | ISO 8601 format (YYYY-MM-DD) | +| **Last Modified** | DateTime | Timestamp with timezone | +| **Created On** | DateTime | Timestamp with timezone | +| **URL** | Text | Stored as text; hyperlinks in visual | +| **Email** | Text | Stored as text | +| **Phone Number** | Text | Stored as text to preserve formatting | +| **Single Select** | Text | Returns selected option | +| **Multiple Select** | Text (comma-separated) | Requires splitting in Power Query | +| **File** | Text (JSON) | Returns array of file objects with URLs | +| **Link to Another Record** | Text (JSON) | Returns array of linked record IDs | +| **Lookup** | Text | Returns looked-up value | +| **Formula** | Varies | Depends on formula output type | +| **Rollup** | Number | Aggregated value from linked records | +| **Count** | Whole Number | Count of linked records | +| **Autonumber** | Whole Number | Sequential identifier | + +### Handling **Link to Another Record** + +**Link to Table r**eturns a list of IDs and values. API returns: `[{id: 1, value: ""Project A""}]` + +```json +{ ""linked_field"": [ {""id"": 1, ""value"": ""Record 1""}, {""id"": 2, ""value"": ""Record 2""} ]} +``` + +Power BI can’t auto-detect relationships between linked Baserow tables. You must manually create relationships in data model. + +When you have a ""Link to Table"" field (e.g., a `Company` linked to a `Contact`), you have two ways to handle this in Power BI. + +**Method 1: Best for simple reports** + +In Power Query, ""Expand"" the linked column to pull in the name of the company right next to the contact. Expand to new rows, then expand to select the `value` field. This creates one main table. + +**Method 2: Best for complex reports** + +If you merge ""Customer Name"" onto 10,000 ""Orders"" in Power Query, you are duplicating the text ""John Smith"" 10,000 times. + +Instead of merging the Link to Table relationship in Power Query, you ingest two separate tables into Power BI: one for Contacts and one for Companies. + +1. Load both tables separately. +2. Go to the **Model View** (the third icon on the left sidebar). +3. Drag the `Company_ID` from the Contact table to the `ID` in the Company table. + +This is much faster for Power BI to process and allows you to slice and dice data across different tables without creating duplicates. This allows you to filter multiple tables by a single slicer easily. + +This requires fetching the linked table separately and performing a merge in Power Query. + +### Baserow field names + +In this tutorial, we add `?user_field_names=true` to the API endpoint because seeing a column named `""Client Status""` is easier than seeing `""field_482""`. + +However, if a user renames the column in Baserow from ""Client Status"" to ""Customer Status"", the Power BI refresh fails immediately because it can no longer find the column it’s looking for. + +For production, we recommend that you import the data using the raw field IDs (e.g., `field_482`) where 482 is the field ID. + +In Power Query, right-click the header `field_482` and select **Rename** after importing, to change it to ""Client Status"" *inside* Power BI. + +Now, you can rename the column in Baserow as much as you want, and your report will never break, because Power Query grabs it by ID and the underlying ID (`field_482`) never changes. + +![PowerBI][9] + +## Troubleshooting + +| **Error Code** | **Meaning** | **Solution** | +| --- | --- | --- | +| **401 Unauthorized** | Bad Token | Check that you added the word ""Token "" (with a space) before the key. | +| **404 Not Found** | Bad URL | Check your Table ID. Ensure you didn't paste the Browser URL instead of API URL. | +| **Expression.Error** | JSON Parsing | You are trying to expand a field that is null. Add a ""Remove Errors"" step or use `try/otherwise`. | +| **Credentials Required** | Gateway | You are trying to refresh in the Cloud without a Gateway. Install the Gateway. | +| ""This dataset includes a dynamic data source"" (Refresh Failed) | Power BI Service blocks scripts where the URL changes completely inside a loop. | Use the script above. It uses `RelativePath`, which tells Power BI that the main server (`BaseUrl`) is static and safe, allowing the refresh to run in the cloud. | + +--- + + +Still need help? If you're looking for something else, please feel free to make recommendations or ask us questions; we’re ready to assist you. + + - [Ask the Baserow community](https://community.baserow.io) + - [Contact support](/contact) for questions about Baserow or help with your account. + + + [1]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/8c40cbc7-c60a-4a86-a0e6-e5330e93cdbf/image%202.png + [2]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/a48e3d72-22cf-4eff-82d1-c4ef2634128d/Power_BI_Integration.jpg + [3]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/434d5aba-27c6-409d-9e4a-435f9265817d/Power_BIs_Web_connector.jpg + [4]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/0c6d1e05-1fac-432e-9d9d-f87f765a81cc/image.png + [5]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/ef1ada59-1528-4d05-8220-df483d586ef0/Set_Correct_Data_Types.jpg + [6]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/6ef564e3-21ac-4ce7-90bb-8f061c5092f4/Build_visuals_with_your_Baserow_data.jpg + [7]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/107df665-bbdb-4b62-9849-91d1629140c0/Automatic_Pagination_(Advanced_Method).jpg + [8]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/eb9dca8c-5dc3-4325-8011-305eb1e14023/automated_refreshes.jpg + [9]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/497db143-a601-4486-9c2f-04f8fd5f1c36/image%201.png",integrations,baserow_user_docs,https://baserow.io/user-docs/power-bi-integration +689,Notion integration,notion-integration,Integrate Baserow and Notion,"# Integrate Baserow and Notion + +Notion and Baserow complement each other, especially for users or teams who want both flexible documentation and structured database management. + +Notion is a note-taking, docs, knowledge and task management tool best for project documentation, meeting notes, wikis, and planning. Baserow is a database creation, data management, and automation tool best for managing structured datasets, CRM, inventory, product data, and issue tracking. + +Both tools offer APIs. Baserow REST API can manage, query, and modify tables. Notion API can read and write pages, databases, and blocks. You can build lightweight scripts to keep them synchronized automatically (e.g., with Python or Node.js). + +Using both gives you a combination of context + data: + +- Notion handles narrative and coordination (what, why, who). +- Baserow handles structured execution (how much, when, where). + + In Baserow, you manage due dates with automated reminders for tasks with a specific deadline, dependencies between tasks, and overall marketing program management. Notion is mainly where you write content, Baserow is where you manage tasks, capture performance data of marketing activities and automate work processes. + +## Use cases for using Baserow and Notion + +The specific use cases can vary greatly. These are examples of what you can build with Notion’s public API. + +|   | In Notion | In Baserow | +| --- | --- | --- | +| Project Management & Operations | Document project goals, roadmaps, meeting notes, retrospectives. Each project page in Notion links to a Baserow view filtered for that project’s tasks. | Manage project tasks, milestones, resource allocations, and dependencies using structured tables. | +| CRM or Client Workflows | Store client onboarding docs, meeting notes, contract templates, and communication records. Link each client’s Notion page to their record in Baserow and click through from Notion to update structured CRM data. | Maintain a CRM database of leads, clients, and deals with automation and filters. | +| Product Development & Knowledge Base | Keep design docs, product specs, changelogs, and team wikis. Product managers view the roadmap in Notion. | Track product features, sprints, bugs, and release schedules. Developers update real data in Baserow. | +| Content Creation Pipeline | Write briefs, content calendars, and style guides. Writers use Notion to draft. | Store metadata: content pieces, publication dates, platforms, statuses, and assigned authors. Editors use Baserow to track pipeline progress. | +| Data-Driven Documentation | Maintain dashboards or reports for management with commentary and charts (embedded from Baserow). Use Notion as your readable reporting layer, pulling real-time data from Baserow. | Store raw data and analytics logs that feed the reports. | + + + +## How to connect Baserow and Notion + +We have created a complete, ready-to-use Content Creation Pipeline workflow package for integrating Notion and Baserow. This step-by-step workflow covers how to use both tools together to manage the workflow day-to-day. + +We’ll use the Content Creation Pipeline usecase. You and your team produce blog posts, videos, and social media content weekly. You want to keep structured data (titles, deadlines, statuses) organized and reportable, write and collaborate in Notion, and have both tools stay in sync automatically. + +By the end of this guide, we’ll have a functional workflow that looks like this: + +| Time | Tool | Action | +| --- | --- | --- | +| Morning | Baserow | Add a new content idea in **Baserow** → automation creates a full Notion draft page. | +| Morning | Notion | Open the content dashboard → see embedded Baserow view of today’s tasks | +| Midday | Baserow | Team collaborates and updates the status of content pieces, adjusts deadlines | +| Afternoon | Notion | Write new content using template linked to Baserow record | +| End of day | Notion | Weekly report page in Notion shows an embedded Baserow “Overview” view. Review dashboard → check off “Published” items, syncs to Baserow automatically | + +Writers & Creators work mainly inside Notion for content writing, and update “Status” (Draft, Editing, etc.) in Notion (which syncs back to Baserow). Managers & Editors use Baserow to track overall progress, see what’s due soon, and filter by team member. They also use Baserow dashboards for metrics (e.g., content count per week). Executives / Clients view a Notion “Dashboard” page embedding the relevant Baserow views: Upcoming publications, Published content this month, and Workload per author. + +## Prerequisites + +To follow along with this guide, you will need: + +- A Notion page template +- A Baserow table schema +- A Baserow automation blueprint + +## What we’ll do + + 1. Create the **Baserow table**. + 2. Create the **Notion database** using a template structure. + 3. Connect both through automations. + 4. Use the Update page API to modify attributes of the Notion page, such as its properties, icon, or cover. Auto-populate and embed the Baserow dashboard view for live update. The public Baserow row URL will show as an embed block (live table row view). + +![Notion <> Baserow technical reference diagram][1] + +### 1. Create Baserow Table Schema + +This will hold the content pipeline. Create a table in Baserow named “Content Pipeline” with the following fields: + +| Field Name | Type | Example / Notes | +| --- | --- | --- | +| **Title** | Text | “10 AI Tools for Designers” | +| **Type** | Single Select | Blog / Video / Newsletter | +| **Status** | Single Select | Idea / Drafting / Editing / Published | +| **Author** | Text or Link to “Team Members” table | “Alex Rivera” | +| **Publish Date** | Date | 2025-11-14 | +| **Notion URL** | URL | Link to the Notion page | +| **Notes** | Long Text | Background info, keywords, etc. | +| **Days Until Publish** | Formula | `date_diff('day', field('Publish Date'), today())` | +| **Overdue?** | Formula | `if(field('Days Until Publish') < 0, '⚠️ Yes', '✅ No')` | + +Then create filtered views (e.g., “By Status” or “By Author”), so your team can easily focus on what’s relevant. + +### 2. Create a database template in Notion + +Database templates save time when adding a new page to a data source. Instead of building manually from a blank page, templates accelerate your workflows by providing a blueprint for the page's properties and content. + +In Notion, we will create a reusable “Weekly Review” page template inside a database. + +Create a database, click the dropdown arrow next to **`New`** at the top right of the database, then select **`+ New template`**. + +--- + +**Template Title:** `{{Title}} – Content Draft`. Whatever you title this page will be the name of the template you use going forward. + +**Page structure:** On this page, we will define properties for the template and add content to the page itself. + +**Metadata** + +| Property | Type | Description | +| --- | --- | --- | +| **Content Type** | Select | Blog / Video / Newsletter / Social | +| **Status** | Select | Idea / Drafting / Editing / Published | +| **Author** | Person | Assigned writer | +| **Publish Date** | Date | Planned publish date | +| **Baserow Record URL** | URL | Link to corresponding Baserow row. | + +**Section 1: Content Brief** + +- **Goal:** What’s the purpose or target KPI? +- **Audience:** Who are we speaking to? +- **Key message:** Main takeaway or insight. + +**Section 2: Draft** + +- Write or paste your draft content here. +- Use headings, callouts, and comments collaboratively. + +**Section 3: Publishing Checklist** + +- [ ] Edited and proofread +- [ ] Approved by manager +- [ ] Graphics complete +- [ ] Uploaded to CMS + +**Section 4: Analytics & Notes** + +- **Performance link:** (once published) +- **Notes / Feedback:** + +--- + +### 3. Create Notion integration + +Integrations define how the public API can interact with your Notion workspace. We want to use the Notion template when creating pages in the API. + +1. Create a new integration with **Insert Content capabilities** on the target parent page or database. For your integration to interact with the page, it needs explicit permission to read/write to that specific Notion page. +2. **Get your API secret:** API requests require an API secret to be successfully authenticated (or “Internal Integration Secret”). Any time your integration is interacting with your workspace, you will need to include the integration token in the `Authorization` header with every API request. + + Your integration can now make API requests related to this Notion page and any of its children. + +3. Get the details needed to create the templates with Baserow automation. + + Use the [List data source templates](https://developers.notion.com/reference/list-data-source-templates) endpoint to retrieve details of all page template IDs and titles available for a data source, or manually navigate to the template in the Notion app and get its ID. + + ```jsx + curl --request GET \ + --url 'https://api.notion.com/v1/data_sources/{data_source_id}/templates' \ + -H 'Notion-Version: 2025-09-03' \ + -H 'Authorization: Bearer '""$NOTION_API_KEY""'' + ``` + + **Create Page** + + Use the [Create a page](https://developers.notion.com/reference/post-page) API to duplicate the template into a new page. + + Provide a `page_id` or `data_source_id` under the `parent` parameter to create a page under an existing page or data source. To set up the page content, use the `template` body parameter to specify the data source template to be used to populate the content and properties of the new page. + + - `parent[type]` = `data_source_id` + - `parent[data_source_id]` = Get a data source ID from the Notion app → database settings → ""Manage data sources” → ""Copy data source ID"" button + - `template[type]` = `template_id` + - `template[template_id]` = ID of the template to apply to the newly created page + + ```jsx + curl 'https://api.notion.com/v1/pages' \ + -H 'Authorization: Bearer '""$NOTION_API_KEY""'' \ + -H ""Content-Type: application/json"" \ + -H ""Notion-Version: 2022-06-28"" \ + --data '{ + ""parent"": { ""data_source_id"": ""d9824bdc84454327be8b5b47500af6ce"" }, + ""template"": { + ""type"": ""template_id"", + ""template_id"": ""YOUR_TEMPLATE_GUID_HERE"" + }, + ""properties"": { + ""Name"": { ""title"": [{ ""text"": { ""content"": ""Tuscan Kale"" } }] }, + ""Content Type"": { ""select"": { ""name"": ""Blog"" } }, + ""Status"": { ""select"": { ""name"": ""Idea"" } }, + ""Author"": { ""people"": [{ ""id"": ""USER_ID"" }] }, + ""Publish Date"": { ""date"": { ""start"": ""2026-01-06"" } }, + ""Baserow Record URL"": { ""url"": ""https://baserow.io/example"" } + } + }' + ``` + + The keys to the `properties` object body param must also match the parent data source's properties (the data columns). Notion property breakdown for the columns: + + - **`Content Type` & `Status`:** Use the `select` object. Note that the value you provide (e.g., ""Blog"") must either already exist in the database options or the integration must have ""Insert"" permissions. + - **`Author`:** Uses the `people` array. You need the specific Notion **User ID** (found via the ""List all users"" endpoint) rather than a name string. + - **`Publish Date`:** Uses the `date` object with ISO 8601 format (`YYYY-MM-DD`). + + The Create Page API request is returned as a blank page with a new Page object until the template finishes applying, aside from any initial page `properties` set on the page. Store the ID of the newly created page in your app's backend storage systems. This will be necessary in the next step. + + **Retrieve a page** + + The webhook payload itself does not contain the page's data (like the Title or URL). It only tells you *which* page changed (`entity.id`) and *which* property was touched (`updated_properties`). To find the matching Baserow record, you need to perform a ""Retrieve a Page"" request to Notion using the ID from the webhook before you can talk to Baserow. + + ```jsx + curl 'https://api.notion.com/v1/pages/b55c9c91-384d-452b-81db-d1ef79372b75' \ + -H 'Notion-Version: 2025-09-03' \ + -H 'Authorization: Bearer '""$NOTION_API_KEY""'' + ``` + + +### 4. Automation to create pages from templates + +Next, we want to automate this process to duplicate the Notion page template for each new piece of content. + +1. Add a new automation in Baserow +2. Create two workflows to keep Notion and Baserow synchronized automatically. + +### Workflow A – New Content Record → Create Notion Page + +Whenever a new row is added to Baserow, we want a fully prepared Notion page to appear automatically. + +**Trigger:** → “Rows are created in Baserow table: Content Pipeline” + +**Actions:** + +1. **Send an HTTP request:** Use the **List data source templates** endpoint to retrieve details of all page template IDs +2. **Send an HTTP request:** Use the **List all users** endpoint to retrieve a paginated list of Users for the Author field. + + > Use the **Iterate on items** node to loop through the list. + > +3. **Send an HTTP request: Use the Create a page** endpoint ****to create a new page in Notion ****from the template + - Select HTTP method, input Endpoint URL, add headers, and add query parameter + - **Body content:** Map fields + - Title → Notion page title + - Type → “Content Type” property + - Author → “Author” property + - Status → “Status” property + - Publish Date → “Publish Date” property + - Baserow Record URL → “Baserow Record URL” property +4. **Update the same Baserow record:** Fill in the Baserow’s `Notion URL` field with the new Notion page link. The Baserow `row_id` value must be an integer or convertible to an integer. + +Start test run. The content will show up identically on each page created with the template. + +> Instead of repeatedly polling the Notion API to check if anything has changed, you can set up a webhook URL for your Notion integration and enable `page.created` and `page.content_updated` events. +> + +![Workflow A – New Content Record → Create Notion Page][2] + +### Workflow B – Status Update in Notion → Sync to Baserow + +We want any updates in Notion sync back to Baserow’s structured data. + +**Trigger:** → **Receive an HTTP request** “Notion database item updated” + +Visit your Notion integration settings → Navigate to the **Webhooks** tab and click **+ Create a subscription.** Enter the Baserow **Webhook URL** to send events. + +> When you create a subscription, Notion sends a one-time POST request to your webhook URL. The body of the request contains a `verification_token`, which proves that Notion can successfully reach your endpoint. Confirm that your endpoint can receive and respond to verification. +> + +**Actions:** + +1. **Send an HTTP request:** Use the **Retrieve a page** endpoint to fetch the Page object using the `entity.id` from the webhook. This will return the full JSON including the ""Name"" (Title) and the ""Baserow Record URL"" properties defined earlier. +2. **List multiple rows: Find a matching Baserow record** by Notion URL or Title. Use the Title or URL retrieved to filter Baserow. +3. **Update fields** in Baserow: + - `Status` + - `Publish Date` + - (Optionally `Notes`) +4. **Send a Slack message:** “When Status = Published → Send message to #content channel: + + ‘🎉 New post published: {{Title}} ({{Notion URL}})’” + + +Notion's systems will asynchronously begin processing a task to populate the page contents and properties based on the template. This way, both tools stay perfectly in sync, structured data remains clean, and documentation remains rich and readable. + +![Workflow B – Status Update in Notion → Sync to Baserow][3] + +## Best Practices + +By combining Notion and Baserow, we have integrated the creative, contextual, collaborative side with the operational, analytical, structured side. This creates a seamless workflow that handles how you think and how you execute. + +- **Keep structured data in Baserow:** anything you might want to filter, sort, or automate. +- **Keep contextual information in Notion:** anything narrative ~~or collaborative.~~ +- **Embed instead of duplicating data:** avoid versioning chaos. +- **Use views smartly in Baserow:** create filtered or aggregated views to show exactly what teams need. +- **Use templates in Notion:** connect them to the right Baserow datasets automatically for consistency. + +![Visual diagram (flowchart) of Notion ↔ Baserow integration workflow showing data flow, automation triggers, and team roles][4] + +--- + + +Still need help? If you're looking for something else, please feel free to make recommendations or ask us questions; we’re ready to assist you. + + - [Ask the Baserow community](https://community.baserow.io) + - [Contact support](/contact) for questions about Baserow or help with your account. + + + [1]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/42538bf9-16a3-4ab9-b98f-079acdcd9e34/Visual%20diagram%20(flowchart)%20of%20Notion%20%E2%86%94%20Baserow%20integration%20workflow%20showing%20data%20flow%2C%20automation%20triggers%2C%20and%20team%20roles.png + [2]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/d99a98fa-3207-4061-ad53-5fa70c2e46e0/Workflow%20A%20%E2%80%93%20New%20Content%20Record%20%E2%86%92%20Create%20Notion%20Page.png + [3]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/1b401607-c4c7-48a4-9b18-98c9f6e5599c/Workflow%20B%20%E2%80%93%20Status%20Update%20in%20Notion%20%E2%86%92%20Sync%20to%20Baserow.png + [4]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/526e741c-98d3-40cc-ad2d-3147fdfd7518/Notion%20__%20Baserow%20technical%20reference%20diagram.jpg",integrations,baserow_user_docs,https://baserow.io/user-docs/notion-integration +690,Power Automate,power-automate-integration,Integrate Baserow and Power Automate,"# Build a Baserow custom connector in Power Automate + +You have your data organized in Baserow. Now you want that data to *do* work. You want to automatically send emails when a status changes, sync rows to a SharePoint list, or generate contracts in Word based on your database fields. + +To do this, you need Microsoft Power Automate to build your own Custom Connector. Power Automate unlocks the entire Microsoft Ecosystem and complex logic. + +By the end of this guide, you will have: + +- A Baserow custom connector inside your Power Automate account. +- A flow that sends an email from Baserow data. +- A way to trigger flows instantly using Webhooks. + +![Build a Baserow custom connector in Power Automate][1] + +## Integration Architecture + +Before we build, let's look at the blueprint. + +1. **The Source:** Baserow holds the data. +2. **The Custom Connector:** This acts as a translator. It takes the ""Create Row"" action in Power Automate and translates it into the language Baserow understands (via API calls). +3. **The Flow:** This is the automation logic (e.g., ""If status is Done, send email""). + +> **Why use a Custom Connector instead of a generic HTTP Request?** +You can use generic HTTP requests, but it requires you to write JSON code every time you build a flow. A Custom Connector gives you a nice, easy-to-use menu where you just fill in the blanks, making it reusable for your whole team. +> + +## Prerequisites + +- **Baserow account:** You need a [Token](https://baserow.io/user-docs/personal-api-tokens). Baserow distinguishes between Data actions (which use the Database Token) and User/File actions (which require a JWT User Token). +- **[Power Automate](https://learn.microsoft.com/en-us/flow/sign-up-sign-in)** account with access to make flows. + +## Configuration guide integrating Baserow into the Microsoft Power Platform via OpenAPI + +To create a custom connector, you must define the API you want to connect to so that the connector understands the API's operations and data structures. + +In this guide, we will create a custom connector from scratch. + +> You can alternatively create a custom connector using Baserow [OpenAPI](https://api.baserow.io/api/redoc/) definition which you can import into Power Automate. The OpenAPI definition must be less than 1 MB and needs to be in OpenAPI 2.0 (formerly known as Swagger) format. +> + +### Step 1: Create the Custom Connector + +To create a custom connector from scratch in Power Automate: + +1. Log in to [make.powerautomate.com](https://www.google.com/search?q=https://make.powerautomate.com&authuser=3). +2. In the left sidebar, click **Data** → **Custom Connectors**. If the item isn’t in the side panel pane, select **…More** and then select the item you want. + + Alternatively, in the left pane, select **Solutions →** Edit or create an [unmanaged solution](https://learn.microsoft.com/en-us/power-apps/maker/data-platform/create-solution). + +3. Select **+** **New** **Custom connector** and select **Create from blank**. + + Alternatively, in the Solution page, select **+** **New** dropdown → **Automation** → **Custom connector** and select **Create from blank**. + +4. Enter the **Connector Name** `Baserow`. Select **Continue** to open your connector wizard. + +We will complete these five sections in Power Automate: General, Security, Definition, Code (optional), and Test. + +### Step 2: Update general details + +In the *General* section, we will give connector information such as the icon, description, scheme, host, and base URL: + +1. Click the **Upload connector icon** or **Upload** in the icon box to upload a PNG or JPG of the Baserow logo. Be sure that it's less than 1MB. You can also designate a background color for the icon. +2. In the **Description** field, enter a value. This description appears in the custom connector's details, and it can help others decide whether the connector might be useful to them. + + + | Parameter | Value | + | --- | --- | + | **Description** | “Simplified Baserow API for Power Automate with Webhook Trigger and Row Actions.” | + | **Connector's URL scheme** | “HTTPS” | + | **Host** | `api.baserow.io` (or your self-hosted domain). | + | **Base URL** | `/` | +3. Select **Security** at the bottom to go to the next section. + +The connector uses the API host and the base URL to determine how to call the API. The **Base URL is** the starting point for all API calls. + +![Create custom connector][2] + +### Step 3: **Specify authentication type** + +This is how Power Automate logs in to Baserow. + +There are several options available for authentication in custom connectors. Baserow uses API key authentication. + +1. In the *Security* section, under Authentication type, select **API Key** from the dropdown. +2. Under API Key, specify a **parameter label**, **name**, and **location**. The label is displayed when someone first makes a connection with the custom connector. The parameter name and location must match what the API expects. + + + | Parameter | Value | | + | --- | --- | --- | + | Parameter label | `Authorization` | This is what the user sees when they log in. | + | Parameter name | `Authorization` | This is the actual header name Baserow expects. | + | Parameter location | Header | | + +Select **Definition** at the bottom to go to the next section. + +![Specify authentication type][3] + +### The connector definition + +The *Definition* page of the custom connector wizard gives you many options for defining how your connector functions and how it's exposed in logic apps, flows, and apps. We will define actions and triggers in the following section. + +The left area will display any actions, triggers, and references that are defined for the connector. + +References are reusable parameters used by both actions and triggers. + +![The connector definition][4] + +### Step 4: Define the Actions + +Actions determine the operations that users can perform. + +1. In the *Definition* tab, select **New action**. +2. In the **General** area, add a summary, description, and operation ID for this action. + + + | Parameter | Value | + | --- | --- | + | **Summary** | “List Rows” | + | **Description** | “Lists all the rows of the table” | + | **Operation ID** | “ListRows” | + | **Visibility** | “**none”** | + + The **Visibility** property for operations and parameters has the following options: + + - **none**: Displayed normally in the flow + - **advanced**: Hidden under another menu + - **internal**: Hidden from the user + - **important**: Always shown to the user first +3. The **Request** area displays information based on the HTTP request for the action. Choose **Import from sample**. + + These actions interact with the table data directly. You must use the Token you generated in the Baserow settings. + + **Auth Details:** + + - **HTTP:** Database token + - **HTTP Authorization Scheme:** API Key (Header: `Authorization`) + - **Format:** `Token your_token_here` + + **Actions:** + + - **List tables** (`list_database_tables`) *Note: Ensure your token has ""Read"" permissions for the database.* + - **List fields** (`list_database_table_fields`) + - **List rows** (`list_database_table_rows`) + - **Get row** (`get_database_table_row`) + - **Create row** (`create_database_table_row`) + - **Update row** (`update_database_table_row`) + - **Delete row** (`delete_database_table_row`) +4. Specify the information necessary to connect to the API from the documentation, specify the request body (provided after the table), and then select **Import**. + + For list rows, verify that `user_field_names` is set to true by default. Sample request for [`list_database_table_rows`](https://api.baserow.io/api/redoc/#tag/Database-table-rows/operation/list_database_table_rows): + + ```markdown + https://api.baserow.io/api/database/rows/table/{table_id}/?user_field_names=true&page=1&size=100&search=""string"" + ``` + + Set the description, default values and visibility for each body property in the wizard. + + | Parameter | Value | + | --- | --- | + | Verb | `GET` | + | URL | `https://api.baserow.io/api/database/rows/table/{table_id}/` | + | Body | Use the example JSON. | +5. In the **Response** area, select **Add default response**. This is where you tell Power Automate what the output looks like. Specify the response body, and then select **Import**. This is provided in the API documentation. + + Sample default response for [`list_database_table_rows`](https://api.baserow.io/api/redoc/#tag/Database-table-rows/operation/list_database_table_rows): + + ```markdown + { + ""count"": 0, + ""next"": ""http://example.com"", + ""previous"": ""http://example.com"", + ""results"": [ + { + ""id"": 0, + ""order"": ""string"" + } + ] + } + ``` + +6. The **Validation** area displays any issues that are detected in the API definition. Fix any issues. You should see a green check mark when the definition validation succeeds. + +At the top right of the wizard, make sure the name is set, and then select **Create connector**. + +![Define the Actions][5] + +### Step 5: Test the Connection + +Now that you created the connector, test it to make sure it's working properly. + +1. Go to the **Test** tab. +2. Click **New Connection**. +3. A pop-up will appear asking for your API Key. You must type the word ""Token"" followed by a space, and then your key. Baserow distinguishes between Data actions (which use the Database Token) and User/File actions (which require a JWT User Token). + - *Format:* `Token eyJ0eXAiOiJK...` +4. Click **Create Connection**. +5. Back in the Test tab, select the refresh icon to make sure the connection information is updated. +6. Select an operation. +7. On the **Test** tab, enter a value for the [Table ID](https://baserow.io/user-docs/database-and-table-id) (the other fields use the defaults that you set earlier). + + The connector calls the API. + +8. Click **Test operation**. Review the response, you should see a status of `200`. + +![Test the Connection][6] + +Now that you created a custom connector and defined its behaviors, you can use the connector. + +You can also share a connector within your organization, or get the connector certified so that people outside your organization can use it. + +## Troubleshooting Common Issues + +| **Issue** | **Diagnosis** | **Solution** | +| --- | --- | --- | +| **401 Unauthorized** | The Connector was rejected. | You likely pasted just the API key. Edit the connection and ensure you type `Token [Your_Key]`. | +| **Flow Fails on ""Apply to Each""** | Power Automate can't find the list. | Ensure you are selecting the `results` list from the dynamic content, not the top-level body. | +| **Can't find column names** | Field ID confusion. | If you didn't use `user_field_names=true`, Baserow sends `field_45` instead of `Client Name`. You may need to look up your field IDs in Baserow to know which is which. | +| **""Bad Gateway"" or Timeout** | Data overload. | If fetching thousands of rows, Power Automate might time out. Use pagination or filtering in the `List rows` action to reduce data size. | + +## Best Practices + +- **Filter at the Source:** Don't pull 10,000 rows into Power Automate and filter them with a Condition action. Use the Baserow API filters in the ""List rows"" step (e.g., `filter__Status__equal=Done`) to only pull what you need. This saves ""Action Credits"" in Power Automate. +- **Secure your Keys:** Never share screenshots of your Flow that show your API Key. +- **Renaming Fields:** If you use the `user_field_names=true` parameter, be aware that if someone renames a column in Baserow, your Flow might break because it is looking for ""Status"" but now it's called ""Project Status"". For mission-critical flows, use the Field IDs (e.g., `field_123`) which never change. + +## Use Case 1: The ""Morning Report"" (Baserow → Email) + +Let's build a flow that sends you an email every morning with the status of your projects. + +1. Create a **Scheduled Cloud Flow**. Set it to repeat every 1 Day. +2. **New Step:** Click ""Add an action"". +3. Click the **Custom** tab. Select your new **Baserow** connector. +4. Select **List rows**. +5. **Table ID:** Enter your Table ID. +6. **Filters:** You can add `filter__field_123__equal` here if you want only ""Active"" projects. +7. **New Step:** Choose **Apply to each**. + - *Input:* Select `results` from the Baserow step dynamic content. +8. **Inside the Loop:** Add an action **Send an email (V2)**. + - *To:* Your email. + - *Subject:* Project Update: `[Name]` (Select Name from dynamic content). + - *Body:* The status is currently `[Status]`. + +## Use Case 2: The ""Instant Trigger"" (Baserow Webhooks → Power Automate) + +Scheduled flows are great, but sometimes you want something to happen *immediately* when a row is created. For this, we use Webhooks. + +**Part A: Prepare Power Automate** + +1. Create an **Automated Cloud Flow**. +2. **Trigger:** Search for `When an HTTP request is received` (Request connector). +3. **JSON Schema:** You can generate this by clicking ""Use sample payload"" and pasting a snippet of your Baserow JSON, or just leave it blank for now. +4. Save the Flow. +5. **Copy URL:** Once saved, the trigger will generate a specialized URL (HTTP POST URL). Copy this. + +**Part B: Configure Baserow** + +1. In your Baserow database, click **Webhooks** (left sidebar). +2. Click **+ Create Webhook**. +3. **URL:** Paste the long URL you got from Power Automate. +4. **Method:** POST. +5. **Events:** Check `rows.created` (or updated/deleted). +6. Click **Create**. + +**Part C: The Logic** + +Now, whenever you create a row in Baserow, it pings Power Automate. + +1. Back in Power Automate, add a **Parse JSON** step to understand the data Baserow sent. +2. Add your actions (e.g., ""Post message to Teams""). + +--- + + +Still need help? If you're looking for something else, please feel free to make recommendations or ask us questions; we’re ready to assist you. + + - [Ask the Baserow community](https://community.baserow.io) + - [Contact support](/contact) for questions about Baserow or help with your account. + + [1]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/8dc8b418-8d16-4f23-a0b3-5edcb46e8425/image.png + [2]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/397062b9-d550-4f3f-8503-6edbb57f9905/Create_custom_connector.jpg + [3]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/7e52af22-5c13-434d-9999-c8440af0f709/Specify_authentication_type.jpg + [4]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/231e249c-91c2-4f40-9cf8-c40ac904a151/The_connector_definition.jpg + [5]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/7f81b7fe-4f13-4a60-8942-bf533c119184/Define_the_Actions.jpg + [6]: https://baserow-backend-production20240528124524339000000001.s3.amazonaws.com/pagedown-uploads/9771c2c2-0469-4fa5-92bd-40323a0d57cf/Test_the_Connection.jpg",integrations,baserow_user_docs,https://baserow.io/user-docs/power-automate-integration 1,What is Baserow?,faq,What is Baserow?,"What is Baserow? Baserow is an open-source no-code database. Our database platform enables both non-technical and technical teams to capture, organize and build logical relationships between data to trigger decisions and automate processes. We focus on openness, scalability, performance, and data security.",faq,faq,https://baserow.io/faq diff --git a/enterprise/web-frontend/modules/baserow_enterprise/module.js b/enterprise/web-frontend/modules/baserow_enterprise/module.js index 5e90e236db..dcb6b17371 100644 --- a/enterprise/web-frontend/modules/baserow_enterprise/module.js +++ b/enterprise/web-frontend/modules/baserow_enterprise/module.js @@ -14,12 +14,6 @@ export default defineNuxtModule({ }, setup(options, nuxt) { const { resolve } = createResolver(import.meta.url) - /*let alreadyExtended = false - this.nuxt.hook('i18n:extend-messages', function (additionalMessages) { - if (alreadyExtended) return - additionalMessages.push({ en, fr, nl, de, es, it, pl, ko }) - alreadyExtended = true - })*/ // Register new alias to the web-frontend directory. nuxt.options.alias['@baserow_enterprise'] = path.resolve(__dirname, './') @@ -77,41 +71,19 @@ export default defineNuxtModule({ src: resolve('./plugins/realtime.js'), }) - // Remove the existing index route and add our own routes. - /*this.extendRoutes((configRoutes) => { - const settingsRoute = configRoutes.find( - (route) => route.name === 'settings' - ) - - // Prevent for adding the route multiple times - if (!settingsRoute.children.find(({ path }) => path === 'teams')) { - settingsRoute.children.push({ - name: 'settings-teams', - path: 'teams', - component: path.resolve(__dirname, 'pages/settings/teams.vue'), - }) - } - - configRoutes.push(...routes) - })*/ - addPlugin({ src: resolve('./plugin.js'), }) + // Runtime config defaults - values can be overridden at runtime via NUXT_ prefixed env vars + // See env-remap.mjs for the env var remapping that enables backwards compatibility Object.assign(nuxt.options.runtimeConfig.public, { - baserowEnterpriseAssistantLLMModel: - process.env.BASEROW_ENTERPRISE_ASSISTANT_LLM_MODEL || null, + baserowEnterpriseAssistantLLMModel: null, }) // Override Baserow's existing default.scss in favor of our own because that one // imports the original. We do this so that we can use the existing variables, // mixins, placeholders etc. nuxt.options.css[0] = path.resolve(__dirname, 'assets/scss/default.scss') - - /*if (this.options.publicRuntimeConfig) { - this.options.publicRuntimeConfig.BASEROW_ENTERPRISE_ASSISTANT_LLM_MODEL = - process.env.BASEROW_ENTERPRISE_ASSISTANT_LLM_MODEL || null - }*/ }, }) diff --git a/premium/web-frontend/modules/baserow_premium/module.js b/premium/web-frontend/modules/baserow_premium/module.js index e1fc8e5dc2..267209c328 100644 --- a/premium/web-frontend/modules/baserow_premium/module.js +++ b/premium/web-frontend/modules/baserow_premium/module.js @@ -18,13 +18,7 @@ export default defineNuxtModule({ // Register new alias to the web-frontend directory. nuxt.options.alias['@baserow_premium'] = resolve('./') - /*let alreadyExtended = false - this.nuxt.hook('i18n:extend-messages', function (additionalMessages) { - if (alreadyExtended) return - additionalMessages.push({ en, fr, nl, de, es, it, pl, ko }) - alreadyExtended = true - })*/ - + // Register locales nuxt.hook('i18n:registerModule', (register) => { register({ langDir: resolve('./locales'), @@ -53,19 +47,11 @@ export default defineNuxtModule({ // mixins, placeholders etc. nuxt.options.css[0] = resolve('./assets/scss/default.scss') + // Runtime config defaults - values can be overridden at runtime via NUXT_ prefixed env vars + // See env-remap.mjs for the env var remapping that enables backwards compatibility Object.assign(nuxt.options.runtimeConfig.public, { - baserowPremiumGroupedAggregateServiceMaxSeries: - process.env.BASEROW_PREMIUM_GROUPED_AGGREGATE_SERVICE_MAX_SERIES || 3, - baserowPricingUrl: process.env.BASEROW_PRICING_URL || null, + baserowPremiumGroupedAggregateServiceMaxSeries: 3, + baserowPricingUrl: null, }) - - /*if (this.options.publicRuntimeConfig) { - this.options.publicRuntimeConfig.BASEROW_PREMIUM_GROUPED_AGGREGATE_SERVICE_MAX_SERIES = - process.env.BASEROW_PREMIUM_GROUPED_AGGREGATE_SERVICE_MAX_SERIES || 3 - // This environment variable exist for the SaaS to override the pricing URL, so - // that the user can be redirected to the correct URL. - this.options.publicRuntimeConfig.BASEROW_PRICING_URL = - process.env.BASEROW_PRICING_URL || null - }*/ }, }) diff --git a/web-frontend/env-remap.mjs b/web-frontend/env-remap.mjs index 93be129547..3851247415 100644 --- a/web-frontend/env-remap.mjs +++ b/web-frontend/env-remap.mjs @@ -47,6 +47,11 @@ const envMapping = { BASEROW_DISABLE_SUPPORT: 'NUXT_PUBLIC_BASEROW_DISABLE_SUPPORT', BASEROW_INTEGRATIONS_PERIODIC_MINUTE_MIN: 'NUXT_PUBLIC_BASEROW_INTEGRATIONS_PERIODIC_MINUTE_MIN', + BASEROW_PREMIUM_GROUPED_AGGREGATE_SERVICE_MAX_SERIES: + 'NUXT_PUBLIC_BASEROW_PREMIUM_GROUPED_AGGREGATE_SERVICE_MAX_SERIES', + BASEROW_PRICING_URL: 'NUXT_PUBLIC_BASEROW_PRICING_URL', + BASEROW_ENTERPRISE_ASSISTANT_LLM_MODEL: + 'NUXT_PUBLIC_BASEROW_ENTERPRISE_ASSISTANT_LLM_MODEL', // Additional env vars SENTRY_DSN: 'NUXT_PUBLIC_SENTRY_CONFIG_DSN', diff --git a/web-frontend/modules/core/assets/scss/components/builder/data_source_item.scss b/web-frontend/modules/core/assets/scss/components/builder/data_source_item.scss index 8b7a37c44b..728d9bcf75 100644 --- a/web-frontend/modules/core/assets/scss/components/builder/data_source_item.scss +++ b/web-frontend/modules/core/assets/scss/components/builder/data_source_item.scss @@ -32,3 +32,8 @@ visibility: visible; } } + +.data-source-item__actions { + display: flex; + gap: 5px; +} diff --git a/web-frontend/modules/core/middleware/authentication.js b/web-frontend/modules/core/middleware/authentication.js index 656aeecc05..129c0937f5 100644 --- a/web-frontend/modules/core/middleware/authentication.js +++ b/web-frontend/modules/core/middleware/authentication.js @@ -32,37 +32,8 @@ export default defineNuxtRouteMiddleware(async (to) => { await store.dispatch('auth/refresh', refreshToken) } catch (error) { if (error.response?.status === 401) { - return navigateTo({ name: 'login' }) + return navigateTo({ name: 'login' }, { external: true }) // force browser 302 redirect to get rid of the jwt cookie in the request headers } } } }) - -/* -Previous Nuxt 2 middleware: -export default function ({ store, req, app, route, redirect }) { - // If nuxt generate or already authenticated, pass this middleware - if ((import.meta.server && !req) || store.getters['auth/isAuthenticated']) return - - const userSession = route.query.user_session - if (userSession) { - setUserSessionCookie(app, userSession) - } - - // token can be in the query string (SSO) or in the cookies (previous session) - let refreshToken = route.query.token - if (refreshToken) { - setToken(app, refreshToken) - } else { - refreshToken = getTokenIfEnoughTimeLeft(app) - } - - if (refreshToken) { - return store.dispatch('auth/refresh', refreshToken).catch((error) => { - if (error.response?.status === 401) { - return redirect({ name: 'login' }) - } - }) - } -} -*/ diff --git a/web-frontend/modules/core/middleware/urlCheck.js b/web-frontend/modules/core/middleware/urlCheck.js index c3ab24cb60..fbe824ad52 100644 --- a/web-frontend/modules/core/middleware/urlCheck.js +++ b/web-frontend/modules/core/middleware/urlCheck.js @@ -7,21 +7,8 @@ function isValidHttpUrl(rawString) { } } -function invalidUrlEnvVariable(envVariableName) { - /** - * This function lets us check on startup that a provided environment variable is - * a valid url. If we didn't do this then whenever the user would try to send a - * HTTP request they would get a mysterious 500 error raised by the http client. - * - * @type {string} - */ - - const envValue = process.env[envVariableName] - return envValue && !isValidHttpUrl(envValue) -} /** - * This middleware makes sure that the current user is admin else a 403 error - * will be shown to the user. + * This middleware validates that URL environment variables are properly configured. */ export default defineNuxtRouteMiddleware(() => { const event = import.meta.server ? useRequestEvent() : null @@ -35,16 +22,14 @@ export default defineNuxtRouteMiddleware(() => { if (import.meta.server && !event) return if (import.meta.server && !config.public.baserowDisablePublicUrlCheck) { - const urlEnvVarsToCheck = [] - if (process.env.BASEROW_PUBLIC_URL) { - urlEnvVarsToCheck.push('BASEROW_PUBLIC_URL') - } else { - urlEnvVarsToCheck.push('PUBLIC_BACKEND_URL', 'PUBLIC_WEB_FRONTEND_URL') + // Validate configured URLs + const urlsToCheck = { + publicBackendUrl: config.public.publicBackendUrl, + publicWebFrontendUrl: config.public.publicWebFrontendUrl, } - for (const name of urlEnvVarsToCheck) { - if (invalidUrlEnvVariable(name)) { - // noinspection HttpUrlsUsage + for (const [name, value] of Object.entries(urlsToCheck)) { + if (value && !isValidHttpUrl(value)) { throw createError({ statusCode: 500, hideBackButton: true, @@ -55,32 +40,3 @@ export default defineNuxtRouteMiddleware(() => { } } }) - -/* -Previous Nuxt 2 middleware: -export default function ({ store, req, error, i18n }) { - // If nuxt generate, pass this middleware - if (import.meta.server && !req) return - - if (import.meta.server && !process.env.BASEROW_DISABLE_PUBLIC_URL_CHECK) { - const urlEnvVarsToCheck = [] - if (process.env.BASEROW_PUBLIC_URL) { - urlEnvVarsToCheck.push('BASEROW_PUBLIC_URL') - } else { - urlEnvVarsToCheck.push('PUBLIC_BACKEND_URL', 'PUBLIC_WEB_FRONTEND_URL') - } - - for (const name of urlEnvVarsToCheck) { - if (invalidUrlEnvVariable(name)) { - // noinspection HttpUrlsUsage - return error({ - statusCode: 500, - hideBackButton: true, - message: i18n.t('urlCheck.invalidUrlEnvVarTitle', { name }), - content: i18n.t('urlCheck.invalidUrlEnvVarDescription', { name }), - }) - } - } - } -} -*/ diff --git a/web-frontend/modules/core/mixins/dropdown.js b/web-frontend/modules/core/mixins/dropdown.js index ca7955ddb3..aaee5831ab 100644 --- a/web-frontend/modules/core/mixins/dropdown.js +++ b/web-frontend/modules/core/mixins/dropdown.js @@ -174,7 +174,7 @@ export default { icon: null, query: '', hasItems: true, - hasDropdownItem: true, + hasDropdownItem: false, focusedDropdownItem: null, opening: false, fixedItemsImmutable: this.fixedItems, diff --git a/web-frontend/modules/core/module.js b/web-frontend/modules/core/module.js index d6f3653b17..dc94b0ac20 100644 --- a/web-frontend/modules/core/module.js +++ b/web-frontend/modules/core/module.js @@ -12,11 +12,9 @@ import { install, } from '@nuxt/kit' import { routes } from './routes' -import { setDefaultResultOrder } from 'node:dns' import _ from 'lodash' import defu from 'defu' import pathe from 'pathe' -import page from '../builder/services/page' import { readFileSync, writeFileSync, mkdirSync } from 'node:fs' import { createRequire } from 'node:module' @@ -102,6 +100,8 @@ export default defineNuxtModule({ nuxt.options.runtimeConfig.public = defu( nuxt.options.runtimeConfig.public, { + buildDate: new Date().toISOString(), + gitCommit: process.env.GITHUB_SHA?.slice(0, 7), downloadFileViaXhr: '0', baserowDisablePublicUrlCheck: false, publicBackendUrl: 'http://localhost:8000', diff --git a/web-frontend/modules/core/pages/debug.vue b/web-frontend/modules/core/pages/debug.vue new file mode 100644 index 0000000000..6ff84a7e83 --- /dev/null +++ b/web-frontend/modules/core/pages/debug.vue @@ -0,0 +1,79 @@ + + + + + diff --git a/web-frontend/modules/core/plugins/featureFlags.js b/web-frontend/modules/core/plugins/featureFlags.js index 8712e9ab71..301e9251c2 100644 --- a/web-frontend/modules/core/plugins/featureFlags.js +++ b/web-frontend/modules/core/plugins/featureFlags.js @@ -1,45 +1,8 @@ -/*const FF_ENABLE_ALL = '*' - -/** - * A comma separated list of feature flags used to enable in-progress or not ready - * features for developers. See docs/development/feature-flags.md for more info - * @param env The environment that should be used to get the flags from - * @returns {string[]} - *\/ -function getFeatureFlags(env = process.env) { - return (env.FEATURE_FLAGS || '') - .split(',') - .map((flag) => flag.trim().toLowerCase()) -} - -/** - * Checks if a feature is enabled - * @param featureFlags The list of feature flags - * @param flag The flag that is being checked for - * @returns {boolean|*} - *\/ -function featureFlagIsEnabled(featureFlags, flag) { - if (featureFlags.includes(FF_ENABLE_ALL)) { - return true - } else { - return featureFlags.includes(flag.toLowerCase()) - } -} - -export default function ({ app }, inject) { - const FEATURE_FLAGS = getFeatureFlags(app.$config) - - inject('featureFlagIsEnabled', (flag) => - featureFlagIsEnabled(FEATURE_FLAGS, flag) - ) -} -*/ - import { useRuntimeConfig } from '#imports' const FF_ENABLE_ALL = '*' -function getFeatureFlags(env = process.env) { +function getFeatureFlags(env) { return (env.FEATURE_FLAGS || '') .split(',') .map((flag) => flag.trim().toLowerCase()) diff --git a/web-frontend/modules/core/routes.js b/web-frontend/modules/core/routes.js index f0d4c8008b..412556f34b 100644 --- a/web-frontend/modules/core/routes.js +++ b/web-frontend/modules/core/routes.js @@ -1,7 +1,7 @@ import path from 'path' // Note that routes can't start with `/api/`, `/ws/` or `/media/` because they are -// reserved for the backend. In some cases, for example with the Heroku or Clouron +// reserved for the backend. In some cases, for example with the Heroku or Cloudron // deployment, the Baserow installation will share a single domain and port and then // those URLS are forwarded to the backend or media files server. The rest is // forwarded to the web-frontend. @@ -11,6 +11,11 @@ export const routes = [ path: '', file: path.resolve(__dirname, 'pages/index.vue'), }, + { + name: 'debug', + path: '/debug', + file: path.resolve(__dirname, 'pages/debug.vue'), + }, { name: 'login-pages', path: '', diff --git a/web-frontend/package.json b/web-frontend/package.json index 6f4faef093..c36f038a5b 100644 --- a/web-frontend/package.json +++ b/web-frontend/package.json @@ -7,7 +7,7 @@ "postinstall": "APP_ENV=dev nuxt prepare", "dev": "APP_ENV=dev node --import ./env-remap.mjs ./node_modules/.bin/nuxt dev", "build": "APP_ENV=production nuxt build", - "preview": "nuxt preview", + "preview": "node --import ./env-remap.mjs ./node_modules/.bin/nuxt preview", "prod": "node --import ./env-remap.mjs .output/server/index.mjs", "storybook": "storybook dev -p 6006 --host 0.0.0.0 --no-open", "test": "NODE_OPTIONS=\"--max-old-space-size=8192\" vitest --run $EXTRA_VITEST_PARAMS && vitest --run $EXTRA_VITEST_PARAMS --config ../premium/web-frontend/vitest.config.ts", @@ -135,7 +135,7 @@ "node-mocks-http": "^1.17.2", "postcss-scss": "^4.0.9", "prettier": "^3.7.4", - "storybook": "9.1.2", + "storybook": "9.1.17", "stylelint": "^16.26.1", "stylelint-config-standard": "^39.0.1", "stylelint-config-standard-scss": "^16.0.0", diff --git a/web-frontend/test/unit/builder/components/elements/components/__snapshots__/ChoiceElement.spec.js.snap b/web-frontend/test/unit/builder/components/elements/components/__snapshots__/ChoiceElement.spec.js.snap index 15f8d77205..fa0e54e7fc 100644 --- a/web-frontend/test/unit/builder/components/elements/components/__snapshots__/ChoiceElement.spec.js.snap +++ b/web-frontend/test/unit/builder/components/elements/components/__snapshots__/ChoiceElement.spec.js.snap @@ -46,12 +46,19 @@ exports[`ChoiceElement > as default 1`] = ` - +
+ + dropdown.empty + +
diff --git a/web-frontend/test/unit/builder/components/elements/components/__snapshots__/RecordSelectorElement.spec.js.snap b/web-frontend/test/unit/builder/components/elements/components/__snapshots__/RecordSelectorElement.spec.js.snap index 49b2d884e8..7b48c021f3 100644 --- a/web-frontend/test/unit/builder/components/elements/components/__snapshots__/RecordSelectorElement.spec.js.snap +++ b/web-frontend/test/unit/builder/components/elements/components/__snapshots__/RecordSelectorElement.spec.js.snap @@ -47,6 +47,7 @@ exports[`RecordSelectorElement > does not paginate if API returns 400/404 1`] = - +
+ + recordSelectorElement.emptyState + +
@@ -130,6 +137,7 @@ exports[`RecordSelectorElement > does not paginate if API returns 400/404 2`] = - +
+ + recordSelectorElement.emptyState + +
@@ -213,6 +227,7 @@ exports[`RecordSelectorElement > does not paginate if API returns 400/404 3`] = - +
+ + recordSelectorElement.emptyState + +
diff --git a/web-frontend/test/unit/core/components/__snapshots__/dropdown.spec.js.snap b/web-frontend/test/unit/core/components/__snapshots__/dropdown.spec.js.snap index fdbfca5cb1..a879a70e06 100644 --- a/web-frontend/test/unit/core/components/__snapshots__/dropdown.spec.js.snap +++ b/web-frontend/test/unit/core/components/__snapshots__/dropdown.spec.js.snap @@ -1,80 +1,5 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`Dropdown component > Test slots 1`] = ` - -`; - exports[`Dropdown component > With items 1`] = ` - +
+ + dropdown.empty + +
@@ -519,12 +451,19 @@ exports[`Dropdown component > basics 2`] = ` - +
+ + dropdown.empty + +
@@ -568,12 +507,19 @@ exports[`Dropdown component > basics 3`] = ` - +
+ + dropdown.empty + +
@@ -1147,387 +1093,3 @@ exports[`Dropdown component > slots 1`] = ` `; - -exports[`Dropdown component > test focus 1`] = ` - -`; - -exports[`Dropdown component > test focus 2`] = ` - -`; - -exports[`Dropdown component > test interactions 1`] = ` - -`; - -exports[`Dropdown component > test interactions 2`] = ` - -`; diff --git a/web-frontend/test/unit/database/__snapshots__/publicView.spec.js.snap b/web-frontend/test/unit/database/__snapshots__/publicView.spec.js.snap index f6fb5759f5..fa4fbdf42f 100644 --- a/web-frontend/test/unit/database/__snapshots__/publicView.spec.js.snap +++ b/web-frontend/test/unit/database/__snapshots__/publicView.spec.js.snap @@ -754,843 +754,3 @@ exports[`Public View Page Tests > Can see a publicly shared grid view 1`] = ` `; - -exports[`Public View Page Tests Can see a publicly shared grid view 1`] = ` -
-
-
- - - - - - - - - - - - -
- -
- - - - - - - - -
-
- -
-
-
-
- -
- - -
-
- - - -
- -
- -
-
- -
- -
- - -
- -
-
-
-
- -
- - - -
- -
-
-
- -
-
- - gridView.rowCount - 1 - -
- -
-
- -
-
- -
-
-
- - -
- - - -
- -
-
- - - -
-
-
- - - -
- -
- - - Last name - - -
- - - - - - - - - - - - -
-
-
-
-
-
- - - -
- -
- - - Notes - - -
- - - - - - - - - - - - -
-
-
-
-
-
- - - -
- -
- - - Active - - -
- - - - - - - - - - - - -
-
-
-
-
-
- - - -
- -
- - - Name - - -
- - - - - - - - - - - - -
-
-
- - -
- -
-
-
- - -
-
-
-
-
- - - -
- -
-
-
- -
- - -
-
-
- - - common.summarize - -
- -
-
-
-
-
- - - common.summarize - -
- -
-
-
-
-
- - - common.summarize - -
- -
-
-
-
-
- - - common.summarize - -
- -
-
-
-
- -
-
- -
-
-
- - -
-
-
-`; diff --git a/web-frontend/yarn.lock b/web-frontend/yarn.lock index c61c753cd5..a903f96832 100644 --- a/web-frontend/yarn.lock +++ b/web-frontend/yarn.lock @@ -9719,10 +9719,10 @@ std-env@^3.10.0, std-env@^3.7.0, std-env@^3.8.1: resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.10.0.tgz#d810b27e3a073047b2b5e40034881f5ea6f9c83b" integrity sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg== -storybook@9.1.2: - version "9.1.2" - resolved "https://registry.yarnpkg.com/storybook/-/storybook-9.1.2.tgz#b81a5d984caba3d88ce6b5cf405cb4970bad8672" - integrity sha512-TYcq7WmgfVCAQge/KueGkVlM/+g33sQcmbATlC3X6y/g2FEeSSLGrb6E6d3iemht8oio+aY6ld3YOdAnMwx45Q== +storybook@9.1.17: + version "9.1.17" + resolved "https://registry.yarnpkg.com/storybook/-/storybook-9.1.17.tgz#1d210ecb7e62260a68644d3e6cc81e04c0dcce85" + integrity sha512-kfr6kxQAjA96ADlH6FMALJwJ+eM80UqXy106yVHNgdsAP/CdzkkicglRAhZAvUycXK9AeadF6KZ00CWLtVMN4w== dependencies: "@storybook/global" "^5.0.0" "@testing-library/jest-dom" "^6.6.3"